netcode 1.4.5: the connect token lifecycle, and the fixes around it - #184
Merged
Conversation
A connect token history entry now carries a state. It is created pending when the server accepts a connection request for a connect token it has not seen, admits retransmitted requests from that same address while the handshake runs, and becomes consumed when the client is installed in a client slot. A consumed entry admits nothing, whatever the source address, so the keys inside a connect token encrypt exactly one session. Entries live until their connect token expires, and a history whose entries all hold unexpired tokens refuses a new connect token instead of evicting one. An entry's time is set at creation and never refreshed. The encryption mapping carries the index of the history entry its handshake belongs to, so installing a client consumes the right entry without anything new on the wire. The server refuses any connect token that could have been issued before it started: max_connect_token_lifetime in the server config is the longest lifetime the backend issues, and a connection request whose connect token expire timestamp minus that lifetime is earlier than the server start time is ignored, alongside the existing expiry check and before the decrypt. The field defaults to NETCODE_DEFAULT_MAX_CONNECT_TOKEN_LIFETIME, and the examples and harnesses set it to the lifetime they issue. Also in this release: - Key material is erased with sodium_memzero rather than memset: the encryption manager's keys on reset and on removal, the client's connect token and context, the server's challenge key on stop and its configured private key on destroy. - IPv4 addresses are converted through ntohl and htonl and IPv6 halves through memcpy, so address conversion is correct on big endian machines, and CI proves it on s390x under QEMU. - netcode_packet_queue_clear pops until the queue is empty rather than freeing the first num_packets slots, so a partially drained queue is cleared correctly. The drain loops its two callers ran first are gone. - The soak harness formats addresses with snprintf. - The default static install is self contained: the vendored sodium objects are compiled into libnetcode, and the install carries an exported CMake package that consumers find with find_package(netcode CONFIG) and link as netcode::netcode. The system libsodium build exports its libsodium instead. CI links a program against both from a clean prefix. - Shared library builds are refused on Windows, where netcode.h declares no export macro. Tests: test_connect_token_entries covers the pending and consumed rules, the unrefreshed entry time, the full history refusal and reuse after expiry; a client and server wired directly through the send and receive overrides drive test_client_server_connection_request_retransmission, which drops the first server packets so the client retransmits its connection request, test_client_server_replay_across_sessions, which replays a first-session datagram into a second session, and test_client_reconnect_with_used_connect_token and test_client_error_connect_token_predates_server_start, which pin the two refusals. Building the test runner with -DNETCODE_NONCE_AUDIT=ON records the key and nonce of every packet the suite encrypts and fails on a repeat; it is a CI leg. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
netcode_server_destroy and netcode_client_destroy each send disconnect packets on the way out, and the wire handed them straight to the other end, which the first destroy had already freed. The wire is down once teardown starts, which is what an application shutting down finds too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The entry time is created-at, not last-seen, and the expire timestamp is the connect token's, which is what bounds the entry's life. Both read as ordinary bookkeeping without that. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The implementation of the rules the STANDARD.md PR states (#183), plus the build, portability and hygiene fixes that ride with the release. Version 1.4.5 in
netcode.hand CMake.Nothing on the wire moves.
The connect token lifecycle
A connect token history entry carries a state:
Entries live until their connect token expires, and a history whose entries all hold unexpired connect tokens refuses a new connect token instead of evicting one, so a flood of connect tokens cannot reopen a token that has been used. An entry's time is set at creation and never refreshed.
The encryption mapping carries the index of the history entry its handshake belongs to, so installing a client consumes the right entry with nothing extra on the wire.
The restart rule
max_connect_token_lifetimein the server config is the longest lifetime the backend issues connect tokens with. The server ignores any connection request whose connect token expire timestamp minus that lifetime is earlier than the time it started, next to the existing expiry check and before the decrypt. The field defaults toNETCODE_DEFAULT_MAX_CONNECT_TOKEN_LIFETIME(30 seconds, the lifetime the examples issue), a zero or negative value takes that default, and the examples and harnesses set it to the lifetime they actually issue.Also in this release
sodium_memzerorather thanmemsetfor the encryption manager's keys on reset and on removal, the client's connect token and context, the server's challenge key on stop, and the private key in the server config at destroy.ntohlandhtonl, IPv6 halves throughmemcpyinstead of auint16_tpointer cast. A big endian s390x job under QEMU runs the suite in Debug and Release.netcode_packet_queue_clearpops until the queue is empty instead of freeing the firstnum_packetsslots, which frees the wrong pointers oncestart_indexhas moved. The drain loops its two callers ran beforehand are gone, andnetcode_client_destroyis covered as a caller.snprintfin the soak harness.libnetcode, and the install carries an exported CMake package. A consumer writesfind_package(netcode CONFIG REQUIRED)and linksnetcode::netcode. The system libsodium build exports the libsodium it found instead. A CI job installs both to a clean prefix and links a program that callsnetcode_initagainst them.netcode.hdeclares no export macro, so a DLL built from these sources exports nothing.Tests
test_connect_token_entries— the pending and consumed acceptance rules, the entry time that is never refreshed, the full history refusal, and reuse of an entry once its connect token has expired.test_client_server_connection_request_retransmission— a client and server wired directly to each other through the send and receive overrides, with the first three packets the server sends dropped, so the client retransmits its connection request into a handshake the server already holds a pending entry for, and still connects.test_client_server_replay_across_sessions— a payload datagram captured from a first session is replayed into a second one and delivers nothing.test_client_reconnect_with_used_connect_token— presenting a spent connect token again, from the address that used it, connects nothing.test_client_error_connect_token_predates_server_start— a connect token that expires earlier than any the backend could have issued after the start is refused, and one with the full lifetime connects.-DNETCODE_NONCE_AUDIT=ONbuilds the test runner with the key and nonce of every packet it encrypts recorded, andtest_nonce_auditfails the run if any pair repeats. 3716 pairs, zero repeats. It is a test-only build option and a CI leg; nothing it adds is compiled into the library.Each test was run against a build with the rule it covers reverted, and each goes red:
test_connect_token_entriescheck failed: ... == NETCODE_CONNECT_TOKEN_ENTRY_REFUSEDtest_connect_token_entriescheck failed: ... == NETCODE_CONNECT_TOKEN_HISTORY_FULLtest_client_server_connection_request_retransmissioncheck failed: ... == NETCODE_CLIENT_STATE_CONNECTEDtest_client_server_replay_across_sessionscheck failed: netcode_server_receive_packet( ... ) == NULLtest_client_reconnect_with_used_connect_tokencheck failed: ... == NETCODE_CLIENT_STATE_CONNECTION_REQUEST_TIMED_OUTtest_client_error_connect_token_predates_server_startcheck failed: ... == NETCODE_CLIENT_STATE_CONNECTION_REQUEST_TIMED_OUTtest_nonce_auditcheck failed: netcode_nonce_audit_repeats() == 0Built and tested Debug and Release locally, and both install shapes were installed to a clean prefix and linked by the consumer program.
🤖 Generated with Claude Code