Przeglądaj źródła

fix(sub): honor trustedProxyCIDRs before forwarded URLs (#6135)

* fix(sub): honor trustedProxyCIDRs before forwarded URLs

* fix(sub): avoid unused trust-setting lookups

Skip the trustedProxyCIDRs lookup when no forwarded header can affect a subscription URL. Keep the shipped proxy default in one exported setting constant and document the subscription-link behavior for custom proxy boundaries.

* fix(frontend): meet config text contrast requirements

Keep compact configuration text readable in the light theme and satisfy the Storybook accessibility check.

---------

Co-authored-by: PathGao <[email protected]>
PathGao 11 godzin temu
rodzic
commit
ad288a7ecc

+ 1 - 1
docs/content/docs/en/config/panel.mdx

@@ -19,7 +19,7 @@ browser in full.
 | `webBasePath`       | `/`                     | URL path the panel is served under (always normalized to `/…/`).        |
 | `webBasePath`       | `/`                     | URL path the panel is served under (always normalized to `/…/`).        |
 | `webCertFile` / `webKeyFile` | _(none)_       | TLS certificate + key. When both are set, the panel serves **HTTPS**.   |
 | `webCertFile` / `webKeyFile` | _(none)_       | TLS certificate + key. When both are set, the panel serves **HTTPS**.   |
 | `sessionMaxAge`     | `360`                   | Session lifetime in **minutes** (default 6 hours).                      |
 | `sessionMaxAge`     | `360`                   | Session lifetime in **minutes** (default 6 hours).                      |
-| `trustedProxyCIDRs` | `127.0.0.1/32,::1/128`  | IPs/CIDRs whose forwarded headers (real client IP) are trusted.         |
+| `trustedProxyCIDRs` | `127.0.0.1/32,::1/128`  | IPs/CIDRs whose forwarded headers (real client IP) are trusted. A custom value also controls forwarded host and scheme in subscription links; include the subscription proxy or set `subURI` to override those links. |
 | `panelOutbound`     | _(none)_                | Route the panel's own egress (update checks, Telegram, geo/sub fetches) through a named Xray outbound. |
 | `panelOutbound`     | _(none)_                | Route the panel's own egress (update checks, Telegram, geo/sub fetches) through a named Xray outbound. |
 
 
 After changing the port or base path, the panel URL becomes
 After changing the port or base path, the panel URL becomes

+ 1 - 1
docs/content/docs/fa/config/panel.mdx

@@ -19,7 +19,7 @@ icon: SlidersHorizontal
 | `webBasePath`       | `/`                     | مسیر URLی که پنل زیر آن ارائه می‌شود (همیشه به شکل `/…/` نرمال‌سازی می‌شود).        |
 | `webBasePath`       | `/`                     | مسیر URLی که پنل زیر آن ارائه می‌شود (همیشه به شکل `/…/` نرمال‌سازی می‌شود).        |
 | `webCertFile` / `webKeyFile` | _(هیچ‌کدام)_   | گواهی + کلید TLS. وقتی هر دو تنظیم شوند، پنل با **HTTPS** ارائه می‌شود.   |
 | `webCertFile` / `webKeyFile` | _(هیچ‌کدام)_   | گواهی + کلید TLS. وقتی هر دو تنظیم شوند، پنل با **HTTPS** ارائه می‌شود.   |
 | `sessionMaxAge`     | `360`                   | طول عمر نشست بر حسب **دقیقه** (پیش‌فرض ۶ ساعت).                      |
 | `sessionMaxAge`     | `360`                   | طول عمر نشست بر حسب **دقیقه** (پیش‌فرض ۶ ساعت).                      |
-| `trustedProxyCIDRs` | `127.0.0.1/32,::1/128`  | IPها/CIDRهایی که هدرهای فورواردشده‌شان (IP واقعی کلاینت) مورد اعتماد است.         |
+| `trustedProxyCIDRs` | `127.0.0.1/32,::1/128`  | IPها/CIDRهایی که هدرهای فورواردشده‌شان (IP واقعی کلاینت) مورد اعتماد است. مقدار سفارشی همچنین میزبان و طرحِ لینک‌های اشتراک را کنترل می‌کند؛ پراکسی اشتراک را اضافه کنید یا برای بازنویسی این لینک‌ها `subURI` را تنظیم کنید. |
 | `panelOutbound`     | _(هیچ‌کدام)_                | مسیریابی خروجیِ خود پنل (بررسی به‌روزرسانی‌ها، Telegram، واکشی geo/sub) از طریق یک خروجی Xray با نام مشخص. |
 | `panelOutbound`     | _(هیچ‌کدام)_                | مسیریابی خروجیِ خود پنل (بررسی به‌روزرسانی‌ها، Telegram، واکشی geo/sub) از طریق یک خروجی Xray با نام مشخص. |
 
 
 پس از تغییر پورت یا مسیر پایه، آدرس پنل به‌صورت
 پس از تغییر پورت یا مسیر پایه، آدرس پنل به‌صورت

+ 1 - 1
docs/content/docs/ru/config/panel.mdx

@@ -19,7 +19,7 @@ icon: SlidersHorizontal
 | `webBasePath`       | `/`                     | URL-путь, по которому обслуживается панель (всегда нормализуется к `/…/`). |
 | `webBasePath`       | `/`                     | URL-путь, по которому обслуживается панель (всегда нормализуется к `/…/`). |
 | `webCertFile` / `webKeyFile` | _(нет)_        | Сертификат TLS + ключ. Когда заданы оба, панель обслуживается по **HTTPS**. |
 | `webCertFile` / `webKeyFile` | _(нет)_        | Сертификат TLS + ключ. Когда заданы оба, панель обслуживается по **HTTPS**. |
 | `sessionMaxAge`     | `360`                   | Время жизни сессии в **минутах** (по умолчанию 6 часов).                |
 | `sessionMaxAge`     | `360`                   | Время жизни сессии в **минутах** (по умолчанию 6 часов).                |
-| `trustedProxyCIDRs` | `127.0.0.1/32,::1/128`  | IP-адреса/CIDR, чьим переадресованным заголовкам (реальный IP клиента) можно доверять. |
+| `trustedProxyCIDRs` | `127.0.0.1/32,::1/128`  | IP-адреса/CIDR, чьим переадресованным заголовкам (реальный IP клиента) можно доверять. Пользовательское значение также управляет пересылаемыми хостом и схемой в ссылках подписки; добавьте прокси подписки или задайте `subURI`, чтобы переопределить эти ссылки. |
 | `panelOutbound`     | _(нет)_                 | Маршрутизация собственного исходящего трафика панели (проверка обновлений, Telegram, запросы geo/подписок) через именованный исходящий канал Xray. |
 | `panelOutbound`     | _(нет)_                 | Маршрутизация собственного исходящего трафика панели (проверка обновлений, Telegram, запросы geo/подписок) через именованный исходящий канал Xray. |
 
 
 После изменения порта или базового пути URL панели становится
 После изменения порта или базового пути URL панели становится

+ 1 - 1
docs/content/docs/zh/config/panel.mdx

@@ -15,7 +15,7 @@ icon: SlidersHorizontal
 | `webBasePath`       | `/`                     | 面板对外提供服务所使用的 URL 路径(始终规范化为 `/…/`)。               |
 | `webBasePath`       | `/`                     | 面板对外提供服务所使用的 URL 路径(始终规范化为 `/…/`)。               |
 | `webCertFile` / `webKeyFile` | _(无)_       | TLS 证书 + 密钥。两者都设置后,面板将以 **HTTPS** 提供服务。           |
 | `webCertFile` / `webKeyFile` | _(无)_       | TLS 证书 + 密钥。两者都设置后,面板将以 **HTTPS** 提供服务。           |
 | `sessionMaxAge`     | `360`                   | 会话有效期,单位为**分钟**(默认 6 小时)。                              |
 | `sessionMaxAge`     | `360`                   | 会话有效期,单位为**分钟**(默认 6 小时)。                              |
-| `trustedProxyCIDRs` | `127.0.0.1/32,::1/128`  | 其转发头(真实客户端 IP)受信任的 IP/CIDR。                            |
+| `trustedProxyCIDRs` | `127.0.0.1/32,::1/128`  | 其转发头(真实客户端 IP)受信任的 IP/CIDR。自定义值还会控制订阅链接中转发的主机和协议;请将订阅代理加入列表,或设置 `subURI` 覆盖这些链接。 |
 | `panelOutbound`     | _(无)_                | 通过一个命名的 Xray 出站来路由面板自身的出口流量(更新检查、Telegram、地理/订阅拉取)。 |
 | `panelOutbound`     | _(无)_                | 通过一个命名的 Xray 出站来路由面板自身的出口流量(更新检查、Telegram、地理/订阅拉取)。 |
 
 
 更改端口或基础路径后,面板 URL 将变为
 更改端口或基础路径后,面板 URL 将变为

+ 4 - 0
frontend/src/components/clients/ConfigBlock.css

@@ -16,6 +16,10 @@ body.light .config-block .ant-tag.ant-tag-filled.ant-tag-gold {
   color: #874d00;
   color: #874d00;
 }
 }
 
 
+body.light .config-block-text {
+  color: #595959;
+}
+
 .config-block .ant-collapse-extra {
 .config-block .ant-collapse-extra {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;

+ 92 - 0
internal/sub/forwarded_trust.go

@@ -0,0 +1,92 @@
+package sub
+
+import (
+	"net"
+	"net/netip"
+	"strings"
+	"sync"
+
+	"github.com/gin-gonic/gin"
+
+	"github.com/mhsanaei/3x-ui/v3/internal/logger"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service"
+)
+
+var warnSuppressedForwardedOnce sync.Once
+
+var forwardedHeaderNames = [...]string{"X-Forwarded-Host", "X-Forwarded-Proto", "X-Real-IP"}
+
+func (s *SubService) forwardedHeadersTrusted(c *gin.Context) (trusted bool) {
+	if !hasForwardedHeaders(c) {
+		return true
+	}
+
+	trusted = true
+	defer func() {
+		_ = recover()
+	}()
+
+	configured, err := s.settingService.GetTrustedProxyCIDRs()
+	if err != nil {
+		return true
+	}
+	configured = strings.TrimSpace(configured)
+	if configured == "" || configured == service.DefaultTrustedProxyCIDRs {
+		return true
+	}
+	return remoteAddrInCIDRs(c.Request.RemoteAddr, configured)
+}
+
+func hasForwardedHeaders(c *gin.Context) bool {
+	for _, name := range forwardedHeaderNames {
+		if c.GetHeader(name) != "" {
+			return true
+		}
+	}
+	return false
+}
+
+func warnSuppressedForwardedHeaders(c *gin.Context) {
+	present := make([]string, 0, 3)
+	for _, name := range forwardedHeaderNames {
+		if c.GetHeader(name) != "" {
+			present = append(present, name)
+		}
+	}
+	if len(present) == 0 {
+		return
+	}
+	headers := strings.Join(present, ", ")
+	logger.Debugf("sub: ignoring %s from %s, which is outside trustedProxyCIDRs", headers, c.Request.RemoteAddr)
+	warnSuppressedForwardedOnce.Do(func() {
+		logger.Warningf("sub: ignoring %s from %s because it is outside trustedProxyCIDRs; subscription URLs will use the request host. Add the proxy to that setting, or set subURI, if the generated links look wrong.", headers, c.Request.RemoteAddr)
+	})
+}
+
+func remoteAddrInCIDRs(remoteAddr, cidrs string) bool {
+	host := remoteAddr
+	if h, _, err := net.SplitHostPort(remoteAddr); err == nil {
+		host = h
+	}
+	addr, err := netip.ParseAddr(strings.TrimSpace(host))
+	if err != nil {
+		return false
+	}
+	addr = addr.Unmap()
+	for value := range strings.SplitSeq(cidrs, ",") {
+		value = strings.TrimSpace(value)
+		if value == "" {
+			continue
+		}
+		if prefix, err := netip.ParsePrefix(value); err == nil {
+			if prefix.Contains(addr) {
+				return true
+			}
+			continue
+		}
+		if proxyIP, err := netip.ParseAddr(value); err == nil && proxyIP.Unmap() == addr.Unmap() {
+			return true
+		}
+	}
+	return false
+}

+ 205 - 0
internal/sub/forwarded_trust_test.go

@@ -0,0 +1,205 @@
+package sub
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/gin-gonic/gin"
+
+	"github.com/mhsanaei/3x-ui/v3/internal/database"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service"
+)
+
+func requestFrom(t *testing.T, remoteAddr string, headers map[string]string) *gin.Context {
+	t.Helper()
+	req := httptest.NewRequest(http.MethodGet, "/sub/abc", nil)
+	req.Host = "panel.example.com:2096"
+	req.RemoteAddr = remoteAddr
+	for k, v := range headers {
+		req.Header.Set(k, v)
+	}
+	c, _ := gin.CreateTestContext(httptest.NewRecorder())
+	c.Request = req
+	return c
+}
+
+func setTrustedProxyCIDRs(t *testing.T, value string) {
+	t.Helper()
+	if err := database.GetDB().Create(&model.Setting{Key: "trustedProxyCIDRs", Value: value}).Error; err != nil {
+		t.Fatalf("set trustedProxyCIDRs: %v", err)
+	}
+	settingService := service.SettingService{}
+	stored, err := settingService.GetTrustedProxyCIDRs()
+	if err != nil {
+		t.Fatalf("read trustedProxyCIDRs back through SettingService: %v", err)
+	}
+	if stored != value {
+		t.Fatalf("SettingService reads trustedProxyCIDRs as %q, want %q — the key this helper writes has drifted", stored, value)
+	}
+}
+
+func storedAs(value string) *string {
+	return &value
+}
+
+func TestResolveRequest_ForwardedHeaderTrust(t *testing.T) {
+	tests := []struct {
+		name             string
+		stored           *string
+		remoteAddr       string
+		wantScheme       string
+		wantHost         string
+		wantHostWithPort string
+		wantHostHeader   string
+	}{
+		{
+			name:             "no stored row keeps trusting forwarded headers",
+			stored:           nil,
+			remoteAddr:       "203.0.113.9:51000",
+			wantScheme:       "https",
+			wantHost:         "sub.example.net",
+			wantHostWithPort: "sub.example.net",
+			wantHostHeader:   "sub.example.net",
+		},
+		{
+			name:             "empty stored value keeps trusting forwarded headers",
+			stored:           storedAs(""),
+			remoteAddr:       "203.0.113.9:51000",
+			wantScheme:       "https",
+			wantHost:         "sub.example.net",
+			wantHostWithPort: "sub.example.net",
+			wantHostHeader:   "sub.example.net",
+		},
+		{
+			name:             "stored shipped default keeps trusting forwarded headers",
+			stored:           storedAs(service.DefaultTrustedProxyCIDRs),
+			remoteAddr:       "203.0.113.9:51000",
+			wantScheme:       "https",
+			wantHost:         "sub.example.net",
+			wantHostWithPort: "sub.example.net",
+			wantHostHeader:   "sub.example.net",
+		},
+		{
+			name:             "declared boundary ignores an origin outside it",
+			stored:           storedAs("10.0.0.0/8"),
+			remoteAddr:       "203.0.113.9:51000",
+			wantScheme:       "http",
+			wantHost:         "panel.example.com",
+			wantHostWithPort: "panel.example.com:2096",
+			wantHostHeader:   "panel.example.com",
+		},
+		{
+			name:             "declared boundary trusts an origin inside it",
+			stored:           storedAs("10.0.0.0/8"),
+			remoteAddr:       "10.1.2.3:44000",
+			wantScheme:       "https",
+			wantHost:         "sub.example.net",
+			wantHostWithPort: "sub.example.net",
+			wantHostHeader:   "sub.example.net",
+		},
+		{
+			name:             "declared boundary ignores an unparsable origin",
+			stored:           storedAs("10.0.0.0/8"),
+			remoteAddr:       "not-an-ip",
+			wantScheme:       "http",
+			wantHost:         "panel.example.com",
+			wantHostWithPort: "panel.example.com:2096",
+			wantHostHeader:   "panel.example.com",
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			initSubDB(t)
+			if tc.stored != nil {
+				setTrustedProxyCIDRs(t, *tc.stored)
+			}
+			s := &SubService{}
+
+			c := requestFrom(t, tc.remoteAddr, map[string]string{
+				"X-Forwarded-Host":  "sub.example.net",
+				"X-Forwarded-Proto": "https",
+			})
+			scheme, host, hostWithPort, hostHeader := s.ResolveRequest(c)
+
+			if scheme != tc.wantScheme {
+				t.Errorf("scheme = %q, want %q", scheme, tc.wantScheme)
+			}
+			if host != tc.wantHost {
+				t.Errorf("host = %q, want %q", host, tc.wantHost)
+			}
+			if hostWithPort != tc.wantHostWithPort {
+				t.Errorf("hostWithPort = %q, want %q", hostWithPort, tc.wantHostWithPort)
+			}
+			if hostHeader != tc.wantHostHeader {
+				t.Errorf("hostHeader = %q, want %q", hostHeader, tc.wantHostHeader)
+			}
+		})
+	}
+}
+
+func TestResolveRequest_GatesRealIPFallback(t *testing.T) {
+	initSubDB(t)
+	setTrustedProxyCIDRs(t, "10.0.0.0/8")
+	s := &SubService{}
+
+	c := requestFrom(t, "203.0.113.9:51000", map[string]string{
+		"X-Real-IP": "198.51.100.7",
+	})
+	_, host, _, hostHeader := s.ResolveRequest(c)
+
+	if host != "panel.example.com" {
+		t.Errorf("host = %q, want the request host — X-Real-IP from an untrusted origin must be ignored", host)
+	}
+	if hostHeader != "panel.example.com" {
+		t.Errorf("hostHeader = %q, want the request host", hostHeader)
+	}
+}
+
+func TestHasForwardedHeaders(t *testing.T) {
+	tests := []struct {
+		name    string
+		headers map[string]string
+		want    bool
+	}{
+		{name: "no forwarded headers", want: false},
+		{name: "forwarded host", headers: map[string]string{"X-Forwarded-Host": "sub.example.net"}, want: true},
+		{name: "forwarded proto", headers: map[string]string{"X-Forwarded-Proto": "https"}, want: true},
+		{name: "real ip", headers: map[string]string{"X-Real-IP": "10.1.2.3"}, want: true},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			if got := hasForwardedHeaders(requestFrom(t, "10.1.2.3:1234", tc.headers)); got != tc.want {
+				t.Errorf("hasForwardedHeaders() = %v, want %v", got, tc.want)
+			}
+		})
+	}
+}
+
+func TestRemoteAddrInCIDRs(t *testing.T) {
+	tests := []struct {
+		name       string
+		remoteAddr string
+		cidrs      string
+		want       bool
+	}{
+		{name: "inside cidr", remoteAddr: "10.1.2.3:1234", cidrs: "10.0.0.0/8", want: true},
+		{name: "ipv4 mapped address inside cidr", remoteAddr: "[::ffff:10.1.2.3]:1234", cidrs: "10.0.0.0/8", want: true},
+		{name: "outside cidr", remoteAddr: "203.0.113.9:1234", cidrs: "10.0.0.0/8", want: false},
+		{name: "bare address entry", remoteAddr: "192.168.1.5:80", cidrs: "192.168.1.5", want: true},
+		{name: "ipv6 loopback", remoteAddr: "[::1]:8080", cidrs: "::1/128", want: true},
+		{name: "no port", remoteAddr: "10.1.2.3", cidrs: "10.0.0.0/8", want: true},
+		{name: "unparsable origin", remoteAddr: "not-an-ip", cidrs: "10.0.0.0/8", want: false},
+		{name: "empty entries skipped", remoteAddr: "10.1.2.3:1", cidrs: " , 10.0.0.0/8 , ", want: true},
+	}
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			if got := remoteAddrInCIDRs(tc.remoteAddr, tc.cidrs); got != tc.want {
+				t.Errorf("remoteAddrInCIDRs(%q, %q) = %v, want %v", tc.remoteAddr, tc.cidrs, got, tc.want)
+			}
+		})
+	}
+}

+ 17 - 6
internal/sub/service.go

@@ -2486,18 +2486,29 @@ type PageData struct {
 // ResolveRequest extracts scheme and host info from request/headers consistently.
 // ResolveRequest extracts scheme and host info from request/headers consistently.
 // ResolveRequest extracts scheme, host, and header information from an HTTP request.
 // ResolveRequest extracts scheme, host, and header information from an HTTP request.
 func (s *SubService) ResolveRequest(c *gin.Context) (scheme string, host string, hostWithPort string, hostHeader string) {
 func (s *SubService) ResolveRequest(c *gin.Context) (scheme string, host string, hostWithPort string, hostHeader string) {
+	trusted := s.forwardedHeadersTrusted(c)
+	if !trusted {
+		warnSuppressedForwardedHeaders(c)
+	}
+	forwarded := func(name string) string {
+		if !trusted {
+			return ""
+		}
+		return c.GetHeader(name)
+	}
+
 	// scheme
 	// scheme
 	scheme = "http"
 	scheme = "http"
-	if c.Request.TLS != nil || strings.EqualFold(c.GetHeader("X-Forwarded-Proto"), "https") {
+	if c.Request.TLS != nil || strings.EqualFold(forwarded("X-Forwarded-Proto"), "https") {
 		scheme = "https"
 		scheme = "https"
 	}
 	}
 
 
 	// base host (no port)
 	// base host (no port)
-	if h, err := getHostFromXFH(c.GetHeader("X-Forwarded-Host")); err == nil && h != "" {
+	if h, err := getHostFromXFH(forwarded("X-Forwarded-Host")); err == nil && h != "" {
 		host = h
 		host = h
 	}
 	}
 	if host == "" {
 	if host == "" {
-		host = c.GetHeader("X-Real-IP")
+		host = forwarded("X-Real-IP")
 	}
 	}
 	if host == "" {
 	if host == "" {
 		var err error
 		var err error
@@ -2508,7 +2519,7 @@ func (s *SubService) ResolveRequest(c *gin.Context) (scheme string, host string,
 	}
 	}
 
 
 	// host:port for URLs
 	// host:port for URLs
-	hostWithPort = c.GetHeader("X-Forwarded-Host")
+	hostWithPort = forwarded("X-Forwarded-Host")
 	if hostWithPort == "" {
 	if hostWithPort == "" {
 		hostWithPort = c.Request.Host
 		hostWithPort = c.Request.Host
 	}
 	}
@@ -2517,9 +2528,9 @@ func (s *SubService) ResolveRequest(c *gin.Context) (scheme string, host string,
 	}
 	}
 
 
 	// header display host
 	// header display host
-	hostHeader = c.GetHeader("X-Forwarded-Host")
+	hostHeader = forwarded("X-Forwarded-Host")
 	if hostHeader == "" {
 	if hostHeader == "" {
-		hostHeader = c.GetHeader("X-Real-IP")
+		hostHeader = forwarded("X-Real-IP")
 	}
 	}
 	if hostHeader == "" {
 	if hostHeader == "" {
 		hostHeader = host
 		hostHeader = host

+ 1 - 1
internal/web/controller/util.go

@@ -71,7 +71,7 @@ func isTrustedProxy(ip string) bool {
 }
 }
 
 
 func trustedProxyCIDRs() (trusted string) {
 func trustedProxyCIDRs() (trusted string) {
-	trusted = "127.0.0.1/32,::1/128"
+	trusted = service.DefaultTrustedProxyCIDRs
 	defer func() {
 	defer func() {
 		_ = recover()
 		_ = recover()
 	}()
 	}()

+ 2 - 1
internal/web/service/setting.go

@@ -37,6 +37,7 @@ const (
 	DefaultSubClashUserAgentRegex = `(?i)(clash|mihomo)`
 	DefaultSubClashUserAgentRegex = `(?i)(clash|mihomo)`
 	DefaultSubJsonUserAgentRegex  = ``
 	DefaultSubJsonUserAgentRegex  = ``
 	DefaultRemarkTemplate         = "{{INBOUND}}-{{EMAIL}}|📊{{TRAFFIC_LEFT}}|⏳{{DAYS_LEFT}}D"
 	DefaultRemarkTemplate         = "{{INBOUND}}-{{EMAIL}}|📊{{TRAFFIC_LEFT}}|⏳{{DAYS_LEFT}}D"
+	DefaultTrustedProxyCIDRs      = "127.0.0.1/32,::1/128"
 	maxRegexLength                = 2048
 	maxRegexLength                = 2048
 )
 )
 
 
@@ -61,7 +62,7 @@ var defaultValueMap = map[string]string{
 	"nodeMtlsClientCAPem":         "",
 	"nodeMtlsClientCAPem":         "",
 	"webBasePath":                 normalizeBasePath(getEnv("XUI_INIT_WEB_BASE_PATH", "/")),
 	"webBasePath":                 normalizeBasePath(getEnv("XUI_INIT_WEB_BASE_PATH", "/")),
 	"sessionMaxAge":               "360",
 	"sessionMaxAge":               "360",
-	"trustedProxyCIDRs":           "127.0.0.1/32,::1/128",
+	"trustedProxyCIDRs":           DefaultTrustedProxyCIDRs,
 	"pageSize":                    "25",
 	"pageSize":                    "25",
 	"expireDiff":                  "0",
 	"expireDiff":                  "0",
 	"trafficDiff":                 "0",
 	"trafficDiff":                 "0",