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)
Why
docs/threat-model.md§ DoS resistance flags connection floods + fork-bomb retry as in-scope concerns. Today both/v1/serverand/v1/clientaccept 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.Acceptwith a429(HTTP — pre-upgrade) so the cost of rejection is the cheapest possible.Suggested starting policy (architect to refine from threat model):
Implementation notes
cmd/pyrycode-relay/main.goRegistry's race-tested patterns)X-Forwarded-Foris the source of truth — handle deployment-context awareness via a config flag (--trust-x-forwarded-for) so a misconfigured deploy doesn't trust spoofable headersslogline (without IP if log hygiene rules forbid; check threat model)Out of scope