@
v0.129.0: CAMPAIGN-4 fixes — rate-limiter key (F-B) + volume-blind estimate (F-A) + no-op claim status (F-C) F-B (MED, security): shared clientIP(r) helper (XFF first-hop, else SplitHostPort host, else raw) replaces requestIP + the duplicated inline derivation in handleLogin, so login AND escrow re-auth key on the port-stripped host IP — distinct direct connections no longer evade the failed-attempt counter. XFF-trust out of scope (commented). F-A (MED, honesty): volumeSizer seam reads volume size from a container view (docker run --rm -v vol:/vol:ro alpine du -sb /vol), replacing the host-path du that returned 0 inside the containerized controller. Failed read -> size_unknown + fits_on_dest forced false (never "fits"). Export pre-flight hard-aborts only on a KNOWN doesn.t-fit. HDD branch unchanged. F-C (LOW-MED): escrowClaimAPIHandler relays agent 404 -> clean 404 and 409 -> 409; 410 and genuine-unreachable 502 unchanged (was: 404 fell through to 502). Tests + red-proofs: ratelimit_ip_test.go (F-B x6), estimate_volsize_test.go (F-A x3), TestEscrowClaim_ProxySemantics +3 (F-C). Alpine busybox du -sb verified prod-valid. Claude-Session: https://claude.ai/code/session_01LbMm4T7Ayzs1unB9pN6Uqd @
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -193,12 +194,26 @@ func (s *Server) claimNow() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func requestIP(r *http.Request) string {
|
||||
ip := r.RemoteAddr
|
||||
// clientIP returns the client IP used as the rate-limiter key. Order: the X-Forwarded-For first
|
||||
// hop (set by the traefik/Cloudflare proxy) wins; otherwise the HOST portion of RemoteAddr with the
|
||||
// ephemeral PORT stripped (net.SplitHostPort). This is the CAMPAIGN-4 F-B fix: keying on the raw
|
||||
// RemoteAddr (IP:PORT) meant every fresh direct connection from one host got a distinct ephemeral
|
||||
// port → a distinct key → the failed-attempt counter never accrued, so a direct-to-controller
|
||||
// (LAN/guest, non-proxied) path had NO brute-force protection. A RemoteAddr with no port
|
||||
// (tests/edge) or an IPv6 form is handled by SplitHostPort, falling back to the raw value.
|
||||
//
|
||||
// Accepted limitation (out of scope here): X-Forwarded-For is attacker-controlled on a direct path,
|
||||
// so a client rotating the first hop still evades the per-IP counter. This fix only closes the
|
||||
// port-in-key bug so the proxied / stable-source-IP case — the real deployment — works; it does NOT
|
||||
// attempt to establish XFF trust.
|
||||
func clientIP(r *http.Request) string {
|
||||
if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
|
||||
ip = strings.Split(fwd, ",")[0]
|
||||
return strings.TrimSpace(strings.Split(fwd, ",")[0])
|
||||
}
|
||||
return strings.TrimSpace(ip)
|
||||
if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
||||
return host
|
||||
}
|
||||
return strings.TrimSpace(r.RemoteAddr)
|
||||
}
|
||||
|
||||
// ── the pages ────────────────────────────────────────────────────────────────────────────────
|
||||
@@ -260,7 +275,7 @@ func (s *Server) handleClaimSubmit(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
wasReset := s.authEnabled() // a password already set → this is a reset, not a first-claim
|
||||
ip := requestIP(r)
|
||||
ip := clientIP(r)
|
||||
|
||||
if locked, _ := s.claimRateLocked(); locked {
|
||||
s.handleClaimPage(w, r, "Túl sok próbálkozás — próbáld újra 15 perc múlva.", "")
|
||||
|
||||
Reference in New Issue
Block a user