package auth import ( "net/http" "testing" ) // The spoofing cases below are the reason this function exists rather than a // one-line r.RemoteAddr read, so they're asserted explicitly rather than // folded into the happy-path table. func TestClientIP(t *testing.T) { tests := []struct { name string remoteAddr string forwarded string realIP string want string }{ { name: "direct connection, no proxy headers", remoteAddr: "203.0.113.5:51234", want: "203.0.113.5", }, { // The attack this guards against: a client connecting straight to // us claims to be someone else. RemoteAddr is public, so it did // NOT come through our proxy, so its XFF is worthless. name: "direct connection ignores forged X-Forwarded-For", remoteAddr: "203.0.113.5:51234", forwarded: "198.51.100.99", want: "203.0.113.5", }, { name: "direct connection ignores forged X-Real-IP", remoteAddr: "203.0.113.5:51234", realIP: "198.51.100.99", want: "203.0.113.5", }, { name: "behind proxy, single forwarded client", remoteAddr: "172.18.0.1:40000", forwarded: "203.0.113.5", want: "203.0.113.5", }, { // A client that prepends a lie to XFF only pollutes the LEFT end; // the proxy appends the address it actually saw on the right. The // right-to-left walk reaches the truth first. name: "behind proxy, forged prefix is skipped for the appended truth", remoteAddr: "10.0.0.2:40000", forwarded: "198.51.100.99, 203.0.113.5", want: "203.0.113.5", }, { name: "behind proxy chain, internal hops skipped", remoteAddr: "10.0.0.2:40000", forwarded: "203.0.113.5, 10.0.0.7, 172.18.0.3", want: "203.0.113.5", }, { name: "behind proxy, X-Real-IP used when no forwarded header", remoteAddr: "127.0.0.1:40000", realIP: "203.0.113.5", want: "203.0.113.5", }, { // LAN client through a LAN proxy: everything is private, so there // is no public address to find. Reporting the peer is honest. name: "behind proxy, all-private chain falls back to remote", remoteAddr: "172.18.0.1:40000", forwarded: "192.168.1.50, 172.18.0.3", want: "172.18.0.1", }, { name: "behind proxy, malformed forwarded entries ignored", remoteAddr: "172.18.0.1:40000", forwarded: "not-an-ip, 203.0.113.5, also-garbage", want: "203.0.113.5", }, { name: "remote addr without a port is tolerated", remoteAddr: "203.0.113.5", want: "203.0.113.5", }, { name: "ipv6 remote addr", remoteAddr: "[2001:db8::1]:51234", want: "2001:db8::1", }, { name: "ipv6 forwarded client behind proxy", remoteAddr: "[fd00::1]:40000", forwarded: "2001:db8::5", want: "2001:db8::5", }, { name: "empty remote addr yields empty", remoteAddr: "", want: "", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { r, err := http.NewRequest(http.MethodGet, "/api/me/sessions", nil) if err != nil { t.Fatalf("NewRequest: %v", err) } r.RemoteAddr = tc.remoteAddr if tc.forwarded != "" { r.Header.Set("X-Forwarded-For", tc.forwarded) } if tc.realIP != "" { r.Header.Set("X-Real-IP", tc.realIP) } if got := ClientIP(r); got != tc.want { t.Errorf("ClientIP() = %q, want %q", got, tc.want) } }) } }