diff --git a/config.go b/config.go index 60f295d..6a4217a 100644 --- a/config.go +++ b/config.go @@ -84,6 +84,13 @@ type Config struct { // ===================================================== // Enable proxy mode (app is behind Nginx/Traefik/Caddy/Cloudflare) ReverseProxyEnabled bool + + // ===================================================== + // Device Remote Access + // ===================================================== + // Host[:port] used to generate device remote access address: + // . + DeviceEndpointHost string } // docker mode fixed path for reading certificate @@ -268,6 +275,24 @@ func parseYamlCfg(cfg *Config, conf string) error { } } + if v := strings.TrimSpace(os.Getenv("DEVICE_ENDPOINT_HOST")); v != "" { + cleaned := v + // 1. Remove scheme if present (http:// or https://) + if idx := strings.Index(cleaned, "://"); idx != -1 { + cleaned = cleaned[idx+3:] + } + + // 2. Remove path/query/fragment if present + // Keep only host[:port] + if idx := strings.IndexAny(cleaned, "/?#"); idx != -1 { + cleaned = cleaned[:idx] + } + + // 3. Final trim + cleaned = strings.TrimSpace(cleaned) + cfg.DeviceEndpointHost = cleaned + } + return nil } diff --git a/docker-compose/.env.arm64.example b/docker-compose/.env.arm64.example index 0b3accc..ef0bec6 100755 --- a/docker-compose/.env.arm64.example +++ b/docker-compose/.env.arm64.example @@ -22,6 +22,29 @@ COTURN_IMAGE=coturn/coturn:edge-alpine-arm64v8 # https://github.com/gl-inet/glkvm-cloud/blob/main/docker-compose/nginx-reverse-proxy-example.conf REVERSE_PROXY_ENABLED=false +# ===================================================== +# Device Remote Access Domain (Reverse Proxy Mode Only) +# ===================================================== +# This option is used to generate the Remote Control URL for devices when +# running behind a reverse proxy. +# +# Effective ONLY when: +# REVERSE_PROXY_ENABLED=true +# +# When set, GLKVM Cloud will generate device access addresses as: +# https://./... (scheme is taken from X-Forwarded-Proto) +# +# Examples: +# DEVICE_ENDPOINT_HOST=kvm.example.com +# DEVICE_ENDPOINT_HOST=kvm.example.com:443 +# +# Notes: +# - Do NOT include scheme (http:// or https://) +# - Do NOT include path (/xxx) +# +# Leave empty to derive the host/port from X-Forwarded-* headers (auto-detect). +DEVICE_ENDPOINT_HOST= + # GLKVM access IP seen by devices/users. # Leave empty to auto-detect at container start. GLKVM_ACCESS_IP= diff --git a/docker-compose/.env.example b/docker-compose/.env.example index cf7af2b..c6cdbaa 100755 --- a/docker-compose/.env.example +++ b/docker-compose/.env.example @@ -22,7 +22,30 @@ COTURN_IMAGE=coturn/coturn:edge-alpine # 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. +# ===================================================== +# Device Remote Access Domain (Reverse Proxy Mode Only) +# ===================================================== +# This option is used to generate the Remote Control URL for devices when +# running behind a reverse proxy. +# +# Effective ONLY when: +# REVERSE_PROXY_ENABLED=true +# +# When set, GLKVM Cloud will generate device access addresses as: +# https://./... (scheme is taken from X-Forwarded-Proto) +# +# Examples: +# DEVICE_ENDPOINT_HOST=kvm.example.com +# DEVICE_ENDPOINT_HOST=kvm.example.com:443 +# +# Notes: +# - Do NOT include scheme (http:// or https://) +# - Do NOT include path (/xxx) +# +# Leave empty to derive the host/port from X-Forwarded-* headers (auto-detect). +DEVICE_ENDPOINT_HOST= + +GLKVM access IP seen by devices/users. # Leave empty to auto-detect at container start. GLKVM_ACCESS_IP= diff --git a/docker-compose/README-CN.md b/docker-compose/README-CN.md index 5ead0d4..ddc85ba 100755 --- a/docker-compose/README-CN.md +++ b/docker-compose/README-CN.md @@ -63,27 +63,24 @@ cd glkvm-cloud/docker-compose/ - `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)之后**: +启用后(`REVERSE_PROXY_ENABLED=true`): -- HTTPS 证书由反向代理管理(而不是由 GLKVM Cloud 本身管理) -- GLKVM Cloud 内部以明文 HTTP 方式监听 -- 同一个 HTTPS 端口可同时用于: - - 访问 GLKVM Cloud Web 管理界面 - - 访问远程 KVM 设备 +- GLKVM Cloud 运行在反向代理(如 Nginx)之后 +- TLS 由反向代理终止,GLKVM Cloud 内部使用 HTTP +- Web UI 与设备远程访问可共用同一个 HTTPS 端口(通常为 443) -例如,在正确配置 Nginx 的情况下: + +##### 必需的反向代理请求头 + +反向代理必须转发以下请求头,否则可能生成包含内部端口(如 `:10443`)的访问地址: ```nginx -# 转发原始的主机名、协议、端口以及客户端 IP -# 在反向代理模式下,这些 Header 是必须的 proxy_set_header Host $host; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -92,14 +89,34 @@ 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://.example.com → 远程设备访问 +```env +DEVICE_ENDPOINT_HOST= +``` + +- **仅在** `REVERSE_PROXY_ENABLED=true` 时生效 +- 用于指定设备远程访问使用的域名 +- 生成的设备访问地址格式为: + +```text +https://./ +``` + +**说明:** + +- 不需要包含 `http(s)://` 或路径 +- 可与 Web UI 域名不同 +- 留空时,将从 `X-Forwarded-*` 请求头自动推导 + +**示例:** + +```text +https://www.example.com → Web UI +https://.kvm.example.com → 设备远程访问 +DEVICE_ENDPOINT_HOST=kvm.example.com ``` -这两个地址可以共用 **同一个 HTTPS 端口(443)**,由反向代理根据访问的域名进行路由区分。 ⚠️ **注意:所有配置均需在 `.env` 中完成,不需要修改 `docker-compose.yml`、模板或脚本。** diff --git a/docker-compose/README.md b/docker-compose/README.md index bc59c1e..2b8e27b 100755 --- a/docker-compose/README.md +++ b/docker-compose/README.md @@ -62,43 +62,66 @@ - `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://.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. + +#### Reverse Proxy Mode (Optional) + +```env +REVERSE_PROXY_ENABLED=false +``` + +When enabled (`REVERSE_PROXY_ENABLED=true`): + +- GLKVM Cloud runs behind a reverse proxy (e.g. Nginx) +- TLS is terminated at the reverse proxy; GLKVM Cloud uses plain HTTP internally +- The Web UI and remote device access can share the same HTTPS port (usually 443) + + +##### Required Reverse Proxy Headers + +The reverse proxy **must** forward the following headers; otherwise, GLKVM Cloud may generate URLs containing internal ports (e.g. `:10443`): + +```nginx +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; +``` + + +##### Device Remote Access Domain (Optional) + +```env +DEVICE_ENDPOINT_HOST= +``` + +- **Effective only when** `REVERSE_PROXY_ENABLED=true` +- Used to specify the domain for device remote access +- Device access URLs are generated as: + +```text +https://./ +``` + +**Notes:** + +- Do not include the scheme (`http://` or `https://`) +- Do not include any path +- The domain may differ from the Web UI domain +- If left empty, the host/port will be derived from `X-Forwarded-*` headers + +**Example:** + +```text +https://www.example.com → Web UI +https://.kvm.example.com → Device remote access +DEVICE_ENDPOINT_HOST=kvm.example.com +``` + + + + ⚠️ **Note:** All configuration should be done in the `.env` file. You don’t need to modify `docker-compose.yml`, templates, or scripts directly. diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index d035834..3601da6 100755 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -53,6 +53,9 @@ services: # ---- Reverse Proxy ---- REVERSE_PROXY_ENABLED: ${REVERSE_PROXY_ENABLED:-false} + + # ---- Device Endpoint Host ---- + DEVICE_ENDPOINT_HOST: ${DEVICE_ENDPOINT_HOST:-} volumes: - ./templates/rttys.conf.template:/tpl/rttys.conf.tmpl:ro - ./scripts/docker-entrypoint.sh:/docker-entrypoint.sh:ro diff --git a/http.go b/http.go index fbf3d89..baee291 100644 --- a/http.go +++ b/http.go @@ -372,6 +372,12 @@ func httpProxyRedirect(srv *RttyServer, c *gin.Context, group string) { rawHost, xfHost, xfProto, xfPort, xRealIP, xFF, ) + // ------------------------------------------------- + // Proxy mode: + // 1) If DEVICE_ENDPOINT_HOST is configured, use it directly + // 2) Otherwise, fallback to forwarded-header logic + // ------------------------------------------------- + // 0) scheme: follow reverse proxy scheme := "" if v := strings.TrimSpace(c.GetHeader("X-Forwarded-Proto")); v != "" { @@ -382,44 +388,50 @@ func httpProxyRedirect(srv *RttyServer, c *gin.Context, group string) { 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 != "" { + // [A] Prefer explicit DEVICE_ENDPOINT_HOST if set + if v := strings.TrimSpace(cfg.DeviceEndpointHost); v != "" { + endpoint := v // already normalized when reading env: host[:port] only + + baseHost := endpoint + port := "" + if h, p, err := net.SplitHostPort(endpoint); err == nil { + baseHost = h 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) + // Build device host: . + // NOTE: DEVICE_ENDPOINT_HOST is a base domain (host[:port]) for device access, + baseHost = strings.TrimSuffix(strings.TrimSpace(baseHost), ".") + deviceHost := devid + if baseHost != "" { + deviceHost = devid + "." + baseHost } - } - // 4) Path: use the current request path - redirectPath := c.Request.URL.Path - if redirectPath == "" { - redirectPath = "/" - } + hostPort := joinHostPortIfNeeded(deviceHost, scheme, port) - u := &url.URL{ - Scheme: scheme, - Host: hostPort, - Path: redirectPath, - } - q := u.Query() - q.Set("sid", sid) - u.RawQuery = q.Encode() + redirectPath := c.Request.URL.Path + location = buildRedirectLocation(scheme, hostPort, redirectPath, sid) + log.Info().Msgf("Using domain redirect (proxy mode, DEVICE_ENDPOINT_HOST): %s", location) + } else { + // 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) - location = u.String() - log.Info().Msgf("Using domain redirect (proxy mode): %s", location) + // 3) Build host: in proxy mode redirect domain to be redirHost + hostPort := joinHostPortIfNeeded(redirHost, scheme, port) + + redirectPath := c.Request.URL.Path + location = buildRedirectLocation(scheme, hostPort, redirectPath, sid) + log.Info().Msgf("Using domain redirect (proxy mode): %s", location) + } } } @@ -728,3 +740,29 @@ func buildRedirectHost(hostname, devid string) string { return devid + "." + suffix } } + +func joinHostPortIfNeeded(host, scheme, port string) string { + if port == "" { + return host + } + // avoid adding default ports + if (scheme == "https" && port == "443") || (scheme == "http" && port == "80") { + return host + } + return net.JoinHostPort(host, port) +} + +func buildRedirectLocation(scheme, hostPort, path, sid string) string { + if path == "" { + path = "/" + } + u := &url.URL{ + Scheme: scheme, + Host: hostPort, + Path: path, + } + q := u.Query() + q.Set("sid", sid) + u.RawQuery = q.Encode() + return u.String() +} diff --git a/main.go b/main.go index 35eb07f..310a004 100644 --- a/main.go +++ b/main.go @@ -41,7 +41,7 @@ import ( ) const RttysVersion = "5.2.0" -const KVMCloudVersion = "v1.6.0" +const KVMCloudVersion = "v1.7.0" var ( GitCommit = ""