mirror of
https://github.com/gl-inet/glkvm-cloud.git
synced 2026-10-04 04:32:22 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4adb10f577 | |||
| cd231e996b | |||
| 739aa235b3 | |||
| 329468bf61 |
@@ -498,13 +498,20 @@ func (srv *RttyServer) ListenAPI() error {
|
||||
hostname = host // Use host directly if no port
|
||||
}
|
||||
|
||||
// Choose value by priority:
|
||||
// 1) If request host is a domain (not an IP), keep it.
|
||||
// 2) Else if it's an IP and cfg.WebrtcIP is set, use cfg.WebrtcIP.
|
||||
// 3) Else keep the request IP.
|
||||
chosen := hostname
|
||||
if isIP(hostname) && cfg.WebrtcIP != "" {
|
||||
chosen = cfg.WebrtcIP
|
||||
// -------- Reverse proxy mode: force IP ----------
|
||||
if cfg.ReverseProxyEnabled {
|
||||
// Reverse proxy mode: always use configured WebRTC IP
|
||||
if strings.TrimSpace(cfg.WebrtcIP) != "" {
|
||||
chosen = strings.TrimSpace(cfg.WebrtcIP)
|
||||
}
|
||||
} else {
|
||||
// -------- 3) Original behavior (unchanged) ----------
|
||||
// 1) If hostname is domain, keep it
|
||||
// 2) If hostname is IP and cfg.WebrtcIP is set, use cfg.WebrtcIP
|
||||
if isIP(hostname) && cfg.WebrtcIP != "" {
|
||||
chosen = cfg.WebrtcIP
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -524,7 +531,10 @@ func (srv *RttyServer) ListenAPI() error {
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
if cfg.SslCert != "" && cfg.SslKey != "" {
|
||||
// If we're behind a reverse proxy (TLS terminated by nginx), never enable TLS here.
|
||||
enableTLS := !cfg.ReverseProxyEnabled && cfg.SslCert != "" && cfg.SslKey != ""
|
||||
|
||||
if enableTLS {
|
||||
crt, err := tls.LoadX509KeyPair(cfg.SslCert, cfg.SslKey)
|
||||
if err != nil {
|
||||
log.Fatal().Msg(err.Error())
|
||||
|
||||
@@ -78,6 +78,12 @@ type Config struct {
|
||||
OIDCGenericAllowedSubs []string
|
||||
OIDCGenericAllowedUsernames []string
|
||||
OIDCGenericAllowedGroups []string
|
||||
|
||||
// =====================================================
|
||||
// Reverse Proxy / Proxy Mode
|
||||
// =====================================================
|
||||
// Enable proxy mode (app is behind Nginx/Traefik/Caddy/Cloudflare)
|
||||
ReverseProxyEnabled bool
|
||||
}
|
||||
|
||||
// docker mode fixed path for reading certificate
|
||||
@@ -251,6 +257,17 @@ func parseYamlCfg(cfg *Config, conf string) error {
|
||||
cfg.OIDCGenericAllowedGroups = splitScopes(s)
|
||||
}
|
||||
|
||||
// Reverse proxy mode is always read from environment variable
|
||||
// to avoid config drift when running behind different proxies per deployment.
|
||||
if v := strings.TrimSpace(os.Getenv("REVERSE_PROXY_ENABLED")); v != "" {
|
||||
// Accept common truthy values: "true/false", "1/0", "yes/no", "on/off"
|
||||
if b, err := strconv.ParseBool(v); err == nil {
|
||||
cfg.ReverseProxyEnabled = b
|
||||
} else {
|
||||
return fmt.Errorf("invalid REVERSE_PROXY_ENABLED value %q, expected boolean (true/false/1/0)", v)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,26 @@
|
||||
GLKVM_IMAGE=glzhitong/glkvm-cloud:latest
|
||||
COTURN_IMAGE=coturn/coturn:edge-alpine
|
||||
|
||||
# Enable reverse proxy mode (e.g. Nginx in front of GLKVM Cloud).
|
||||
# When enabled, TLS is handled by the proxy and GLKVM Cloud runs in plain HTTP.
|
||||
#
|
||||
# Note:
|
||||
# In reverse-proxy mode, remote device access depends on the correct forwarded headers
|
||||
# from the front-end proxy. If these headers are missing or incorrect, GLKVM Cloud may
|
||||
# generate redirect URLs with the internal port (e.g. :10443).
|
||||
#
|
||||
# Please make sure your Nginx config includes:
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Forwarded-Host $host;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# proxy_set_header X-Forwarded-Port $server_port;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
#
|
||||
# Reference (verified working example):
|
||||
# https://github.com/gl-inet/glkvm-cloud/blob/main/docker-compose/nginx-reverse-proxy-example.conf
|
||||
REVERSE_PROXY_ENABLED=false
|
||||
|
||||
# GLKVM access IP seen by devices/users.
|
||||
# Leave empty to auto-detect at container start.
|
||||
GLKVM_ACCESS_IP=
|
||||
|
||||
@@ -53,6 +53,44 @@ cp .env.example .env
|
||||
- `OIDC_ALLOWED_USERNAMES`:允许的用户名列表(可选)
|
||||
- `OIDC_ALLOWED_GROUPS`:允许的用户组列表(可选)
|
||||
|
||||
#### **反向代理模式(可选)**
|
||||
|
||||
```env
|
||||
# 启用反向代理模式(例如在 GLKVM Cloud 前使用 Nginx)
|
||||
# 启用后,TLS 由反向代理终止,GLKVM Cloud 内部使用明文 HTTP
|
||||
REVERSE_PROXY_ENABLED=false
|
||||
```
|
||||
|
||||
当 `REVERSE_PROXY_ENABLED` 设置为 `true` 时,GLKVM Cloud 将运行在 **反向代理(如 Nginx)之后**:
|
||||
|
||||
- HTTPS 证书由反向代理管理(而不是由 GLKVM Cloud 本身管理)
|
||||
- GLKVM Cloud 内部以明文 HTTP 方式监听
|
||||
- 同一个 HTTPS 端口可同时用于:
|
||||
- 访问 GLKVM Cloud Web 管理界面
|
||||
- 访问远程 KVM 设备
|
||||
|
||||
例如,在正确配置 Nginx 的情况下:
|
||||
|
||||
```nginx
|
||||
# 转发原始的主机名、协议、端口以及客户端 IP
|
||||
# 在反向代理模式下,这些 Header 是必须的
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
```
|
||||
|
||||
你可以通过以下地址访问:
|
||||
|
||||
```text
|
||||
https://www.example.com → GLKVM Cloud 管理界面
|
||||
https://<device_id>.example.com → 远程设备访问
|
||||
```
|
||||
|
||||
这两个地址可以共用 **同一个 HTTPS 端口(443)**,由反向代理根据访问的域名进行路由区分。
|
||||
|
||||
⚠️ **注意:所有配置均需在 `.env` 中完成,不需要修改 `docker-compose.yml`、模板或脚本。**
|
||||
|
||||
### 3. **启动服务**
|
||||
|
||||
@@ -54,9 +54,47 @@
|
||||
- `OIDC_ALLOWED_USERNAMES`: comma-separated list of allowed usernames (`preferred_username` or `name`) (optional)
|
||||
- `OIDC_ALLOWED_GROUPS`: comma-separated list of allowed OIDC groups (optional)
|
||||
|
||||
**Reverse Proxy Mode (Optional)**
|
||||
|
||||
```env
|
||||
# Enable reverse proxy mode (e.g. Nginx in front of GLKVM Cloud).
|
||||
# When enabled, TLS is terminated by the reverse proxy and GLKVM Cloud runs in plain HTTP.
|
||||
REVERSE_PROXY_ENABLED=false
|
||||
```
|
||||
|
||||
When `REVERSE_PROXY_ENABLED` is set to `true`, GLKVM Cloud is designed to run **behind a reverse proxy** such as Nginx:
|
||||
|
||||
- HTTPS certificates are managed by the reverse proxy (not by GLKVM Cloud itself)
|
||||
- GLKVM Cloud listens on plain HTTP internally
|
||||
- The same HTTPS port can be used for both:
|
||||
- Accessing the GLKVM Cloud web UI
|
||||
- Accessing remote KVM devices
|
||||
|
||||
For example, with proper Nginx configuration,
|
||||
|
||||
```nginx
|
||||
# Forward original host, scheme, port and client IP
|
||||
# These headers are required when running GLKVM Cloud behind a reverse proxy.
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
```
|
||||
|
||||
you can use:
|
||||
|
||||
```text
|
||||
https://www.example.com → GLKVM Cloud web interface
|
||||
https://<device_id>.example.com → Remote device access
|
||||
```
|
||||
|
||||
Both addresses can share the **same HTTPS port (443)**, while routing is handled by the reverse proxy based on the domain name.
|
||||
|
||||
⚠️ **Note:** All configuration should be done in the `.env` file.
|
||||
You don’t need to modify `docker-compose.yml`, templates, or scripts directly.
|
||||
|
||||
|
||||
3. **Start the services**
|
||||
|
||||
```bash
|
||||
|
||||
@@ -49,6 +49,9 @@ services:
|
||||
OIDC_ALLOWED_SUBS: ${OIDC_ALLOWED_SUBS:-}
|
||||
OIDC_ALLOWED_USERNAMES: ${OIDC_ALLOWED_USERNAMES:-}
|
||||
OIDC_ALLOWED_GROUPS: ${OIDC_ALLOWED_GROUPS:-}
|
||||
|
||||
# ---- Reverse Proxy ----
|
||||
REVERSE_PROXY_ENABLED: ${REVERSE_PROXY_ENABLED:-false}
|
||||
volumes:
|
||||
- ./templates/rttys.conf.template:/tpl/rttys.conf.tmpl:ro
|
||||
- ./scripts/docker-entrypoint.sh:/docker-entrypoint.sh:ro
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
# =========================================================
|
||||
# GLKVM Cloud - Reverse Proxy Mode (Nginx Example)
|
||||
#
|
||||
# This configuration shows how to run GLKVM Cloud behind
|
||||
# Nginx in reverse proxy mode.
|
||||
#
|
||||
# - TLS is terminated by Nginx
|
||||
# - GLKVM Cloud listens on plain HTTP internally
|
||||
# - Web UI and remote device access share the same HTTPS port
|
||||
# - Routing is based on the requested domain name
|
||||
# =========================================================
|
||||
|
||||
# WebSocket connection helper
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
# --- Web UI: https://www.example.com ---
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name www.example.com;
|
||||
|
||||
ssl_certificate /path/to/fullchain.pem;
|
||||
ssl_certificate_key /path/to/privkey.pem;
|
||||
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
|
||||
location / {
|
||||
proxy_http_version 1.1;
|
||||
|
||||
# Required forwarded headers for reverse proxy mode
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
# WebSocket support
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
|
||||
# GLKVM Cloud web service (HTTP)
|
||||
proxy_pass http://127.0.0.1:1443;
|
||||
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
}
|
||||
|
||||
# --- Device Access: https://<device_id>.example.com ---
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name *.example.com;
|
||||
|
||||
ssl_certificate /path/to/fullchain.pem;
|
||||
ssl_certificate_key /path/to/privkey.pem;
|
||||
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
|
||||
location / {
|
||||
proxy_http_version 1.1;
|
||||
|
||||
# Required forwarded headers for reverse proxy mode
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
# WebSocket support
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
|
||||
# GLKVM Cloud device access service (HTTP)
|
||||
proxy_pass http://127.0.0.1:10443;
|
||||
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,9 @@ func (srv *RttyServer) ListenHttpProxy() {
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
if cfg.SslCert != "" && cfg.SslKey != "" {
|
||||
// In reverse proxy mode (TLS terminated by nginx), never enable TLS here.
|
||||
enableTLS := !cfg.ReverseProxyEnabled && cfg.SslCert != "" && cfg.SslKey != ""
|
||||
if enableTLS {
|
||||
crt, err := tls.LoadX509KeyPair(cfg.SslCert, cfg.SslKey)
|
||||
if err != nil {
|
||||
log.Fatal().Msg(err.Error())
|
||||
@@ -344,8 +346,81 @@ func httpProxyRedirect(srv *RttyServer, c *gin.Context, group string) {
|
||||
log.Info().Msgf("Using IP redirect: %s", location)
|
||||
} else {
|
||||
redirHost := buildRedirectHost(hostname, devid)
|
||||
location = fmt.Sprintf("https://%s%s?sid=%s", redirHost, cfg.AddrHttpProxy, sid)
|
||||
log.Info().Msgf("Using domain redirect: %s", location)
|
||||
// Keep original behavior when NOT in reverse proxy mode
|
||||
if !cfg.ReverseProxyEnabled {
|
||||
location = fmt.Sprintf("https://%s%s?sid=%s", redirHost, cfg.AddrHttpProxy, sid)
|
||||
log.Info().Msgf("Using domain redirect: %s", location)
|
||||
} else {
|
||||
// ---- verify forwarded headers from reverse proxy ----
|
||||
rawHost := c.GetHeader("Host")
|
||||
xfHost := c.GetHeader("X-Forwarded-Host")
|
||||
xfProto := c.GetHeader("X-Forwarded-Proto")
|
||||
xfPort := c.GetHeader("X-Forwarded-Port")
|
||||
xRealIP := c.GetHeader("X-Real-IP")
|
||||
xFF := c.GetHeader("X-Forwarded-For")
|
||||
|
||||
log.Info().Msgf(
|
||||
"reverse-proxy info: method=%s uri=%s host=%q tls=%v remoteIP=%q",
|
||||
c.Request.Method,
|
||||
c.Request.URL.String(),
|
||||
rawHost,
|
||||
c.Request.TLS != nil,
|
||||
c.ClientIP(),
|
||||
)
|
||||
log.Info().Msgf(
|
||||
"reverse-proxy headers: Host=%q X-Forwarded-Host=%q X-Forwarded-Proto=%q X-Forwarded-Port=%q X-Real-IP=%q X-Forwarded-For=%q",
|
||||
rawHost, xfHost, xfProto, xfPort, xRealIP, xFF,
|
||||
)
|
||||
|
||||
// 0) scheme: follow reverse proxy
|
||||
scheme := ""
|
||||
if v := strings.TrimSpace(c.GetHeader("X-Forwarded-Proto")); v != "" {
|
||||
scheme = strings.ToLower(strings.Split(v, ",")[0])
|
||||
} else if c.Request.TLS != nil {
|
||||
scheme = "https"
|
||||
} else {
|
||||
scheme = "http"
|
||||
}
|
||||
|
||||
// 1) external port: prefer the one user actually accessed
|
||||
port := ""
|
||||
if fp := strings.TrimSpace(c.GetHeader("X-Forwarded-Port")); fp != "" {
|
||||
port = strings.TrimSpace(strings.Split(fp, ",")[0])
|
||||
} else if fh := strings.TrimSpace(c.GetHeader("X-Forwarded-Host")); fh != "" {
|
||||
fh = strings.TrimSpace(strings.Split(fh, ",")[0])
|
||||
if _, p, err := net.SplitHostPort(fh); err == nil && p != "" {
|
||||
port = p
|
||||
}
|
||||
}
|
||||
log.Info().Msgf("port: %s", port)
|
||||
|
||||
// 3) Build host: in proxy mode redirect domain to be redirHost
|
||||
hostPort := redirHost
|
||||
if port != "" {
|
||||
// avoid adding default ports
|
||||
if (scheme == "https" && port != "443") || (scheme == "http" && port != "80") {
|
||||
hostPort = net.JoinHostPort(redirHost, port)
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Path: use the current request path
|
||||
redirectPath := c.Request.URL.Path
|
||||
if redirectPath == "" {
|
||||
redirectPath = "/"
|
||||
}
|
||||
|
||||
u := &url.URL{
|
||||
Scheme: scheme,
|
||||
Host: hostPort,
|
||||
Path: redirectPath,
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("sid", sid)
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
location = u.String()
|
||||
log.Info().Msgf("Using domain redirect (proxy mode): %s", location)
|
||||
}
|
||||
}
|
||||
|
||||
log.Info().Msgf("Final redirect location: %s", location)
|
||||
|
||||
Reference in New Issue
Block a user