package membership import ( "net/http" "testing" ) // TestClientIPTrustedProxy covers the rate-limit key extraction behind a reverse // proxy: forwarding headers are believed ONLY when the immediate peer is a // configured trusted proxy, and never otherwise. This is what keeps the per-IP // rate limit per-client once the control plane runs behind the same-origin Caddy // proxy, without opening a quota-fanning hole for a direct attacker. func TestClientIPTrustedProxy(t *testing.T) { const caddy = "135.125.201.30" cases := []struct { name string proxies []string remote string xff string xRealIP string want string }{ { name: "no trusted proxies ignores XFF", remote: "203.0.113.7:5000", xff: "1.2.3.4", want: "203.0.113.7", }, { name: "trusted proxy honors XFF client", proxies: []string{caddy}, remote: caddy + ":4451", xff: "198.51.100.23", want: "198.51.100.23", }, { name: "loopback proxy honors XFF (magnus-local hop)", proxies: []string{"127.0.0.1/32", "::1/128"}, remote: "127.0.0.1:33344", xff: "198.51.100.99", want: "198.51.100.99", }, { name: "untrusted peer cannot spoof XFF", proxies: []string{caddy}, remote: "203.0.113.7:5000", xff: "10.0.0.1", want: "203.0.113.7", }, { name: "XFF read right-to-left, trusted hops skipped", proxies: []string{caddy}, remote: caddy + ":4451", xff: "198.51.100.23, " + caddy, want: "198.51.100.23", }, { name: "client-prepended forgery is skipped, real appended wins", proxies: []string{caddy}, remote: caddy + ":4451", xff: "9.9.9.9, 198.51.100.23", want: "198.51.100.23", }, { name: "X-Real-IP fallback when no XFF", proxies: []string{caddy}, remote: caddy + ":4451", xRealIP: "198.51.100.77", want: "198.51.100.77", }, { name: "trusted peer but no forwarding header falls back to peer", proxies: []string{caddy}, remote: caddy + ":4451", want: caddy, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { s := &Server{} if len(tc.proxies) > 0 { if err := s.SetTrustedProxies(tc.proxies); err != nil { t.Fatalf("SetTrustedProxies(%v): %v", tc.proxies, err) } } r, _ := http.NewRequest(http.MethodGet, "/rooms", nil) r.RemoteAddr = tc.remote if tc.xff != "" { r.Header.Set("X-Forwarded-For", tc.xff) } if tc.xRealIP != "" { r.Header.Set("X-Real-IP", tc.xRealIP) } if got := s.clientIP(r); got != tc.want { t.Fatalf("clientIP = %q, want %q", got, tc.want) } }) } } // TestParseTrustedProxiesRejectsGarbage proves a malformed entry is a hard error // (the command turns it into a startup failure) rather than a silently ignored // misconfiguration that would leave the rate limit collapsed behind the proxy. func TestParseTrustedProxiesRejectsGarbage(t *testing.T) { if _, err := parseTrustedProxies([]string{"not-an-ip"}); err == nil { t.Fatal("expected error for non-IP/CIDR entry, got nil") } if _, err := parseTrustedProxies([]string{"10.0.0.0/8", "127.0.0.1"}); err != nil { t.Fatalf("valid entries rejected: %v", err) } }