peer: reject invalid X-DART-Hop values at both servers - #57
Conversation
Both ServeHTTP paths parsed X-DART-Hop with strconv.Atoi and discarded the error, so a malformed value silently became 0 and a negative value stayed negative. The engine bounds relay recursion with hop >= maxHop and every relay increments the value, so a negative start delayed the loop-safety cutoff (a huge negative one effectively forever) exactly when membership skew creates a relay cycle. Add parseHop: absent header means depth 0; any malformed, negative, or out-of-range value is rejected with 400 before the Source is invoked, identically on the buffered Server and the cut-through StreamServer. The maxHop bound itself stays with the engine, so any non-negative hop remains a valid wire value at the transport layer. Regression coverage: TestServerHopValidation (both servers, valid and invalid hops incl. min int64/overflow; proven to fail on the old code, which served 200 for hop=-1), TestParseHop (decoder edges), and TestRelayHopBoundary (engine: maxHop-1 relays, maxHop declines without touching the origin). Fixes #50
|
@copilot please review this PR. Focus points: (1) the new |
Review complete; no code changes requested. (1) |
The old-code repro dance during issue triage accidentally trimmed this test from the committed file (caught in review): the docs and PR description referenced TestParseHop but it was absent. Re-add the decoder-edge table (empty, leading plus, -0, overflow). Refs #50
|
@copilot thanks — you were right: |
Re-review complete. |
Fixes #50
What
Both peer HTTP servers (
ServerandStreamServer) parsedX-DART-Hopwithhop, _ := strconv.Atoi(...), discarding the error. A malformed value silently became 0 and a negative value stayed negative. The engine bounds relay recursion withhop >= maxHop(64) and every relay increments the value, so a negative start delayed the loop-safety cutoff — a huge negative one (e.g. min int64) effectively forever — exactly when membership skew creates a relay cycle.This PR adds a shared
parseHopdecoder used identically by both servers: the header is optional (absent = depth 0); any malformed, negative, or out-of-range value is rejected with400 Bad Requestbefore theSourceis invoked. ThemaxHopbound itself stays with the engine, so any non-negative hop remains a valid wire value at the transport layer.Triage evidence
Verified against
84aac4e(main HEAD, the commit the issue names):internal/peer/peer.go:103andinternal/peer/stream.go:95both readhop, _ := strconv.Atoi(r.Header.Get(HeaderHop)).internal/engine/engine.go:360,437andinternal/engine/stream.go:70reject onlyhop >= maxHop; nothing rejects a negative hop.GET /peer/v1/block/abcdef/7carryingX-DART-Hop: -1was served200with the block bytes by both servers, and the source observedHop: -1. With this PR both servers answer400and never call the source.Classification: implementation defect (protocol validation / loop safety), as the issue states. Not a deliberate simplification — the header is documented as the loop-safety bound, and the validation is cheap. The fix matches the issue's "Expected behavior": both server implementations now share the same validation semantics.
Fixes
internal/peer/peer.go: newparseHop(absent → 0; malformed / negative / int-overflow → invalid);Server.ServeHTTPrejects invalid hops with 400;BlockRequest.Hopdoc updated.internal/peer/stream.go:StreamServer.ServeHTTPuses the sameparseHopvalidation — identical semantics on both servers.docs/peer.md: wire-form section documents theX-DART-Hopdomain (optional, non-negative decimal; 400 otherwise) and why; test-table rows added.docs/engine.md: test-table row for the new boundary test.Regression tests
TestServerHopValidation(internal/peer): table-driven over both server implementations — absent/0/63/64/4096 accepted with the exact hop observed by the source;-1,-4096, min int64, int64 overflow,abc,1.5, leading-space,1_000→ 400 with the source never invoked. Proven to fail on the old code (old code returned 200 and served bytes forhop=-1).TestParseHop(internal/peer): decoder edges ("",+5,-0, overflow).TestRelayHopBoundary(internal/engine): pins themaxHopgate on bothPeerSourceandPeerStreamSource—maxHop-1still relays (origin contacted),maxHopdeclines without touching the origin.Verification
Run in the fix worktree (go1.22.12, module targets go 1.22):
Old-code check: with only the fix stashed,
go test ./internal/peer/ -run TestServerHopValidationfails on every invalid-hop case (status = 200, want 400); with the fix restored, the full suite passes.