fix(node): reset a peer's reachability gate when an announce changes its http_url (#270) - #280
Merged
Merged
Conversation
added 3 commits
July 29, 2026 16:56
…a fresh insert (#270) upsert_peer's ON CONFLICT branch rewrites http_url and says nothing about last_ping_ok, so a repointed peer keeps the reachability the previous host earned. These four cases state the behavior the fix has to produce, and they land before it so the failure is observed rather than assumed. Pre-fix results: url_change_clears_reachability ... FAILED same_url_reannounce_keeps_reachability ... ok fresh_peer_inserts_unreachable ... ok same_url_reannounce_does_not_grant_reachability ... ok panicked at crates/gitlawb-node/src/db/mod.rs:5737: a repointed peer must re-earn reachability, not inherit it The failure is on the last_ping_ok assertion; the http_url assertion above it passes, so the row is being rewritten and only the gate is wrong. The three that already pass are regression pins, not filler. same_url_reannounce_keeps_reachability guards against clearing the flag on every conflict rather than only on a change. same_url_reannounce_does_not_grant_reachability guards the other direction and is the only case that fails if the conditional is flattened to last_ping_ok = (peers.http_url IS NOT DISTINCT FROM $2), which would let any unsigned same-URL re-announce set the flag TRUE with no probe. fresh_peer_inserts_unreachable guards the INSERT branch. Refs #270
…its http_url (#270) The ON CONFLICT branch rewrote http_url and left last_ping_ok alone, so an unsigned announce could repoint an existing peer's URL and inherit the reachable=true that the previous host earned. api/repos.rs filters the federated fan-out on that flag and then stamps each entry from the peer's response body with the peer's DID, so the inheritance is what made the injection instant rather than probe-delayed. Clear the flag when the URL actually changes, and leave it alone when it does not. In the DO UPDATE branch peers.http_url is the existing pre-update row and the comparison runs under the conflict row lock, so this needs no second round trip and does not race a read-then-write. Both writers funnel through upsert_peer, so the announce handler and the bootstrap announce-back are both covered. Observed transition on the tests from the previous commit: before url_change_clears_reachability ... FAILED (last_ping_ok assertion) after url_change_clears_reachability ... ok the other three ... ok throughout Both guards were confirmed load-bearing by reverting, not by reading a green suite. Removing the CASE clause entirely turns url_change_clears_reachability RED again. Flattening it to last_ping_ok = (peers.http_url IS NOT DISTINCT FROM $2) still passes three of the four cases and is caught only by same_url_reannounce_does_not_grant_reachability, which is why that case exists. This bounds the automatic inheritance; it does not close the rewrite. Four other http_url consumers never read this flag, and until #248 lands the unauthenticated ping route can write it back from the attacker's own host. #273 is the closure. Closes #270
…es (#270) Review of the previous two commits found the SQL correct and the comment wrong in two places, plus two implementations of the intent that all four tests accepted. Comment fixes: The claim that four http_url consumers "never read the flag at all" named the public resolve route, which does read it: api/resolve.rs republishes it as `reachable`, as does the peer listing. The accurate split is that only the federated repo fan-out in api/repos.rs GATES on the flag, four consumers act on a repointed URL regardless of it, and two surfaces republish it. Three reviewers caught this independently. The atomicity sentence implied the conflict row lock protects the flag. It orders announces against each other and nothing else: mark_peer_ping writes by DID with no http_url predicate, so a probe of the previous URL that lands after a reset can still re-grant the flag until the next round. Said so, and qualified the self-healing claim on cosmetic differences, which holds only if no further announce lands first. Two tests added, each proven load-bearing against a shape the original four accept: cosmetic_url_difference_counts_as_a_change Pins the exact-comparison decision. Normalizing the comparison (rtrim(peers.http_url, '/') vs rtrim($2, '/')) passes the other five and fails only this one. The original four only ever compare identical or wholly different hosts, so none of them constrain the comparison. same_url_reannounce_still_advances_last_seen The reset must ride the existing UPDATE, not gate it. Hoisting the condition to a statement-level WHERE passes the other five while skipping the update entirely on a same-URL re-announce, so liveness stops advancing and the peer ages out on last_seen. Observed with both timestamps identical. Gates: cargo test -p gitlawb-node 516 passed 0 failed; clippy --all-targets -D warnings clean workspace-wide; fmt --check clean. Refs #270
Contributor
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughPeer upserts now clear ChangesPeer reachability semantics
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related issues
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
kevincodex1
approved these changes
Jul 30, 2026
14 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
upsert_peer'sON CONFLICTbranch rewrotehttp_urland said nothing aboutlast_ping_ok, so an unsigned announce could repoint an existing peer's URL and inherit thereachable = truethe previous host earned. The federated repo fan-out (crates/gitlawb-node/src/api/repos.rs:1451) filters on that flag and then stamps each entry from the peer's response body with that peer's DID, so the inheritance is what made the injection instant rather than probe-delayed. Reaching the branch needs no credential:require_signed_peer_writesdefaults false (config.rs:48) and the announce handler's unsigned path only logs a warning.The change
One clause on the existing statement:
In the
DO UPDATEbranchpeers.http_urlis the existing pre-update row and$2is the proposed value, so the comparison reads the stored URL, and it evaluates under the conflict row lock instead of a read-then-write that could lose an update. Both writers funnel throughupsert_peer(the announce handler and the bootstrap announce-back inmain.rs), so one clause covers both with no per-caller opt-out. No schema change, no migration.What this bounds, and what it does not
Worth being precise, since the flag is easy to over-trust:
Only the federated repo fan-out gates on
last_ping_ok. Four consumers act on a repointedhttp_urlregardless of it (sync.rs's origin resolve, the post-receive notify fan-out,trigger_sync, and the public resolve route), and two read surfaces republish it asreachable. So this removes the automatic inheritance; it does not close the rewrite. Binding a DID to its first-seen announcing key is what closes it, which is #273 and a federation-model decision rather than a patch.Until #248 lands, the reset is also bypassable in one unauthenticated request:
GET /api/v1/peers/{did}/pingcarries no auth layer and no IP brake, and writesmark_peer_pingfrom the stored URL's own probe response. #248 already deletes that write and is approved, so this branch deliberately leavesping_peeralone rather than editing the same three lines.Comparison is exact, which cuts both ways. A cosmetic difference such as a trailing slash also clears the flag, and an unauthenticated caller can use that to hold a legitimate peer out of the flag-gated fan-out at roughly 12 requests per hour per victim against the 600/hour/IP default brake. That cost is accepted here because the alternative is silent trust inheritance, and it goes away with #273. Normalizing instead would mean canonicalizing the stored value, this comparison, and the existing
trim_end_matchescall sites.All of the above is recorded in a comment at the fix site so the next reader does not mistake the mitigation for a closure.
Verification
Tests landed before the fix, in their own commit, and the failure was observed rather than assumed:
url_change_clears_reachabilitylast_ping_okassertion, with thehttp_urlassertion passingThree of the six are green in both directions by design. They are regression pins, and each one was confirmed load-bearing by applying the wrong implementation it targets and watching it go red:
CASEclause removedurl_change_clears_reachabilitylast_ping_ok = (peers.http_url IS NOT DISTINCT FROM $2)same_url_reannounce_does_not_grant_reachabilityWHEREsame_url_reannounce_still_advances_last_seenrtrim(...)normalizing comparisoncosmetic_url_difference_counts_as_a_changeEach of those four passes at least three of the other tests, which is why the set is six and not one. The second would be the dangerous regression: it lets any unsigned same-URL re-announce set the flag TRUE with no probe at all, which is worse than the bug under repair. The third silently stops
last_seenadvancing on a liveness re-announce, so the peer ages out.cargo test -p gitlawb-node: 516 passed, 0 failed, no pre-existing test flipped.cargo clippy --all-targets -- -D warningsclean workspace-wide.cargo fmt --checkclean.Scope
Issue #270 was narrowed to this mitigation on 2026-07-28, with the rest split out: #272 (unvalidated
reposlug on the same unsigned route, shipped as #274) and #273 (bind a DID to its first-seen key). Nothing here touchessync.rs,api/repos.rs,api/resolve.rs,main.rs, orserver.rs.Closes #270
Summary by CodeRabbit