Files
EvoBGP/internal/httpapi/trusted_proxy_test.go
T
DenozordecandCursor f8e0520817 feat(httpapi): harden control plane and speed up prefix refresh
Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь.

Co-authored-by: Cursor <[email protected]>
2026-11-02 17:23:59 +07:00

130 lines
4.6 KiB
Go

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", "[email protected]"},
{"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))
}
}