Repository navigation
Commit d00d89a
* feat(node): add per-client-IP limiters + knobs for the peer-sync routes (#82)
Plumb two dedicated per-IP RateLimiters into config/state so the peer-sync
enqueue routes can be braked independently: sync_trigger_rate_limiter (tight,
GITLAWB_SYNC_TRIGGER_RATE_LIMIT, default 60/hr) and peer_write_rate_limiter
(generous, GITLAWB_PEER_WRITE_RATE_LIMIT, default 600/hr). Both are bounded and
swept by the periodic cleanup loop, keyed via the node-wide push_limiter_trust.
No route is wired to them yet; that lands next.
* fix(node): require a signature on /sync/trigger and rate-limit the peer-sync routes (#82)
/api/v1/sync/trigger was reachable unauthenticated in the default config and
drove an O(peers) outbound fan-out + per-repo sync_queue enqueue — an anonymous
amplification/DoS. The same enqueue_sink is reachable via /sync/notify, which
also accepts unsigned known-peer requests and had no rate limit.
- Move /sync/trigger into its own always-signed route group (add_auth_layers),
independent of require_signed_peer_writes, and give it a tight per-IP brake.
- Add a separate, generous per-IP brake to the peer-write group
(/peers/announce, /sync/notify); notify's signature behavior is unchanged
(rolling-upgrade compat), it only gains the brake. Separate buckets so an
unsigned notify flood can't drain the signed trigger caller's quota.
- Per-IP (not per-DID): a did:key farm self-registers, so a signature/per-DID
gate does not cap cost (INV-10); the brake keys on the client IP before auth.
Adds an adversarial test matrix (unsigned->401 both modes, IP flood->429,
DID-agnostic brake, forged X-Forwarded-For can't bypass, notify flood->429,
distinct-IP not throttled, cross-route non-exhaustion, 0-disables-but-sig-holds,
announce still accepts unsigned).
* fix(gl): surface node denial on sync trigger and sanitize the error body (#82)
Now that the node gates /api/v1/sync/trigger behind a signature and rate limit,
'gl sync trigger' must not render a 401/429 as a fabricated '✓ sync triggered /
0 peers' success. Check resp.status() before parsing; on a non-2xx, extract the
node's message and bail. Strip C0/C1 control bytes from the node-supplied string
before printing (INV-6) — a hostile node could embed ANSI/OSC escapes in the
body. Adds mockito tests: 401/429 surface as errors (RED without the status
check), control bytes are stripped, and the 200 path is unchanged.
* fix(review): bound gl error-body read, strip bidi controls, genericize 429 message (#82)
Code-review follow-ups on the #82 change:
- gl sync trigger: read the node error body with a capped reader
(read_body_capped) instead of resp.text(), so a hostile/broken node can't
force an unbounded allocation to surface a denial (INV-6, read half).
- sanitize_node_msg: also strip Unicode bidi/format controls (U+202A-202E,
U+2066-2069, LRM/RLM/ALM) — is_control() only covers the Cc category, but a
right-to-left override can reorder the displayed error line.
- too_many_requests(): the shared per-IP 429 middleware now serves the peer-sync
routes too, so drop the push-specific wording ('rate limit exceeded'); the
push-specific 429 in the receive-pack handler is unchanged.
Adds tests for the bidi/length sanitize and the oversized-body path.
* test(#82): close reasoned-not-run gaps with load-bearing tests
Executes the branches that were previously only reasoned:
- read_body_capped byte-cap test (RED against resp.text() — 2MB body -> ≤cap):
vets the bounded-read fix, not just the display cap.
- sync-route 429 body asserts 'rate limit exceeded' and NOT 'push' (RED before
the message was genericized); also finishes genericizing the warn log, which
the response-body change had missed.
- real RFC 9421 signature through the full router reaches the handler (200) in
both config modes — the positive path through the actual gate, not a
direct-mount with an injected DID.
- /peers/announce flood -> 429 (the peer_write brake covers announce, not just
notify).
- trigger_counts extraction unit test (happy + malformed -> 0,0), closing the
count-formatting gap in the 200 path.
No production behavior change beyond the warn-log wording.
* fix(#82): require an identity for sync trigger and fix the rollout docs
Address review feedback on the always-signed /sync/trigger route.
- gl sync trigger now requires a local identity via load_keypair_from_dir and
fails locally with "identity not found" before issuing a request, instead of
.ok() silently sending an unsigned request that can only 401 remotely. Add a
test proving it fails before dialing the node.
- Split /sync/trigger out of the staged unsigned peer-write rollout in the
README and the OSS-readiness audit; only /peers/announce and /sync/notify keep
the rolling-upgrade unsigned behavior.
- Reorder the two peer-sync rate-limit vars in .env.example for dotenv-linter.
- Bump crossbeam-epoch to 0.9.20 (RUSTSEC-2026-0204), matching main, so cargo
audit passes.
---------
Co-authored-by: t <t@t>
1 parent 67ad2b8 commit d00d89a
13 files changed
Lines changed: 851 additions & 23 deletions
File tree
- crates
- gitlawb-node/src
- api
- auth
- gl/src
- docs
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
95 | 95 | | |
96 | 96 | | |
97 | 97 | | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
98 | 108 | | |
99 | 109 | | |
100 | 110 | | |
| |||
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
302 | 302 | | |
303 | 303 | | |
304 | 304 | | |
305 | | - | |
| 305 | + | |
306 | 306 | | |
307 | 307 | | |
308 | 308 | | |
309 | 309 | | |
310 | | - | |
311 | 310 | | |
312 | 311 | | |
313 | | - | |
| 312 | + | |
314 | 313 | | |
315 | 314 | | |
316 | 315 | | |
317 | 316 | | |
318 | 317 | | |
| 318 | + | |
| 319 | + | |
319 | 320 | | |
320 | 321 | | |
321 | 322 | | |
| |||
0 commit comments