package httpapi import ( "fmt" "net/http/httptest" "net/netip" "testing" "time" ) func TestClientIPTrustedProxyChain(t *testing.T) { prefixes := []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8")} tests := []struct { name string remote string xff string want string }{ {"untrusted peer ignores spoof", "198.51.100.10:1234", "203.0.113.9", "198.51.100.10"}, {"rightmost untrusted hop", "10.0.0.2:1234", "203.0.113.9, 198.51.100.8", "198.51.100.8"}, {"trusted chain", "10.0.0.2:1234", "203.0.113.9, 10.0.0.3", "203.0.113.9"}, {"mapped peer", "[::ffff:10.0.0.2]:1234", "203.0.113.9", "203.0.113.9"}, {"mapped forwarded client", "10.0.0.2:1234", "::ffff:203.0.113.9", "203.0.113.9"}, {"malformed nearest hop", "10.0.0.2:1234", "203.0.113.9, bogus", "10.0.0.2"}, {"malformed farther hop ignored", "10.0.0.2:1234", "bogus, 198.51.100.8", "198.51.100.8"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := httptest.NewRequest("GET", "http://example.test/", nil) r.RemoteAddr = tt.remote r.Header.Set("X-Forwarded-For", tt.xff) if got := clientIPWithTrustedProxies(r, prefixes); got != tt.want { t.Fatalf("client IP = %q, want %q", got, tt.want) } }) } } func TestClientIPConfigurationFailsClosed(t *testing.T) { r := httptest.NewRequest("GET", "http://example.test/", nil) r.RemoteAddr = "10.0.0.2:1234" r.Header.Set("X-Forwarded-For", "203.0.113.9") r.Header.Set("X-Real-IP", "203.0.113.8") if got := clientIPWithTrustedProxies(r, nil); got != "10.0.0.2" { t.Fatalf("direct client IP = %q", got) } t.Setenv("EVOBGP_TRUSTED_PROXY_CIDRS", "10.0.0.0/8, invalid") if got := clientIP(r); got != "10.0.0.2" { t.Fatalf("invalid CIDR client IP = %q", got) } t.Setenv("EVOBGP_TRUSTED_PROXY_CIDRS", "::ffff:10.0.0.2/128") if got := clientIP(r); got != "203.0.113.9" { t.Fatalf("mapped CIDR client IP = %q", got) } } func TestPublicControlPlaneURLTrust(t *testing.T) { t.Setenv("EVOBGP_TRUSTED_PROXY_CIDRS", "10.0.0.0/8") r := httptest.NewRequest("POST", "http://internal.local:8080/v1/speakers", nil) r.Header.Set("Origin", "https://evil.example") r.Header.Set("X-Forwarded-Proto", "https") r.Header.Set("X-Forwarded-Host", "bgp.example.test") r.RemoteAddr = "198.51.100.8:1234" if got := publicControlPlaneURL(r, ""); got != "http://internal.local:8080" { t.Fatalf("direct URL = %q", got) } r.RemoteAddr = "10.0.0.2:1234" if got := publicControlPlaneURL(r, ""); got != "https://bgp.example.test" { t.Fatalf("proxied URL = %q", got) } if got := publicControlPlaneURL(r, "https://explicit.example/"); got != "https://explicit.example" { t.Fatalf("override URL = %q", got) } for _, bad := range []struct{ proto, host string }{ {"javascript", "bgp.example.test"}, {"https,http", "bgp.example.test"}, {"https", "evil.example/path"}, {"https", "evil.example@bgp.example.test"}, {"https", "evil.example,bgp.example.test"}, {"https", "bgp.example.test:99999"}, {"https", "bgp.example.test:"}, } { r.Header.Set("X-Forwarded-Proto", bad.proto) r.Header.Set("X-Forwarded-Host", bad.host) if got := publicControlPlaneURL(r, ""); got != "http://internal.local:8080" { t.Errorf("bad forwarded headers (%q, %q): %q", bad.proto, bad.host, got) } } r.Header.Set("X-Forwarded-Proto", "https") r.Header.Set("X-Forwarded-Host", "bgp.example.test") r.Header.Add("X-Forwarded-Host", "evil.example") if got := publicControlPlaneURL(r, ""); got != "http://internal.local:8080" { t.Fatalf("duplicate forwarded host = %q", got) } } func TestAuthRateLimiterManyIdentities(t *testing.T) { l := &authRateLimiter{hits: make(map[string]authRateHit), limit: 1, window: time.Minute, maxClients: 32} now := time.Unix(1000, 0) for i := 0; i < 1000; i++ { allowed := l.allowAt(fmt.Sprintf("ip-%d", i), now) if allowed != (i < l.maxClients) { t.Fatalf("identity %d allowed = %t", i, allowed) } } if len(l.hits) != l.maxClients || l.allowAt("ip-0", now) { t.Fatalf("active counters changed under cardinality pressure: %d", len(l.hits)) } } func TestAuthRateLimiterCapacityAndExpiry(t *testing.T) { l := &authRateLimiter{hits: make(map[string]authRateHit), limit: 2, window: time.Minute, maxClients: 2} now := time.Unix(1000, 0) if !l.allowAt("a", now) || !l.allowAt("b", now) || !l.allowAt("a", now) { t.Fatal("initial requests rejected") } if l.allowAt("a", now) || l.allowAt("c", now) { t.Fatal("active limit or capacity bypassed") } if len(l.hits) != 2 { t.Fatalf("map size = %d", len(l.hits)) } if !l.allowAt("c", now.Add(time.Minute)) { t.Fatal("expired entries did not release capacity") } if len(l.hits) != 1 { t.Fatalf("expired entries remained: %d", len(l.hits)) } }