fix: reconnect-proof rate limiting (stable id + real client IP)

Socket-event rate limits keyed on socket.id (new on every reconnect) and
passed NO ip argument, so the "IP-limited" prefixes never actually applied —
reconnecting reset every limit, and chat had no IP limit at all.

- New rateLimitContext helper resolves a STABLE identity (anon_token -> id,
  falling back to socket id) and the real client IP (X-Forwarded-For honoured
  only when TRUST_PROXY=true, else handshake address — no spoofing on direct
  exposure).
- game:start, address:set and chat:message now pass (stableId, action, ip);
  connection:new uses the real IP. 'chat:' added to IP-limited prefixes.
- Express 'trust proxy' enabled when TRUST_PROXY=true; documented in
  .env.example. Aligned the contradictory default limit values with the
  production override.

Adds rateLimit.test.js (reconnect can't bypass, IP backstop for fresh
identities, XFF gating). Full suite: 18 suites / 143 tests.
This commit is contained in:
jwinterm
2026-05-31 10:21:28 -04:00
parent 41676fa072
commit 448cb2f528
9 changed files with 146 additions and 24 deletions
+5
View File
@@ -343,6 +343,11 @@ RPC_POLL_INTERVAL=2000
# Same-origin requests are always allowed. Leave unset if admin panel is on same host.
# ALLOWED_ORIGINS=https://admin.example.com
# Set to 'true' ONLY when running behind a trusted reverse proxy (Nginx/Caddy). When
# true, the real client IP is read from the X-Forwarded-For header for rate limiting and
# req.ip. Leave unset/false for direct exposure, otherwise clients could spoof their IP.
# TRUST_PROXY=true
# =============================================================================
# SECTION 12: SERVER & MULTI-INSTANCE
# =============================================================================