package middleware_test import ( "net/http" "net/http/httptest" "net/netip" "testing" "time" "go4.org/netipx" "sneak.berlin/go/neoirc/internal/config" "sneak.berlin/go/neoirc/internal/globals" "sneak.berlin/go/neoirc/internal/logger" "sneak.berlin/go/neoirc/internal/middleware" ) func TestClientIP(t *testing.T) { t.Parallel() // The proxies are in 10.0.0.0/8. Clients use the documentation // ranges 192.0.2.0/24, 198.51.100.0/24 and 203.0.113.0/24. tests := []struct { name string remoteAddr string forwarded []string realIP string want string }{ { name: "untrusted peer ignores forged headers", remoteAddr: "192.0.2.1:4000", forwarded: []string{"203.0.113.7"}, realIP: "203.0.113.8", want: "192.0.2.1:4000", }, { name: "trusted peer without headers", remoteAddr: "10.0.0.1:4000", want: "10.0.0.1:4000", }, { name: "trusted peer gives rightmost untrusted entry", remoteAddr: "10.0.0.1:4000", forwarded: []string{"198.51.100.9, 203.0.113.7, 10.0.0.2"}, want: "203.0.113.7:4000", }, { name: "entries split over several headers", remoteAddr: "10.0.0.1:4000", forwarded: []string{"198.51.100.9", "203.0.113.7", "10.0.0.2"}, want: "203.0.113.7:4000", }, { name: "all-trusted chain gives leftmost entry", remoteAddr: "10.0.0.1:4000", forwarded: []string{"10.0.0.3, 10.0.0.2"}, want: "10.0.0.3:4000", }, { name: "X-Real-IP without X-Forwarded-For", remoteAddr: "10.0.0.1:4000", realIP: "203.0.113.8", want: "203.0.113.8:4000", }, { name: "X-Forwarded-For wins over X-Real-IP", remoteAddr: "10.0.0.1:4000", forwarded: []string{"203.0.113.7"}, realIP: "203.0.113.8", want: "203.0.113.7:4000", }, { name: "unparseable X-Forwarded-For entry gives peer", remoteAddr: "10.0.0.1:4000", forwarded: []string{"203.0.113.7, not-an-ip, 10.0.0.2"}, want: "10.0.0.1:4000", }, { name: "unparseable entry past the client is not read", remoteAddr: "10.0.0.1:4000", forwarded: []string{"not-an-ip, 203.0.113.7"}, want: "203.0.113.7:4000", }, { name: "unparseable X-Real-IP gives peer", remoteAddr: "10.0.0.1:4000", realIP: "not-an-ip", want: "10.0.0.1:4000", }, { name: "mapped IPv6 peer is unmapped and trusted", remoteAddr: "[::ffff:10.0.0.1]:4000", forwarded: []string{"203.0.113.7"}, want: "203.0.113.7:4000", }, { name: "mapped IPv6 peer outside the set", remoteAddr: "[::ffff:192.0.2.1]:4000", forwarded: []string{"203.0.113.7"}, want: "192.0.2.1:4000", }, { name: "IPv6 client", remoteAddr: "10.0.0.1:4000", forwarded: []string{"2001:db8::1"}, want: "[2001:db8::1]:4000", }, } handler := newMiddleware(t, "10.0.0.0/8").ClientIP()( http.HandlerFunc(func( writer http.ResponseWriter, request *http.Request, ) { _, _ = writer.Write([]byte(request.RemoteAddr)) }), ) for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel() request := httptest.NewRequestWithContext( t.Context(), http.MethodGet, "/", nil, ) request.RemoteAddr = test.remoteAddr for _, value := range test.forwarded { request.Header.Add("X-Forwarded-For", value) } if test.realIP != "" { request.Header.Set("X-Real-IP", test.realIP) } recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) got := recorder.Body.String() if got != test.want { t.Errorf("RemoteAddr = %q, want %q", got, test.want) } }) } } // newMiddleware returns a Middleware that trusts the network trusted. func newMiddleware(t *testing.T, trusted string) *middleware.Middleware { t.Helper() var builder netipx.IPSetBuilder builder.AddPrefix(netip.MustParsePrefix(trusted)) trustedSet, err := builder.IPSet() if err != nil { t.Fatal(err) } globs := &globals.Globals{ Appname: "neoirc-test", Version: "test", StartTime: time.Now(), } log, err := logger.New(nil, logger.Params{Globals: globs}) if err != nil { t.Fatal(err) } mware, err := middleware.New(nil, middleware.Params{ Logger: log, Globals: globs, Config: &config.Config{TrustedProxies: trustedSet}, }) if err != nil { t.Fatal(err) } return mware }