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
136 changes: 136 additions & 0 deletions compat/Compat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,140 @@ private static int DoNonces()
return 0;
}

private static int DoTokenLifetimes()
{
NetcodeLog.Level = LogLevel.None;
byte[] sent = Array.Empty<byte>();
var config = new ServerConfig { ProtocolId = SessionProtocolId, OverrideSendAndReceive = true };
SessionPrivateKey.CopyTo(config.PrivateKey, 0);
config.SendPacketOverride = (in Address to, ReadOnlySpan<byte> data) => sent = data.ToArray();
config.ReceivePacketOverride = (ref Address from, Span<byte> data) => 0;
using var server = new Server("127.0.0.1:40000", config, 0.0);
server.Start(4);
server.SetConnectTokenExpiryFloorForTest(1000);
Address x = Address.Parse("127.0.0.1:50000");

ConnectTokenData Token(int timeout)
{
ConnectTokenData token = ScenarioToken(0x1111, 0xA1, 1);
if (!ConnectTokenPrivate.Decrypt(token.PrivateData, token.VersionInfo, token.ProtocolId,
token.ExpireTimestamp, token.Nonce, SessionPrivateKey)) throw new InvalidOperationException();
var privateToken = new ConnectTokenPrivate();
if (!privateToken.Read(token.PrivateData)) throw new InvalidOperationException();
privateToken.TimeoutSeconds = timeout;
privateToken.Write(token.PrivateData);
if (!ConnectTokenPrivate.Encrypt(token.PrivateData, token.VersionInfo, token.ProtocolId,
token.ExpireTimestamp, token.Nonce, SessionPrivateKey)) throw new InvalidOperationException();
token.TimeoutSeconds = timeout;
return token;
}

void Request(in Address from, ConnectTokenData token, double logicalTime)
{
byte[] wire = new byte[Defines.MaxPacketBytes];
int bytes = PacketIO.WriteConnectionRequestPacket(wire, token.VersionInfo, token.ProtocolId,
token.ExpireTimestamp, token.Nonce, token.PrivateData);
server.ProcessPacketAtTimestamp(in from, wire.AsSpan(0, bytes), 1001, logicalTime);
}

foreach (int timeout in new[] { -1, 0, 1, 5, 30 })
{
ConnectTokenData token = Token(timeout);
Request(in x, token, 0.0);
int offset = -1;
for (int t = 0; t <= 40; t++)
{
server.Update(t);
if (server.NumEncryptionMappings == 0) { offset = t; break; }
}
Console.WriteLine($"timeout {timeout} -> {(offset < 0 ? "never" : offset.ToString())}");
server.Stop();
server.Start(4);
server.SetConnectTokenExpiryFloorForTest(1000);

Request(in x, token, 0.0);
byte[] challenge = (byte[])sent.Clone();
bool[] allowed = new bool[PacketType.NumPackets];
allowed[PacketType.ConnectionChallenge] = true;
ReadPacketResult packet = PacketIO.ReadPacket(challenge, true, token.ServerToClientKey,
SessionProtocolId, 1001, false, ReadOnlySpan<byte>.Empty, allowed, null);
if (packet.Type != PacketType.ConnectionChallenge) return 1;
byte[] responseData = challenge.AsSpan(packet.DataOffset, packet.DataLength).ToArray();
byte[] response = new byte[Defines.MaxPacketBytes];
int responseBytes = PacketIO.WriteEncryptedPacket(response, PacketType.ConnectionResponse,
responseData, 1, token.ClientToServerKey, SessionProtocolId);
server.Update(0.1);
server.ProcessPacketAtTimestamp(in x, response.AsSpan(0, responseBytes), 1001, 0.1);
Console.WriteLine($"response {timeout} -> {(server.NumConnectedClients == 1 ? "connects" : "refused")}");
server.Stop();
server.Start(4);
server.SetConnectTokenExpiryFloorForTest(1000);
}

ulong[] expiries = { ulong.MaxValue, 1060, 1031, 1030, 1030, 1001, 1000 };
ulong[] floors = { 1000, 1000, 1000, 1000, 1030, 1000, 1000 };
string[] expiryNames = { "max", "plus60", "plus31", "plus30", "floor", "plus1", "expired" };
ConnectTokenData expiryToken = Token(15);
for (int i = 0; i < expiries.Length; i++)
{
byte[] privateData = (byte[])expiryToken.PrivateData.Clone();
if (!ConnectTokenPrivate.Decrypt(privateData, expiryToken.VersionInfo, expiryToken.ProtocolId,
expiryToken.ExpireTimestamp, expiryToken.Nonce, SessionPrivateKey)) return 1;
if (!ConnectTokenPrivate.Encrypt(privateData, expiryToken.VersionInfo, expiryToken.ProtocolId,
expiries[i], expiryToken.Nonce, SessionPrivateKey)) return 1;
byte[] wire = new byte[Defines.MaxPacketBytes];
int bytes = PacketIO.WriteConnectionRequestPacket(wire, expiryToken.VersionInfo,
expiryToken.ProtocolId, expiries[i], expiryToken.Nonce, privateData);
bool[] allowed = new bool[PacketType.NumPackets];
allowed[PacketType.ConnectionRequest] = true;
ReadPacketResult packet = PacketIO.ReadPacket(wire.AsSpan(0, bytes), true,
ReadOnlySpan<byte>.Empty, SessionProtocolId, 1000, true,
SessionPrivateKey, allowed, null, floors[i], 30);
Console.WriteLine($"expiry {expiryNames[i]} -> {(packet.Type == PacketType.ConnectionRequest ? "accepted" : "refused")}");
}

return 0;
}

private static int DoClientAddress()
{
NetcodeLog.Level = LogLevel.None;
var config = new ClientConfig
{
OverrideSendAndReceive = true,
SendPacketOverride = (in Address to, ReadOnlySpan<byte> data) => { },
ReceivePacketOverride = (ref Address from, Span<byte> data) => 0,
};
using var client = new Client("0.0.0.0:50000", config, 0.0);
client.SocketAvailabilityOverride = type => Client.CanReach(type, true, false, false);

(string name, string[] addresses)[] cases =
{
("v6", new[] { "[::1]:40000" }),
("v6v4", new[] { "[::1]:40000", "127.0.0.1:40000" }),
("v4", new[] { "127.0.0.1:40000" }),
("v4v6", new[] { "127.0.0.1:40000", "[::1]:40000" }),
};
for (int i = 0; i < cases.Length; i++)
{
(string name, string[] addresses) = cases[i];
ConnectTokenData connectToken = ScenarioToken(0x1111, 0xA1, i + 1);
connectToken.NumServerAddresses = addresses.Length;
for (int j = 0; j < addresses.Length; j++)
connectToken.ServerAddresses[j] = Address.Parse(addresses[j]);
byte[] token = new byte[Protocol.ConnectTokenBytes];
connectToken.Write(token);
client.Connect(token);
if (name == "v4v6" && !client.ConnectToNextServer()) return 1;
int index = client.State == ClientState.SendingConnectionRequest
? client.ServerAddressIndex
: -1;
Console.WriteLine($"{name} state {Client.StateName(client.State)} address {(index < 0 ? "none" : index.ToString())}");
}

return 0;
}

private static int Main(string[] args)
{
NetcodeLog.Level = LogLevel.Error;
Expand All @@ -888,6 +1022,8 @@ private static int Main(string[] args)
["drain"] => DoDrain(),
["nonces"] => DoNonces(),
["challenge-mapping", var inputDir] => DoChallengeMapping(inputDir),
["token-lifetimes"] => DoTokenLifetimes(),
["client-address"] => DoClientAddress(),
_ => Usage(),
};
}
Expand Down
146 changes: 145 additions & 1 deletion compat/c/compat.c
Original file line number Diff line number Diff line change
Expand Up @@ -1052,7 +1052,7 @@ static void scenario_process( struct netcode_server_t * server, struct netcode_a
{
uint8_t wire[NETCODE_MAX_PACKET_BYTES];
int bytes = netcode_write_packet( packet, wire, sizeof( wire ), sequence, key, SESSION_PROTOCOL_ID );
if ( !write_file( input_dir, name, wire, bytes ) ) abort();
if ( input_dir && !write_file( input_dir, name, wire, bytes ) ) abort();
uint8_t allowed[NETCODE_CONNECTION_NUM_PACKETS] = { 0 };
allowed[NETCODE_CONNECTION_REQUEST_PACKET] = 1;
allowed[NETCODE_CONNECTION_RESPONSE_PACKET] = 1;
Expand Down Expand Up @@ -1289,6 +1289,146 @@ static int do_nonces( void )
return 1;
}

static int do_token_lifetimes( void )
{
netcode_log_level( NETCODE_LOG_LEVEL_NONE );
struct netcode_server_config_t config;
netcode_default_server_config( &config );
config.protocol_id = SESSION_PROTOCOL_ID;
config.override_send_and_receive = 1;
config.send_packet_override = scenario_send;
config.receive_packet_override = scenario_receive;
memcpy( config.private_key, session_private_key, NETCODE_KEY_BYTES );
struct netcode_server_t * server = netcode_server_create( "127.0.0.1:40000", &config, 0.0 );
if ( !server ) return 0;
netcode_server_start( server, 4 );
server->min_connect_token_expire_timestamp = 1000;
struct netcode_address_t x;
netcode_parse_address( "127.0.0.1:50000", &x );

int timeouts[] = { -1, 0, 1, 5, 30 };
for ( int i = 0; i < 5; i++ )
{
struct netcode_connect_token_t token;
scenario_token( &token, 0x1111, 0xa1, 1 );
struct netcode_connect_token_private_t private_token;
if ( netcode_decrypt_connect_token_private( token.private_data, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES,
token.version_info, token.protocol_id, token.expire_timestamp, token.nonce, session_private_key ) != NETCODE_OK ) return 0;
if ( netcode_read_connect_token_private( token.private_data, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES, &private_token ) != NETCODE_OK ) return 0;
private_token.timeout_seconds = timeouts[i];
netcode_write_connect_token_private( &private_token, token.private_data, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES );
if ( netcode_encrypt_connect_token_private( token.private_data, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES,
token.version_info, token.protocol_id, token.expire_timestamp, token.nonce, session_private_key ) != NETCODE_OK ) return 0;
token.timeout_seconds = timeouts[i];
scenario_request( server, &x, &token, 0.0, NULL, NULL );
int offset = -1;
for ( int t = 0; t <= 40; t++ )
{
netcode_server_update( server, (double) t );
int active = 0;
for ( int j = 0; j < server->encryption_manager.num_encryption_mappings; j++ )
if ( server->encryption_manager.address[j].type != NETCODE_ADDRESS_NONE &&
!netcode_encryption_manager_entry_expired( &server->encryption_manager, j, (double) t ) )
active++;
if ( active == 0 ) { offset = t; break; }
}
if ( offset < 0 ) printf( "timeout %d -> never\n", timeouts[i] );
else printf( "timeout %d -> %d\n", timeouts[i], offset );
netcode_server_stop( server );
netcode_server_start( server, 4 );
server->min_connect_token_expire_timestamp = 1000;

scenario_request( server, &x, &token, 0.0, NULL, NULL );
struct netcode_connection_challenge_packet_t challenge = scenario_challenge( &token );
netcode_server_update( server, 0.1 );
scenario_response( server, &x, &token, &challenge, 1, 0.1, NULL, NULL );
printf( "response %d -> %s\n", timeouts[i], server->num_connected_clients == 1 ? "connects" : "refused" );
netcode_server_stop( server );
netcode_server_start( server, 4 );
server->min_connect_token_expire_timestamp = 1000;
}

const uint64_t expiries[] = { UINT64_MAX, 1060, 1031, 1030, 1030, 1001, 1000 };
const uint64_t floors[] = { 1000, 1000, 1000, 1000, 1030, 1000, 1000 };
const char * expiry_names[] = { "max", "plus60", "plus31", "plus30", "floor", "plus1", "expired" };
for ( int i = 0; i < 7; i++ )
{
struct netcode_connect_token_t token;
scenario_token( &token, 0x1111, 0xa1, 1 );
if ( netcode_decrypt_connect_token_private( token.private_data, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES,
token.version_info, token.protocol_id, token.expire_timestamp, token.nonce, session_private_key ) != NETCODE_OK ) return 0;
token.expire_timestamp = expiries[i];
if ( netcode_encrypt_connect_token_private( token.private_data, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES,
token.version_info, token.protocol_id, token.expire_timestamp, token.nonce, session_private_key ) != NETCODE_OK ) return 0;
struct netcode_connection_request_packet_t request;
memset( &request, 0, sizeof( request ) );
request.packet_type = NETCODE_CONNECTION_REQUEST_PACKET;
memcpy( request.version_info, token.version_info, NETCODE_VERSION_INFO_BYTES );
request.protocol_id = token.protocol_id;
request.connect_token_expire_timestamp = token.expire_timestamp;
memcpy( request.connect_token_nonce, token.nonce, NETCODE_CONNECT_TOKEN_NONCE_BYTES );
memcpy( request.connect_token_data, token.private_data, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES );
uint8_t wire[NETCODE_MAX_PACKET_BYTES];
int bytes = netcode_write_packet( &request, wire, sizeof( wire ), 0, token.client_to_server_key, SESSION_PROTOCOL_ID );
uint64_t sequence = 0;
uint8_t allowed[NETCODE_CONNECTION_NUM_PACKETS] = { 0 };
allowed[NETCODE_CONNECTION_REQUEST_PACKET] = 1;
void * packet = netcode_read_packet( wire, bytes, &sequence, NULL, SESSION_PROTOCOL_ID,
1000, floors[i], 30, session_private_key, allowed, NULL, NULL, NULL );
printf( "expiry %s -> %s\n", expiry_names[i], packet ? "accepted" : "refused" );
free( packet );
}
netcode_server_destroy( server );
return 1;
}

static int do_client_address( void )
{
netcode_log_level( NETCODE_LOG_LEVEL_NONE );
struct netcode_client_config_t config;
netcode_default_client_config( &config );
config.override_send_and_receive = 1;
config.send_packet_override = scenario_send;
config.receive_packet_override = scenario_receive;
struct netcode_client_t * client = netcode_client_create( "0.0.0.0:50000", &config, 0.0 );
if ( !client ) return 0;
// The nonzero handle is only an in-memory family marker. Override transport
// remains on outside the two direct address-selection calls.
client->socket_holder.ipv4.handle = 1;

const char * lists[4][2] = { { "[::1]:40000", NULL }, { "[::1]:40000", "127.0.0.1:40000" },
{ "127.0.0.1:40000", NULL }, { "127.0.0.1:40000", "[::1]:40000" } };
const char * names[4] = { "v6", "v6v4", "v4", "v4v6" };
for ( int i = 0; i < 4; i++ )
{
uint8_t connect_token[NETCODE_CONNECT_TOKEN_BYTES];
struct netcode_connect_token_t token;
scenario_token( &token, 0x1111, 0xa1, (uint8_t) ( i + 1 ) );
int num = lists[i][1] ? 2 : 1;
token.num_server_addresses = num;
for ( int j = 0; j < num; j++ )
if ( netcode_parse_address( lists[i][j], &token.server_addresses[j] ) != NETCODE_OK ) return 0;
netcode_write_connect_token( &token, connect_token, sizeof( connect_token ) );
netcode_client_disconnect( client );
client->config.override_send_and_receive = 0;
netcode_client_connect( client, connect_token );
client->config.override_send_and_receive = 1;
if ( i == 3 )
{
client->config.override_send_and_receive = 0;
if ( !netcode_client_connect_to_next_server( client ) ) return 0;
client->config.override_send_and_receive = 1;
}
int state = netcode_client_state( client );
printf( "%s state %s address %s\n", names[i], netcode_client_state_name( state ),
state == NETCODE_CLIENT_STATE_SENDING_CONNECTION_REQUEST ?
( client->server_address_index == 1 ? "1" : "0" ) : "none" );
}
client->socket_holder.ipv4.handle = 0;
netcode_client_destroy( client );
return 1;
}

int main( int argc, char ** argv )
{
if ( netcode_init() != NETCODE_OK )
Expand Down Expand Up @@ -1319,6 +1459,10 @@ int main( int argc, char ** argv )
ok = do_nonces();
else if ( argc == 3 && strcmp( argv[1], "challenge-mapping" ) == 0 )
ok = do_challenge_mapping( argv[2] );
else if ( argc == 2 && strcmp( argv[1], "token-lifetimes" ) == 0 )
ok = do_token_lifetimes();
else if ( argc == 2 && strcmp( argv[1], "client-address" ) == 0 )
ok = do_client_address();
else
printf( "usage: compat-c goldens <dir> | verify <dir> | mint <file> <address> | server <port> | client <port> <token> | diffpackets | drain | nonces\n" );

Expand Down
26 changes: 25 additions & 1 deletion scripts/interop.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# scripts/interop.sh [path-to-netcode-clone]
#
# Builds the C half of the compat harness against the REAL netcode.c (plus its
# vendored libsodium), then runs four legs:
# vendored libsodium), then runs ten legs:
#
# 1. golden byte identity — both sides write the same token/packet goldens from
# the same fixed keys/nonces/timestamps; every file must be byte identical.
Expand Down Expand Up @@ -40,6 +40,12 @@ if ! git -C "$NETCODE" merge-base --is-ancestor 1c58478d HEAD; then
exit 1
fi

if ! git -C "$NETCODE" merge-base --is-ancestor 067b1959 HEAD ||
! git -C "$NETCODE" merge-base --is-ancestor 39f04965 HEAD; then
echo "INTEROP FAILED: C reference must contain both token-lifetime and socket-family fixes."
exit 1
fi

WORK="$(mktemp -d)"
SERVER_PID=""

Expand Down Expand Up @@ -135,4 +141,22 @@ if ! diff -u "$WORK/c-nonces.txt" "$WORK/cs-nonces.txt"; then
fi
echo " global nonce differential matched"

echo "== leg 9: token-lifetimes differential (preconnect mapping lifetime)"
$CS token-lifetimes > "$WORK/cs-lifetimes.txt"
"$WORK/compat-c" token-lifetimes > "$WORK/c-lifetimes.txt"
if ! diff -u "$WORK/c-lifetimes.txt" "$WORK/cs-lifetimes.txt"; then
echo " token-lifetimes differential differed; first differing lines above"
exit 1
fi
echo " token-lifetimes differential matched"

echo "== leg 10: client-address differential (socket-family refusal)"
$CS client-address > "$WORK/cs-client-address.txt"
"$WORK/compat-c" client-address > "$WORK/c-client-address.txt"
if ! diff -u "$WORK/c-client-address.txt" "$WORK/cs-client-address.txt"; then
echo " client-address differential differed; first differing lines above"
exit 1
fi
echo " client-address differential matched"

echo "INTEROP GATE PASSED"
Loading
Loading