package middleware import ( "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" ) // newClientIPRouter mirrors the production trusted-proxy configuration: // only loopback is trusted (the Caddy/nginx host). func newClientIPRouter() *gin.Engine { gin.SetMode(gin.TestMode) r := gin.New() if err := r.SetTrustedProxies([]string{"127.0.0.1", "::1"}); err != nil { panic(err) } r.GET("/ip", func(c *gin.Context) { c.String(http.StatusOK, c.ClientIP()) }) return r } func TestClientIPSpoofingBlocked(t *testing.T) { r := newClientIPRouter() // A direct (untrusted) client sending a forged X-Forwarded-For must not // be able to change the recorded IP. req := httptest.NewRequest(http.MethodGet, "/ip", nil) req.RemoteAddr = "203.0.113.5:12345" req.Header.Set("X-Forwarded-For", "6.6.6.6") w := httptest.NewRecorder() r.ServeHTTP(w, req) if got := w.Body.String(); got != "203.0.113.5" { t.Errorf("direct client with forged XFF: got %q, want 203.0.113.5", got) } // A trusted proxy (loopback) forwarding a real chain: the rightmost // untrusted entry wins, earlier (client-supplied) entries are ignored. req = httptest.NewRequest(http.MethodGet, "/ip", nil) req.RemoteAddr = "127.0.0.1:54321" req.Header.Set("X-Forwarded-For", "6.6.6.6, 198.51.100.42") w = httptest.NewRecorder() r.ServeHTTP(w, req) if got := w.Body.String(); got != "198.51.100.42" { t.Errorf("proxy-forwarded chain: got %q, want 198.51.100.42 (client-supplied entry must be ignored)", got) } // A trusted proxy forwarding a single entry: that entry is the client. req = httptest.NewRequest(http.MethodGet, "/ip", nil) req.RemoteAddr = "127.0.0.1:54321" req.Header.Set("X-Forwarded-For", "198.51.100.42") w = httptest.NewRecorder() r.ServeHTTP(w, req) if got := w.Body.String(); got != "198.51.100.42" { t.Errorf("proxy-forwarded single entry: got %q, want 198.51.100.42", got) } }