A secret session token and a stateless return-address cookie
Status: Accepted (2026-11-02)
Context
A single sequential u32 used to serve both as the authentication of Client → Server packets and as the player's generation, which the server sends to everyone. Anyone who could spoof a source address could therefore act for another session: a forged ClientBye disconnected the victim, and forged requests filled the victim's reliable lane with refusals. Each ClientHello from a new address also took a slot and ran Lua on the server thread, so a flood of spoofed addresses could occupy the server entirely.
This replaces the shared token of ADR 0004 with two separate values.
Decision
- Two values.
session_tokenis au64secret from the operating system's CSPRNG, sent only to the client's own address inServerHello.player_tokenis the publicu32generation that Server → Client packets carry. They have different types, so the server and its tests cannot mix them up. - A stateless cookie proves the return address. The first answer to a
ClientHelloisConnectChallenge, no larger than the request, and the server stores nothing for it. A slot, the player's name and theplayer_connectingevent come only after the client echoes the cookie. The cookie is SipHash-2-4 under a random per-process key over the address, the port and a 10-second time window, so it stays valid for one to two windows. See Handshake. - Limits after the address is proven. At most 5 connections per IP, and at most 5 attempts at once and then one per 2 s per IP that reach
player_connecting; the table of tracked addresses is bounded. Only addresses that answered a cookie are tracked, so the bound applies to real addresses, not to a spoofed flood. Loopback is exempt, because bots and local clients run there. - A repeated
ClientHelloreturns the same session and does not extend it. - The reliable lane cannot be held hostage. A connection whose unacked events have waited 15 s without the acks moving is disconnected; a lossy link that drains slowly keeps moving and lives. Resends are limited to 32 per connection per wakeup, which must outrun the event rate because a lost event makes every later one due (ADR 0007).
- Logging is rate-limited. A warning about a sender passes a per-IP budget and a shared one; the shared budget holds against spoofed addresses, each of which would otherwise get a fresh per-IP budget. Suppressed warnings are counted and logged as one line per second.
- Lua player handles are bound to their connection. After
player_disconnecta handle affects no one, even when a new player takes the same id. Every handle of a connection shares one liveness flag, so this check needs no extra lookup.
Consequences
- Every Client → Server packet changed:
ClientHellogrew by the 8-byte cookie, and packets with a token grew by 4 bytes. - A handle of a player who left answers
nil, err; a gamemode that relied on reusing handles must take the new one. - Per-IP limits are by address: a NAT with more than 5 players behind it hits them. The limit is a constant for now.
Alternatives considered
- Signing or encrypting the traffic. The session token is visible only on the path to the client, and the protocol does not protect against an observer on that path anyway. Signing every frame would cost on every packet without solving a problem the token leaves open. Rejected.
- Server state for every
ClientHello(a SYN queue). A flood of spoofed addresses fills it by itself. Rejected. - A per-client cookie token with a timer. The stateless cookie is simpler and gives an attacker no memory to fill. Rejected.
- A configurable per-IP limit. Not needed yet: the constant with the loopback exemption covers current use.
- Checking the generation through the server facade inside a handle. A handle already reads its id inside the facade access, and nested access is not allowed; a shared per-connection liveness flag needs none. Rejected.
