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
19 changes: 10 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ independent implementations (C#, Go, Rust, TypeScript).
- `sodium/` — vendored subset of libsodium, amalgamated into a single `sodium.h` +
`sodium.c` pair (see `sodium/NOTES.md` for how it is generated and validated).
- Build: CMake. `cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build --parallel`,
then `ctest --test-dir build --output-on-failure` runs the suite (40 tests). The
then `ctest --test-dir build --output-on-failure` runs the suite (41 tests). The
`netcode_test` target compiles netcode.c into itself with `NETCODE_ENABLE_TESTS`, so it
links only sodium. `-DNETCODE_SANITIZE=ON` adds ASan+UBSan (sodium gets ASan only);
`-DNETCODE_FUZZ=ON` builds the `fuzz/` harnesses (libFuzzer where available, else a
Expand Down Expand Up @@ -72,7 +72,7 @@ hard-disconnecting client doesn't wedge `recvfrom` (netcode.c:554), `IPV6_V6ONLY
dual-stack IPv4+IPv6 sockets, loopback clients for integrated host-and-play, allocator
override hooks, and full send/receive transport overrides. A built-in network simulator
(latency/jitter/loss/duplication) makes the connection tests deterministic without
touching real sockets. Few networking libraries ship this complete a test story: 40
touching real sockets. Few networking libraries ship this complete a test story: 41
unit + integration tests covering every client error state, reconnect, multi-server
fallback, dual-stack, loopback — plus a soak test and a profiler. All pass today.

Expand Down Expand Up @@ -109,13 +109,14 @@ also by design, not a gap: UDP is unreliable, so a send error is semantically id
to a dropped packet, and the protocol must tolerate drops anyway. A persistently dead
socket surfaces the same way persistent packet loss does — as a connection timeout
through the state machine, which is the correct channel for an unreliable protocol.
The actual gaps are the places the state machine can't reach. Creation failures
collapse to NULL: a bad address string, a socket creation failure, and a bind failure
(the operationally common one — port already in use) are indistinguishable to the
caller, even though the internal `NETCODE_SOCKET_ERROR_*` codes (netcode.c:486)
enumerate exactly these cases before being discarded. And the server has no
state-machine equivalent at all — its failures reduce to `netcode_server_running()`
returning false with no why.
The actual gaps are the places the state machine can't reach. Client create failures
are now queryable — when `netcode_client_create` returns NULL,
`netcode_client_create_error()` reports which step failed (address parse, socket
create per family, simulator-requires-port, allocation) — though the finer
`NETCODE_SOCKET_ERROR_*` granularity (bind vs sockopt vs create) still stops inside
the socket layer. The remaining gap is the server: create returning NULL is
undifferentiated, and there is no state-machine equivalent — failures reduce to
`netcode_server_running()` returning false with no why.

**Small sharp edges:**
- Global mutable state (log level, printf/assert hooks, the `netcode_init` reference
Expand Down
102 changes: 102 additions & 0 deletions netcode.c
Original file line number Diff line number Diff line change
Expand Up @@ -2673,6 +2673,13 @@ struct netcode_client_t
int loopback;
};

static int client_create_error;

int netcode_client_create_error()
{
return client_create_error;
}

int netcode_client_socket_create( struct netcode_socket_t * socket,
struct netcode_address_t * address,
int send_buffer_size,
Expand All @@ -2689,6 +2696,8 @@ int netcode_client_socket_create( struct netcode_socket_t * socket,
{
if ( netcode_socket_create( socket, address, send_buffer_size, receive_buffer_size ) != NETCODE_SOCKET_ERROR_NONE )
{
client_create_error = ( address->type == NETCODE_ADDRESS_IPV6 ) ? NETCODE_CLIENT_CREATE_ERROR_CREATE_SOCKET_IPV6_FAILED
: NETCODE_CLIENT_CREATE_ERROR_CREATE_SOCKET_IPV4_FAILED;
return 0;
}
}
Expand All @@ -2698,6 +2707,7 @@ int netcode_client_socket_create( struct netcode_socket_t * socket,
if ( address->port == 0 )
{
netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: must bind to a specific port when using network simulator\n" );
client_create_error = NETCODE_CLIENT_CREATE_ERROR_SIMULATOR_REQUIRES_PORT;
return 0;
}
}
Expand All @@ -2713,6 +2723,8 @@ struct netcode_client_t * netcode_client_create_dual( NETCODE_CONST char * addre
netcode_assert( config );
netcode_assert( netcode.initialized );

client_create_error = NETCODE_CLIENT_CREATE_ERROR_NONE;

// tolerate a zeroed config: default the allocator functions so a forgotten
// netcode_default_client_config is an inconvenience, not a crash

Expand All @@ -2732,12 +2744,14 @@ struct netcode_client_t * netcode_client_create_dual( NETCODE_CONST char * addre
if ( netcode_parse_address( address1_string, &address1 ) != NETCODE_OK )
{
netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: failed to parse client address\n" );
client_create_error = NETCODE_CLIENT_CREATE_ERROR_PARSE_ADDRESS_FAILED;
return NULL;
}

if ( address2_string != NULL && netcode_parse_address( address2_string, &address2 ) != NETCODE_OK )
{
netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: failed to parse client address2\n" );
client_create_error = NETCODE_CLIENT_CREATE_ERROR_PARSE_ADDRESS2_FAILED;
return NULL;
}

Expand Down Expand Up @@ -2771,6 +2785,7 @@ struct netcode_client_t * netcode_client_create_dual( NETCODE_CONST char * addre
{
netcode_socket_destroy( &socket_ipv4 );
netcode_socket_destroy( &socket_ipv6 );
client_create_error = NETCODE_CLIENT_CREATE_ERROR_ALLOCATE_CLIENT_FAILED;
return NULL;
}

Expand Down Expand Up @@ -6861,6 +6876,92 @@ void test_init_and_defaults()
}
}

static void * test_failing_allocate_function( void * context, size_t bytes )
{
(void) context;
(void) bytes;
return NULL;
}

void test_client_create_error()
{
struct netcode_client_config_t client_config;
netcode_default_client_config( &client_config );

// successful create leaves the create error as NONE

{
struct netcode_client_t * client = netcode_client_create( "0.0.0.0:50000", &client_config, 0.0 );

check( client );
check( netcode_client_create_error() == NETCODE_CLIENT_CREATE_ERROR_NONE );

netcode_client_destroy( client );
}

// bad first address

check( netcode_client_create( "not an address", &client_config, 0.0 ) == NULL );
check( netcode_client_create_error() == NETCODE_CLIENT_CREATE_ERROR_PARSE_ADDRESS_FAILED );

// bad second address

check( netcode_client_create_dual( "0.0.0.0:50000", "not an address", &client_config, 0.0 ) == NULL );
check( netcode_client_create_error() == NETCODE_CLIENT_CREATE_ERROR_PARSE_ADDRESS2_FAILED );

// the network simulator requires binding to a specific port

{
struct netcode_network_simulator_t * network_simulator = netcode_network_simulator_create( NULL, NULL, NULL );

struct netcode_client_config_t simulator_config;
netcode_default_client_config( &simulator_config );
simulator_config.network_simulator = network_simulator;

check( netcode_client_create( "0.0.0.0", &simulator_config, 0.0 ) == NULL );
check( netcode_client_create_error() == NETCODE_CLIENT_CREATE_ERROR_SIMULATOR_REQUIRES_PORT );

netcode_network_simulator_destroy( network_simulator );
}

// binding a second client to a port already in use fails at socket creation (ipv4)

{
struct netcode_client_t * first_client = netcode_client_create( "127.0.0.1:50000", &client_config, 0.0 );

check( first_client );

check( netcode_client_create( "127.0.0.1:50000", &client_config, 0.0 ) == NULL );
check( netcode_client_create_error() == NETCODE_CLIENT_CREATE_ERROR_CREATE_SOCKET_IPV4_FAILED );

netcode_client_destroy( first_client );
}

// and the same over ipv6 reports the ipv6 error

{
struct netcode_client_t * first_client = netcode_client_create( "[::1]:50000", &client_config, 0.0 );

check( first_client );

check( netcode_client_create( "[::1]:50000", &client_config, 0.0 ) == NULL );
check( netcode_client_create_error() == NETCODE_CLIENT_CREATE_ERROR_CREATE_SOCKET_IPV6_FAILED );

netcode_client_destroy( first_client );
}

// client struct allocation failure

{
struct netcode_client_config_t failing_config;
netcode_default_client_config( &failing_config );
failing_config.allocate_function = test_failing_allocate_function;

check( netcode_client_create( "0.0.0.0:50000", &failing_config, 0.0 ) == NULL );
check( netcode_client_create_error() == NETCODE_CLIENT_CREATE_ERROR_ALLOCATE_CLIENT_FAILED );
}
}

void test_network_simulator_determinism()
{
// the network simulator has its own seeded rng, so two simulators given
Expand Down Expand Up @@ -9222,6 +9323,7 @@ void netcode_test()
RUN_TEST( test_replay_protection );
RUN_TEST( test_runtime_guards );
RUN_TEST( test_init_and_defaults );
RUN_TEST( test_client_create_error );
RUN_TEST( test_network_simulator_determinism );
RUN_TEST( test_client_create );
RUN_TEST( test_server_create );
Expand Down
16 changes: 16 additions & 0 deletions netcode.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@
#define NETCODE_CLIENT_STATE_SENDING_CONNECTION_RESPONSE 2
#define NETCODE_CLIENT_STATE_CONNECTED 3

#define NETCODE_CLIENT_CREATE_ERROR_NONE 0
#define NETCODE_CLIENT_CREATE_ERROR_PARSE_ADDRESS_FAILED 1
#define NETCODE_CLIENT_CREATE_ERROR_PARSE_ADDRESS2_FAILED 2
#define NETCODE_CLIENT_CREATE_ERROR_SIMULATOR_REQUIRES_PORT 3
#define NETCODE_CLIENT_CREATE_ERROR_CREATE_SOCKET_IPV4_FAILED 4
#define NETCODE_CLIENT_CREATE_ERROR_CREATE_SOCKET_IPV6_FAILED 5
#define NETCODE_CLIENT_CREATE_ERROR_ALLOCATE_CLIENT_FAILED 6

#define NETCODE_MAX_CLIENTS 256
#define NETCODE_MAX_PACKET_SIZE 1200

Expand Down Expand Up @@ -167,6 +175,14 @@ struct netcode_client_t * netcode_client_create( NETCODE_CONST char * address, N

struct netcode_client_t * netcode_client_create_dual( NETCODE_CONST char * address1, NETCODE_CONST char * address2, NETCODE_CONST struct netcode_client_config_t * config, double time );

/*
If netcode_client_create or netcode_client_create_dual returns NULL, call this to find out why.
Returns the NETCODE_CLIENT_CREATE_ERROR_* value from the most recent client create call,
or NETCODE_CLIENT_CREATE_ERROR_NONE if that call succeeded.
*/

int netcode_client_create_error();

void netcode_client_destroy( struct netcode_client_t * client );

void netcode_client_connect( struct netcode_client_t * client, uint8_t * connect_token );
Expand Down
Loading