diff --git a/backend/internal/server/http.go b/backend/internal/server/http.go index 65548c9e24..fef7cd056e 100644 --- a/backend/internal/server/http.go +++ b/backend/internal/server/http.go @@ -47,18 +47,7 @@ func ProvideRouter( r := gin.New() r.Use(middleware2.Recovery()) - if len(cfg.Server.TrustedProxies) > 0 { - if err := r.SetTrustedProxies(cfg.Server.TrustedProxies); err != nil { - log.Printf("Failed to set trusted proxies: %v", err) - } - } else { - if err := r.SetTrustedProxies(nil); err != nil { - log.Printf("Failed to disable trusted proxies: %v", err) - } - if cfg.Server.Mode == "release" { - log.Printf("Warning: server.trusted_proxies is empty in release mode; client IP trust chain is disabled") - } - } + configureTrustedProxies(r, cfg.Server) // Wire up websearch Manager builder so it initializes on startup and rebuilds on config save. settingService.SetWebSearchManagerBuilder(context.Background(), func(cfg *service.WebSearchEmulationConfig, proxyURLs map[int64]string) { @@ -99,6 +88,25 @@ func ProvideRouter( return SetupRouter(r, handlers, jwtAuth, adminAuth, apiKeyAuth, auditLog, stepUpAuth, apiKeyService, subscriptionService, opsService, settingService, cfg, redisClient) } +func configureTrustedProxies(r *gin.Engine, cfg config.ServerConfig) { + if cfg.TrustedProxiesConfigured { + if err := r.SetTrustedProxies(cfg.TrustedProxies); err != nil { + log.Printf("Failed to set trusted proxies: %v", err) + _ = r.SetTrustedProxies(nil) + } + if len(cfg.TrustedProxies) == 0 && cfg.Mode == "release" { + log.Printf("Warning: server.trusted_proxies is explicitly empty; forwarded client IP trust is disabled") + } + } else { + if err := r.SetTrustedProxies(nil); err != nil { + log.Printf("Failed to disable trusted proxies: %v", err) + } + if cfg.Mode == "release" { + log.Printf("Warning: server.trusted_proxies is not configured; disabling the forwarded-IP compatibility switch will use direct peer addresses only") + } + } +} + // ProvideHTTPServer 提供 HTTP 服务器 func ProvideHTTPServer(cfg *config.Config, router *gin.Engine) *http.Server { httpHandler := http.Handler(router) diff --git a/backend/internal/server/http_ingress_test.go b/backend/internal/server/http_ingress_test.go index 63cf7a047a..5e986bbd7c 100644 --- a/backend/internal/server/http_ingress_test.go +++ b/backend/internal/server/http_ingress_test.go @@ -55,6 +55,56 @@ func TestProvideHTTPServerEnablesBoundedH2C(t *testing.T) { require.True(t, srv.Protocols.HTTP1()) } +func TestConfigureTrustedProxies(t *testing.T) { + gin.SetMode(gin.TestMode) + tests := []struct { + name string + cfg config.ServerConfig + want string + }{ + { + name: "configured proxy resolves forwarded client", + cfg: config.ServerConfig{ + TrustedProxies: []string{"9.9.9.9/32"}, + TrustedProxiesConfigured: true, + }, + want: "1.2.3.4", + }, + { + name: "explicit empty list ignores forwarded client", + cfg: config.ServerConfig{ + TrustedProxiesConfigured: true, + }, + want: "9.9.9.9", + }, + { + name: "invalid proxy list fails closed", + cfg: config.ServerConfig{ + TrustedProxies: []string{"not-a-cidr"}, + TrustedProxiesConfigured: true, + }, + want: "9.9.9.9", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + r := gin.New() + configureTrustedProxies(r, tc.cfg) + r.GET("/t", func(c *gin.Context) { c.String(http.StatusOK, c.ClientIP()) }) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/t", nil) + req.RemoteAddr = "9.9.9.9:12345" + req.Header.Set("X-Forwarded-For", "1.2.3.4") + r.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + require.Equal(t, tc.want, w.Body.String()) + }) + } +} + func TestHTTPServerRejectsOversizedHTTP1Header(t *testing.T) { r := gin.New() r.GET("/", func(c *gin.Context) { c.Status(http.StatusOK) })