R-753: the box tells visitors apart — cloudflared at a fixed address, traefik trusts only it, the controller reads the hop traefik saw
- felhom-tunnel network 172.16.253.0/29 (ip-range .4/30): cloudflared alone at .2, traefik at .3; traefik's websecure trusts forwarded headers from 172.16.253.2/32 only, and every request passes felhom-forwarded@file, which removes the client-writable host/path/address headers (X-Forwarded-Host/-Uri/-Method/-Prefix, Forwarded, True-Client-Ip, …) and fixes X-Forwarded-Port to 443 (measured: Cloudflare passes a client's X-Forwarded-Host/-Port). - EnsureBaseStack reconciles a RUNNING traefik/cloudflared whose rendered files changed (recreate), refuses a rewrite that would drop a certificate resolver, and moves cloudflared only once traefik is on the tunnel network. - clientIP: believed only when the TCP peer is traefik; the rightmost X-Forwarded-For entry (the hop traefik saw); the tunnel hop → CF-Connecting-IP (the edge refuses a client-sent one, measured 403). rateKey: IPv6 per /64. Dashboard login, claim, share and escrow counters key on it; the setup gate logs it. - Dashboard login messages: keys, informal voice, both languages. Red-proofs: RP-A1 (leftmost hop), RP-A2 (shared tunnel key), RP-A3 (no reconcile) — felhom.eu audits/visitors-2026-10-01/A. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -60,7 +60,7 @@ func TestLoginRateLimit_DirectDistinctPorts_Limited(t *testing.T) {
|
||||
t.Fatalf("attempt %d expected Hibás jelszó, got: %s", i, ex(last))
|
||||
}
|
||||
}
|
||||
if !strings.Contains(last, "Túl sok sikertelen") {
|
||||
if !strings.Contains(last, "Túl sok hibás próbálkozás") {
|
||||
t.Fatalf("attempt 6 (distinct ports, no XFF) MUST be rate-limited; got: %s", ex(last))
|
||||
}
|
||||
}
|
||||
@@ -72,25 +72,22 @@ func TestLoginRateLimit_StableXFF_Limited(t *testing.T) {
|
||||
for i := 1; i <= 6; i++ {
|
||||
last = doLogin(s, fmt.Sprintf("10.9.9.9:%d", 5000+i), "203.0.113.9", "wrong").Body.String()
|
||||
}
|
||||
if !strings.Contains(last, "Túl sok sikertelen") {
|
||||
if !strings.Contains(last, "Túl sok hibás próbálkozás") {
|
||||
t.Fatalf("attempt 6 with a stable XFF MUST be rate-limited; got: %s", ex(last))
|
||||
}
|
||||
}
|
||||
|
||||
// Scenario C (documented accepted limitation): rotating the X-Forwarded-For first hop evades the
|
||||
// per-IP counter. This is NOT what the fix targets (XFF is attacker-controlled on a direct path);
|
||||
// the test pins the known behavior so a future XFF-trust change is a conscious decision.
|
||||
func TestLoginRateLimit_RotatingXFF_NotLimited(t *testing.T) {
|
||||
// Scenario C (R-753, reversed on purpose): a peer that is NOT traefik wrote its X-Forwarded-For itself, so rotating it
|
||||
// no longer evades the counter — the key is the TCP peer. (Before v0.286.0 this test pinned the evasion as "a future
|
||||
// XFF-trust change is a conscious decision"; this is that decision, `09` §3 decision 63.)
|
||||
func TestLoginRateLimit_RotatingXFF_Limited(t *testing.T) {
|
||||
s := rateLimitTestServer(t)
|
||||
var last string
|
||||
for i := 1; i <= 6; i++ {
|
||||
last = doLogin(s, "10.9.9.9:5000", fmt.Sprintf("203.0.113.%d", i), "wrong").Body.String()
|
||||
}
|
||||
if strings.Contains(last, "Túl sok sikertelen") {
|
||||
t.Fatalf("rotating XFF is a known evasion (out of scope) — expected NOT limited")
|
||||
}
|
||||
if !strings.Contains(last, "Hibás jelszó") {
|
||||
t.Fatalf("expected Hibás jelszó on rotating XFF; got: %s", ex(last))
|
||||
if !strings.Contains(last, "Túl sok hibás próbálkozás") {
|
||||
t.Fatalf("a rotating X-Forwarded-For from a direct peer must NOT evade the counter; got: %s", ex(last))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,14 +126,15 @@ func TestLoginRateLimit_SuccessClearsCounter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// clientIP unit: port stripped; XFF first-hop wins; no-port and IPv6 handled.
|
||||
// clientIP unit (direct peers — the forwarded paths are TestClientIP_Paths in clientaddr_test.go): port stripped; a
|
||||
// direct peer's own X-Forwarded-For is never believed (R-753).
|
||||
func TestClientIP_StripsPort(t *testing.T) {
|
||||
cases := []struct{ remote, xff, want string }{
|
||||
{"127.0.0.1:5001", "", "127.0.0.1"},
|
||||
{"127.0.0.1:5002", "203.0.113.9", "203.0.113.9"},
|
||||
{"127.0.0.1:5002", "203.0.113.9", "127.0.0.1"},
|
||||
{"[::1]:443", "", "::1"},
|
||||
{"192.168.0.5", "", "192.168.0.5"}, // no port → raw
|
||||
{"10.0.0.1:80", "198.51.100.7, 203.0.113.9", "198.51.100.7"},
|
||||
{"10.0.0.1:80", "198.51.100.7, 203.0.113.9", "10.0.0.1"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
|
||||
Reference in New Issue
Block a user