From dc21b7015ab791b87a1befd8523812ff67defefc Mon Sep 17 00:00:00 2001 From: Glenn Fiedler Date: Sun, 12 Jul 2026 16:26:35 -0400 Subject: [PATCH 1/2] Re-seed server global packet sequence on start, not just create Global packets (connection challenge, connection denied) encrypt with the same per-connect-token server-to-client key as per-client packets, whose sequences start at zero. The global sequence therefore starts at 1ULL << 63 so the AEAD nonces stay disjoint under the shared key. That seed was only applied in netcode_server_create, and netcode_server_stop reset the global sequence to zero. A server that was stopped and started again sent challenge packets from global sequence 0, 1, 2 ... overlapping the per-client sequence space: the same key and nonce encrypting two different plaintexts, which breaks the AEAD security guarantees. Seed the global sequence in netcode_server_start as well. No wire format change. Pinned by test_server_restart_global_sequence. Found while porting netcode to Rust (mas-bandwidth/netcode.rs). Co-Authored-By: Claude Fable 5 --- netcode.c | 46 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/netcode.c b/netcode.c index 9b9f4d1..0ab9671 100755 --- a/netcode.c +++ b/netcode.c @@ -4117,9 +4117,18 @@ void netcode_server_start( struct netcode_server_t * server, int max_clients ) server->running = 1; server->max_clients = max_clients; server->num_connected_clients = 0; - server->challenge_sequence = 0; + server->challenge_sequence = 0; netcode_generate_key( server->challenge_key ); + // global packets (challenge, denied) encrypt with the same per-token server to client + // keys as per-client packets, whose sequences start at zero, so the global sequence + // lives in the top half of the sequence space to keep AEAD nonces disjoint under a + // shared key. netcode_server_stop zeroes it, so it must be re-seeded on every start, + // not just in netcode_server_create -- otherwise a stopped and restarted server would + // reuse nonces between global and per-client packets. + + server->global_sequence = 1ULL << 63; + int i; for ( i = 0; i < server->max_clients; i++ ) { @@ -7216,8 +7225,38 @@ void test_server_create() } } -static uint8_t private_key[NETCODE_KEY_BYTES] = { 0x60, 0x6a, 0xbe, 0x6e, 0xc9, 0x19, 0x10, 0xea, - 0x9a, 0x65, 0x62, 0xf6, 0x6f, 0x2b, 0x30, 0xe4, +void test_server_restart_global_sequence() +{ + // global packets (challenge, denied) share per-token server to client keys with + // per-client packets, so the global sequence must stay in the top half of the + // sequence space or a stopped and restarted server reuses AEAD nonces. regression + // test: netcode_server_stop zeroes the global sequence, start must re-seed it. + + struct netcode_server_config_t server_config; + netcode_default_server_config( &server_config ); + + struct netcode_server_t * server = netcode_server_create( "127.0.0.1:40000", &server_config, 0.0 ); + + check( server ); + check( server->global_sequence == 1ULL << 63 ); + + netcode_server_start( server, 1 ); + + check( server->global_sequence == 1ULL << 63 ); + + server->global_sequence += 1000; // as if the server had sent some global packets + + netcode_server_stop( server ); + + netcode_server_start( server, 1 ); + + check( server->global_sequence == 1ULL << 63 ); + + netcode_server_destroy( server ); +} + +static uint8_t private_key[NETCODE_KEY_BYTES] = { 0x60, 0x6a, 0xbe, 0x6e, 0xc9, 0x19, 0x10, 0xea, + 0x9a, 0x65, 0x62, 0xf6, 0x6f, 0x2b, 0x30, 0xe4, 0x43, 0x71, 0xd6, 0x2c, 0xd1, 0x99, 0x27, 0x26, 0x6b, 0x3c, 0x60, 0xf4, 0xb7, 0x15, 0xab, 0xa1 }; @@ -9495,6 +9534,7 @@ void netcode_test() RUN_TEST( test_network_simulator_determinism ); RUN_TEST( test_client_create ); RUN_TEST( test_server_create ); + RUN_TEST( test_server_restart_global_sequence ); RUN_TEST( test_client_server_connect ); RUN_TEST( test_client_server_ipv4_socket_connect ); RUN_TEST( test_client_server_ipv6_socket_connect ); From 919aa2494609cfe1df649be61202c3f8275fe11b Mon Sep 17 00:00:00 2001 From: Glenn Fiedler Date: Sun, 12 Jul 2026 16:27:55 -0400 Subject: [PATCH 2/2] Document the global sequence restart fix in IMPLEMENTERS.md Adds finding 3: the server global packet sequence must be re-seeded to 1 << 63 on every start, not just in create, or a stopped-and-restarted server reuses AEAD nonces between global and per-client packets under the shared per-token server-to-client key. Fixed in dc21b70. Ports that copied the create/stop behavior should check their restart path. Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 7 ++++++- IMPLEMENTERS.md | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index a5c4d4a..0bdc3c3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -146,7 +146,12 @@ biggest code-quality issue is consolidated — `netcode_server_process_packet` d to the shared read-and-process path, the client receive loops feed `netcode_client_process_packet`, sockaddr conversion and simulator/override/socket send dispatch are single helpers, and client slot reset is shared between the disconnect -paths, for a net −116 lines with no public header or wire change.) +paths, for a net −116 lines with no public header or wire change; the server re-seeds +its global packet sequence to `1ULL << 63` in `netcode_server_start`, not just in +create — `netcode_server_stop` zeroes it, so a stopped-and-restarted server used to +send challenge packets from sequence 0 and reuse AEAD nonces against per-client packets +under the same per-token key — pinned by test_server_restart_global_sequence and +written up as finding 3 in IMPLEMENTERS.md.) **Process.** CI builds and tests every push across Linux x64, Linux arm64, macOS Apple Silicon, and Windows (MSVC + MinGW) in Debug and Release, runs an ASan+UBSan leg, and diff --git a/IMPLEMENTERS.md b/IMPLEMENTERS.md index 7f0374c..cd651e8 100644 --- a/IMPLEMENTERS.md +++ b/IMPLEMENTERS.md @@ -65,6 +65,32 @@ implementation was written from the older document rather than from the code, ve placed it before. The corrected list also notes the replay window advances only after a successful decrypt. +## 3. Server global packet sequence must be re-seeded on restart (check your port) + +**Where:** the server start/stop lifecycle. + +Global packets — the packets a server sends before a client occupies a slot, i.e. +_connection challenge_ and _connection denied_ — are encrypted with the same +per-connect-token server-to-client key as per-client packets, whose sequence numbers +start at zero. To keep the AEAD nonces disjoint under that shared key, the server's +global sequence number starts at `1 << 63`, the top half of the sequence space. + +The reference implementation applied that seed only in `netcode_server_create`, and +`netcode_server_stop` reset the global sequence to zero. A server object that was +stopped and then started again sent challenge packets from global sequence 0, 1, 2 ... +— inside the per-client sequence space. Once a client connected, the server encrypted +two different plaintexts (a challenge packet and a keep-alive or payload packet) with +the same key and the same nonce, which voids the confidentiality and authenticity +guarantees of the AEAD for those packets. + +The fix is to seed the global sequence in server start as well as create, so the +invariant holds for every running lifetime of the server object. Not a wire format +change — the sequence seed is server-local behavior. Fixed here in commit `dc21b70`, +pinned by `test_server_restart_global_sequence`. Ports that copied the create/stop +behavior from this codebase should check what their restart path does to the global +sequence. Found while porting netcode to Rust +([netcode.rs](https://github.com/mas-bandwidth/netcode.rs), which seeds on start). + ## Reporting back If you maintain a netcode implementation and confirm (or refute) any of the above in your