Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь. Co-authored-by: Cursor <cursoragent@cursor.com>
130 lines
4.6 KiB
Go
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", "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))
|
|
}
|
|
}
|