Skip to content

fix: reduce invite API per-IP rate limit to 4/24h and key on socket peer IP#74

Merged
sanity merged 2 commits into
mainfrom
reduce-invite-rate-limit
Jul 16, 2026
Merged

fix: reduce invite API per-IP rate limit to 4/24h and key on socket peer IP#74
sanity merged 2 commits into
mainfrom
reduce-invite-rate-limit

Conversation

@sanity

@sanity sanity commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Problem

The freenet.org/quickstart flow mints River invitations into the shared
Freenet Official room via gkapi's POST https://gkapi.freenet.org/create-invite.
The room is being spammed via bulk invite generation. Two issues made this easy:

  1. The per-IP limit was 20 invites / 24h — too loose.
  2. The limiter keyed on X-Forwarded-For first. gkapi is directly
    internet-facing
    (binds 0.0.0.0:80/:443, terminates its own Let's
    Encrypt TLS, no reverse proxy; gkapi.freenet.org resolves straight to the
    host). With no trusted proxy in front, X-Forwarded-For is fully
    client-controlled, so the limit was bypassable at any threshold by
    rotating a spoofed header.

Approach

  • Limit 20 → 4 per IP per rolling 24h window. MAX_INVITES_PER_WINDOW = 4
    in rust/api/src/rate_limit.rs; 24h window unchanged. 4 covers legitimate
    re-invites while still slowing bulk creation. The 429 message is updated to
    match.
  • Key the rate limiter on the socket peer IP, not X-Forwarded-For.
    get_client_ip now returns addr.ip() from ConnectInfo<SocketAddr> (already
    wired via into_make_service_with_connect_info::<SocketAddr>() in main.rs)
    and no longer trusts X-Forwarded-For / X-Real-IP. addr.ip() is the bare
    IpAddr (no port), so IPv4/IPv6 peers key correctly. A comment marks the spot
    to revisit only if a trusted reverse proxy / Cloudflare is ever added (trust
    XFF from that proxy's IP only — never blanket XFF trust again).
  • Added test_rate_limiter_enforces_four_per_window, which hardcodes the
    intended value (4) and asserts the 5th invite from an IP is rejected. The
    pre-existing rate-limit tests are parameterized on the constant and would not
    catch an accidental bump (history shows it drifting 5 -> 20). Runs in CI via
    cargo make integration-test (cd rust/api && cargo test).

Testing

cargo test in rust/api — all 5 rate_limit::tests pass, including the new one.

Notes

  • gkapi binds 0.0.0.0 (IPv4-only) today, so peers are IPv4 in practice; the
    bare-IpAddr keying is still correct if IPv6 binding is ever added.
  • Live deploy is a rebuild-from-main on the host (update-from-github.fish);
    it only takes effect after this merges to main.

[AI-assisted - Claude]

🤖 Generated with Claude Code

https://claude.ai/code/session_01B49wBfvR8EjpfYTw5muNV9

The freenet.org/quickstart flow mints River invites into the shared
"Freenet Official" room via gkapi's POST /create-invite endpoint. The
room is being spammed through bulk invite generation, so tighten the
per-IP limit from 20 to 2 invites per rolling 24h window.

Also add test_rate_limiter_enforces_two_per_window, which hardcodes the
intended value. The existing rate-limit tests are parameterized on
MAX_INVITES_PER_WINDOW, so they pass at any value and would not catch an
accidental bump of the constant (its history shows it drifting 5 -> 20).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B49wBfvR8EjpfYTw5muNV9
@sanity

sanity commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Review (Light tier)

  • Codex (external model): clean — "The rate-limit constant, user-facing message, and regression test consistently enforce the intended two-invite limit. No correctness issues were found in the changed code."
  • Adversarial read (self): one finding, already written up in the PR description — the IP source (X-Forwarded-For-first in get_client_ip) is client-spoofable because gkapi is directly internet-facing with no reverse proxy, so a determined spammer can bypass the limit at any threshold. Deliberately left out of this PR to keep it to the requested change; recommended as a follow-up (key on the socket peer addr.ip() since there is genuinely no proxy).

Local cargo test in rust/api: 5/5 rate_limit::tests pass, including the new test_rate_limiter_enforces_two_per_window.

[AI-assisted - Claude]

Per review on PR #74:

- Raise MAX_INVITES_PER_WINDOW from 2 to 4. A few invites per day covers
  legitimate re-invites while still slowing bulk account creation.
- Key the rate limiter on the TCP connection peer address (addr.ip())
  instead of trusting X-Forwarded-For / X-Real-IP. gkapi is directly
  internet-facing (binds :80/:443, own TLS, no reverse proxy), so those
  headers are fully client-controlled and made the limit bypassable at
  any threshold by rotating a spoofed header. ConnectInfo<SocketAddr> is
  already wired via into_make_service_with_connect_info, and addr.ip()
  yields the bare IpAddr so IPv4/IPv6 peers key correctly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B49wBfvR8EjpfYTw5muNV9
@sanity sanity changed the title fix: reduce invite API per-IP rate limit from 20 to 2 per 24h fix: reduce invite API per-IP rate limit to 4/24h and key on socket peer IP Jul 16, 2026
@sanity

sanity commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Re-review after update (limit → 4, IP source → socket peer)

The prior review was on the earlier diff; re-ran on the new HEAD 1df2ad8.

  • Codex (external model): clean — "The lower invite limit is consistently reflected in implementation, tests, and API messaging. Using the direct socket peer address also closes the spoofable proxy-header bypass for the documented deployment topology." No findings.
  • Local cargo test in rust/api: 5/5 rate_limit::tests pass, including test_rate_limiter_enforces_four_per_window.

CI note: the build check (Freenet Deploy workflow) builds Hugo + WASM and does not compile rust/api; the "Integration Tests" workflow that runs cargo test is workflow_dispatch-only (its pull_request/push triggers are commented out). So the rust/api change is validated by the local cargo test above, not by CI. deploy correctly SKIPPED (gated to push-to-main).

[AI-assisted - Claude]

@sanity
sanity marked this pull request as ready for review July 16, 2026 14:12
@sanity
sanity merged commit 6aa4823 into main Jul 16, 2026
3 checks passed
@sanity
sanity deleted the reduce-invite-rate-limit branch July 16, 2026 14:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant