Skip to content

relay: per-IP rate limit on /v1/server + /v1/client WS upgrades #34

@ilmoniemi

Description

@ilmoniemi

Why

docs/threat-model.md § DoS resistance flags connection floods + fork-bomb retry as in-scope concerns. Today both /v1/server and /v1/client accept unbounded WS upgrade attempts from any single IP. A misbehaving phone (or an attacker) can pin per-connection memory + goroutines without amortised cost.

What

A per-IP token bucket on the WS upgrade attempts (NOT on individual frames — frame-level cost is bounded by SetReadLimit #29 + max-phones #30). Reject excess attempts before websocket.Accept with a 429 (HTTP — pre-upgrade) so the cost of rejection is the cheapest possible.

Suggested starting policy (architect to refine from threat model):

  • 10 upgrade attempts / IP / minute, burst 20
  • Bucket store: in-memory map keyed by client IP, with periodic eviction of stale buckets

Implementation notes

  • Lives as middleware around both upgrade handlers in cmd/pyrycode-relay/main.go
  • Pure function for bucket-decision (testable in isolation; same shape as Registry's race-tested patterns)
  • Behind a reverse proxy (nginx, Cloudflare, fly-proxy), X-Forwarded-For is the source of truth — handle deployment-context awareness via a config flag (--trust-x-forwarded-for) so a misconfigured deploy doesn't trust spoofable headers
  • Rate-limit decisions get a slog line (without IP if log hygiene rules forbid; check threat model)

Out of scope

  • Rate limiting per server-id or per device token (different threat surface; consider separately if it surfaces)
  • Distributed rate limiting (single-instance only — multi-instance would need shared state / Redis)

Metadata

Metadata

Assignees

No one assigned

    Labels

    security-sensitiveTouches auth, crypto, or internet-exposed input paths

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions