You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
STANDARD.md: scope the nonce guarantee to a server run, add optional restart mitigations (#165)
Purely additive: 26 lines added, 0 removed. No existing normative text changes,
no packet changes, protocol version stays NETCODE 1.02. An implementation that
ignores all of this remains conformant and interoperable.
THE BLIND SPOT. The spec stated that sequence numbers are used as nonces, but
never stated the SCOPE of the uniqueness guarantee. It holds within a single
server run: per-client sequences start at 0, the global sequence starts at 2^63.
It does not survive a restart, because the server-to-client key lives in the
connect token and the used-token history is in memory. A client presenting the
same still-unexpired connect token to a restarted server repeats a nonce under
that key. Identical in the C reference, netcode.go and netcode.rs -- a design
limit, not a port defect, and out of scope of dc21b70 (which is about the two
sequence spaces staying disjoint WITHIN a run).
INVARIANT, NOT SITES. The normative requirement is that STARTING the server sets
the global sequence to 2^63; the value while stopped is explicitly unspecified.
This matters: the C reference zeroes it in netcode_server_stop, while netcode.go
and netcode.rs set 2^63 there. All three are correct because start restores the
floor. Wording it as 'set on start and on stop' would have made the reference
implementation non-conformant to its own specification.
TWO OPTIONAL MITIGATIONS, both MAY, both wire-compatible, neither needing any
client change:
- reject connect tokens whose expire timestamp predates server start + the
maximum token lifetime (stateless, two comparisons, no startup delay);
- persist the used-connect-token history across restarts (durable state, but
correct when token lifetimes vary).
The optional check is placed explicitly in the ordered connection-request steps,
immediately after the expiry check and before the decrypt, since it reads only
the unencrypted expire timestamp.
An earlier draft proposed rejecting tokens by CREATE timestamp. That is
impossible: create_timestamp lives in the public connect token that only the
client parses. The private token the server decrypts carries client_id, timeout,
addresses, both keys and user data -- no creation time. Putting it there would
change the wire format.
Client state machine section updated to say the rejection reuses the existing
ignore path: no new state, no new transition, indistinguishable from an
unreachable server, and the correct response (request a new token) is already
what those states imply.
Copy file name to clipboardExpand all lines: STANDARD.md
+26Lines changed: 26 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -313,6 +313,28 @@ Replay protection is applied to the following packet types on both client and se
313
313
314
314
The replay buffer size is implementation specific, but as a guide, a few seconds worth of packets at a typical send rate (20-60HZ) should be supported. Conservatively, a replay buffer size of 256 entries per-client should be sufficient for most applications.
315
315
316
+
## Nonce Reuse and Server Restarts
317
+
318
+
Encrypted packet sequence numbers are used directly as nonces for packet encryption (see _Replay Protection_ above). Within a single run of a dedicated server instance, netcode keeps the nonce spaces disjoint:
319
+
320
+
* Per-client packet sequence numbers start at zero for each client slot.
321
+
322
+
* The server's _global sequence number_, used for packets sent before a client is assigned to a slot (_connection challenge packet_ and _connection denied packet_), is set to 2^63 when the server starts.
323
+
324
+
The normative requirement is that starting the server sets the global sequence number to 2^63. The value it holds while the server is not running is unspecified, and an implementation may leave it at 2^63 or zero it; what matters is that a start restores the floor. Because per-client sequence numbers start at zero and the global sequence number is at least 2^63 for the whole run, global packets and per-client packets never share a sequence number under the same key during a run.
325
+
326
+
**This guarantee is scoped to a single run of the server, and does not extend across a restart.** The server to client key is carried inside the private connect token, so it is the same key every time that connect token is presented. The history of connect tokens already used (see _Processing Connection Requests_) is held in memory, and is therefore empty after a restart. Per-client sequence numbers also begin again at zero. A client that presents the same connect token to a server that has restarted will therefore have packets encrypted under that key at sequence numbers that were already used before the restart, repeating a nonce under that key.
327
+
328
+
The window is bounded by connect token expiry. A server ignores any connection request whose connect token expire timestamp is <= the current timestamp, so only a connect token that is still unexpired after the restart can be affected.
329
+
330
+
An implementation MAY close the window entirely. Both of the options below are optional, neither changes any packet, and neither requires any change to clients. **A server that implements neither remains conformant and fully interoperable, and the protocol version remains "NETCODE 1.02" either way.**
331
+
332
+
***Reject connect tokens that predate the server start.** The server records its start time, and ignores any connection request whose connect token expire timestamp is earlier than the start time plus the maximum connect token lifetime issued by the deployment. A connect token issued before the restart always fails this test, because its expire timestamp is at most its issue time plus that lifetime. A connect token issued with a full lifetime after the restart always passes it, so the server can accept connections immediately on start. This requires no persistent state, and no data beyond the expire timestamp already present in the connection request packet. Note that a connect token deliberately issued with a shorter lifetime than the configured maximum will be rejected until the maximum has elapsed.
333
+
334
+
***Persist the connect token history.** The server writes the history of connect tokens already used - the private connect token hmac, address and time - to durable storage, and restores it on start. A connect token used before the restart is then rejected after it by the existing rule. This costs durable storage, and is the option to choose when the deployment issues connect tokens with widely varying lifetimes.
335
+
336
+
Neither option requires the client to know which, if either, the server implements. In both cases a rejected connect token produces the existing behaviour: the connection request is ignored, and the client requests a new connect token from the web backend.
337
+
316
338
## Client State Machine
317
339
318
340
The client has the following states:
@@ -356,6 +378,8 @@ All other transitions from _sending connection request_ are failure cases. In th
356
378
357
379
If a _connection request denied_ packet is received while in _sending connection request_ the client transitions to _connection denied_. If neither a _connection challenge packet_ or a _connection denied packet_ are received within the timeout period specified in the connect token, the client transitions to _connection request timed out_.
358
380
381
+
A server that implements either of the optional restart mitigations described in _Nonce Reuse and Server Restarts_ rejects an affected connect token by ignoring the connection request, which is the same path as any other rejected request. No new client state and no new transition is required, and the client cannot distinguish this case from a server that is unreachable: it retries at its normal rate, moves on to the next server address in the connect token, and finally transitions to _connection request timed out_, or to _connect token expired_ if the whole process outlasts the token. The correct client response is to request a new connect token from the web backend, which is already the response to those states.
382
+
359
383
### Sending Challenge Response
360
384
361
385
While in _sending challenge response_ the client sends _challenge response packets_ to the server at some rate, like 10HZ.
@@ -432,6 +456,8 @@ The server takes the following steps, in this exact order, when processing a _co
432
456
433
457
* If the connect token expire timestamp is <= the current timestamp, ignore the packet.
434
458
459
+
* OPTIONAL. If the server implements the connect token restart mitigation described in _Nonce Reuse and Server Restarts_, and the connect token expire timestamp is earlier than the server start time plus the maximum connect token lifetime issued by the deployment, ignore the packet. This check belongs here, immediately after the expiry check and before the decrypt, because it uses only the unencrypted expire timestamp and so costs nothing on a request that is going to be rejected anyway. A server that does not implement the mitigation omits this step entirely.
460
+
435
461
* If the encrypted private connect token data doesn't decrypt with the private key, using the associated data constructed from: version info, protocol id and expire timestamp, ignore the packet.
436
462
437
463
* If the decrypted private connect token fails to be read for any reason, for example, having a number of server addresses outside of the expected range of [1,32], or having an address type value that is not 1 (IPv4) or 2 (IPv6), ignore the packet.
0 commit comments