Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions IMPLEMENTERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 43 additions & 3 deletions netcode.c
Original file line number Diff line number Diff line change
Expand Up @@ -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++ )
{
Expand Down Expand Up @@ -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 };

Expand Down Expand Up @@ -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 );
Expand Down
Loading