feat: support configurable device access domain in proxy mode

Allow device remote access to use a different root domain from the Web UI when
running behind a reverse proxy.

Signed-off-by: GL.iNet-Yongping.Xie <yongping.xie@gl-inet.com>
This commit is contained in:
GL.iNet-Yongping.Xie
2026-01-06 23:13:39 -08:00
parent 1c473458cf
commit 8703f91ebf
8 changed files with 239 additions and 87 deletions
+25
View File
@@ -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:
// <deviceId>.<DEVICE_ENDPOINT_HOST>
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
}
+23
View File
@@ -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://<deviceId>.<DEVICE_ENDPOINT_HOST>/... (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=
+24 -1
View File
@@ -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://<deviceId>.<DEVICE_ENDPOINT_HOST>/... (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=
+34 -17
View File
@@ -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://<device_id>.example.com → 远程设备访问
```env
DEVICE_ENDPOINT_HOST=
```
- **仅在** `REVERSE_PROXY_ENABLED=true` 时生效
- 用于指定设备远程访问使用的域名
- 生成的设备访问地址格式为:
```text
https://<deviceId>.<DEVICE_ENDPOINT_HOST>/
```
**说明:**
- 不需要包含 `http(s)://` 或路径
- 可与 Web UI 域名不同
- 留空时,将从 `X-Forwarded-*` 请求头自动推导
**示例:**
```text
https://www.example.com → Web UI
https://<deviceId>.kvm.example.com → 设备远程访问
DEVICE_ENDPOINT_HOST=kvm.example.com
```
这两个地址可以共用 **同一个 HTTPS 端口(443)**,由反向代理根据访问的域名进行路由区分。
⚠️ **注意:所有配置均需在 `.env` 中完成,不需要修改 `docker-compose.yml`、模板或脚本。**
+60 -37
View File
@@ -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://<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.
#### 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://<deviceId>.<DEVICE_ENDPOINT_HOST>/
```
**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://<deviceId>.kvm.example.com → Device remote access
DEVICE_ENDPOINT_HOST=kvm.example.com
```
⚠️ **Note:** All configuration should be done in the `.env` file.
You dont need to modify `docker-compose.yml`, templates, or scripts directly.
+3
View File
@@ -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
+69 -31
View File
@@ -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: <deviceId>.<baseHost>
// 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()
}
+1 -1
View File
@@ -41,7 +41,7 @@ import (
)
const RttysVersion = "5.2.0"
const KVMCloudVersion = "v1.6.0"
const KVMCloudVersion = "v1.7.0"
var (
GitCommit = ""