Skip to content

Commit 94ee2a3

Browse files
committed
test_p2p: Test connect failures and verify retry counts
1 parent 6c0c4e4 commit 94ee2a3

2 files changed

Lines changed: 171 additions & 11 deletions

File tree

tests/test_p2p.cpp

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ HSteamListenSocket g_hListenSock;
2020
HSteamNetConnection g_hConnection;
2121
int g_nRepeat = 1;
2222
int g_nConnectionsDone = 0;
23+
bool g_bExpectFailure = false; // if true, connection failure is the expected outcome
24+
int g_nConnectionTimeoutMS = -1; // -1 = library default
2325
enum ETestRole
2426
{
2527
k_ETestRole_Undefined,
@@ -51,6 +53,8 @@ void PrintUsage()
5153
" --turn-server <host:port> TURN relay server address\n"
5254
" --ice-implementation <n> ICE implementation: 0=default, 1=native\n"
5355
" --repeat <n> Repeat the connection test N times (default: 1)\n"
56+
" --expect-failure Treat connection failure as success, success as failure\n"
57+
" --timeout-ms <n> Override initial connection timeout in milliseconds\n"
5458
#ifdef STEAMNETWORKINGSOCKETS_ENABLE_MOCK
5559
"\n"
5660
"Mock network options:\n"
@@ -157,12 +161,30 @@ void OnSteamNetConnectionStatusChanged( SteamNetConnectionStatusChangedCallback_
157161
{
158162
g_hConnection = k_HSteamNetConnection_Invalid;
159163

160-
bool bError = ( pInfo->m_info.m_eState == k_ESteamNetworkingConnectionState_ProblemDetectedLocally )
161-
|| ( pInfo->m_info.m_eEndReason != k_ESteamNetConnectionEnd_App_Generic );
162-
if ( bError )
163-
Quit( 1 );
164+
if ( g_bExpectFailure )
165+
{
166+
// Connection failure is the expected outcome.
167+
// ProblemDetectedLocally (or ClosedByPeer with non-app reason) = expected.
168+
// ClosedByPeer with App_Generic = the peer completed successfully, which is wrong.
169+
bool bUnexpectedSuccess = ( pInfo->m_info.m_eState == k_ESteamNetworkingConnectionState_ClosedByPeer )
170+
&& ( pInfo->m_info.m_eEndReason == k_ESteamNetConnectionEnd_App_Generic );
171+
if ( bUnexpectedSuccess )
172+
{
173+
TEST_Printf( "ERROR: connection closed cleanly, but failure was expected\n" );
174+
Quit( 1 );
175+
}
176+
TEST_Printf( "Connection failed as expected\n" );
177+
SteamNetworkingSocketsLib::TEST_ICE_ctr_Print();
178+
}
179+
else
180+
{
181+
bool bError = ( pInfo->m_info.m_eState == k_ESteamNetworkingConnectionState_ProblemDetectedLocally )
182+
|| ( pInfo->m_info.m_eEndReason != k_ESteamNetConnectionEnd_App_Generic );
183+
if ( bError )
184+
Quit( 1 );
185+
}
164186

165-
// Clean close — the main loop will start the next iteration or exit.
187+
// Clean close (or expected failure) — main loop starts next iteration or exits.
166188
++g_nConnectionsDone;
167189
}
168190
else
@@ -271,6 +293,10 @@ int main( int argc, const char **argv )
271293
g_nICEImplementation = atoi( GetArg() );
272294
else if ( !strcmp( pszSwitch, "--repeat" ) )
273295
g_nRepeat = atoi( GetArg() );
296+
else if ( !strcmp( pszSwitch, "--expect-failure" ) )
297+
g_bExpectFailure = true;
298+
else if ( !strcmp( pszSwitch, "--timeout-ms" ) )
299+
g_nConnectionTimeoutMS = atoi( GetArg() );
274300
else if ( !strcmp( pszSwitch, "--client" ) )
275301
g_eTestRole = k_ETestRole_Client;
276302
else if ( !strcmp( pszSwitch, "--server" ) )
@@ -387,6 +413,9 @@ int main( int argc, const char **argv )
387413
// Initialize library, with the desired local identity
388414
TEST_Init( &identityLocal );
389415

416+
if ( g_nConnectionTimeoutMS > 0 )
417+
SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_TimeoutInitial, g_nConnectionTimeoutMS );
418+
390419
SteamNetworkingUtils()->SetGlobalConfigValueString( k_ESteamNetworkingConfig_P2P_STUN_ServerList, pszSTUNServer );
391420
if ( pszTURNServer != nullptr )
392421
SteamNetworkingUtils()->SetGlobalConfigValueString( k_ESteamNetworkingConfig_P2P_TURN_ServerList, pszTURNServer );
@@ -523,6 +552,12 @@ int main( int argc, const char **argv )
523552
// In this example code we will assume all messages are '\0'-terminated strings.
524553
// Obviously, this is not secure.
525554
TEST_Printf( "Received message '%s'\n", pMessage->GetData() );
555+
if ( g_bExpectFailure )
556+
{
557+
TEST_Printf( "ERROR: received a message but connection failure was expected\n" );
558+
pMessage->Release();
559+
Quit( 1 );
560+
}
526561

527562
// Free message struct and buffer.
528563
pMessage->Release();
@@ -560,8 +595,12 @@ int main( int argc, const char **argv )
560595
}
561596
}
562597

563-
// Server exits once it has handled the expected number of connections.
564-
if ( g_eTestRole == k_ETestRole_Server && g_nConnectionsDone >= g_nRepeat )
598+
// Exit once all expected connections are done.
599+
// For the server, this means N accepted+closed connections.
600+
// For the client, normal exits happen inside the message handler, but
601+
// when --expect-failure is set the failure is detected in the state callback,
602+
// so we need this check here too.
603+
if ( g_nConnectionsDone >= g_nRepeat && g_hConnection == k_HSteamNetConnection_Invalid )
565604
break;
566605
}
567606

tests/test_p2p.py

Lines changed: 125 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,10 @@ def StartProcessInThread( tag, cmdline, env=None, ready_message=None, ready_even
157157
thread.start()
158158
return thread
159159

160-
def StartClientInThread( role, local, remote, extra_args=[] ):
160+
_DEFAULT_STUN = object() # sentinel: use the shared STUN server
161+
_DEFAULT_TURN = object() # sentinel: use the shared TURN server
162+
163+
def StartClientInThread( role, local, remote, extra_args=[], stun=_DEFAULT_STUN, turn=_DEFAULT_TURN ):
161164
cmdline = [
162165
"./test_p2p",
163166
"--" + role,
@@ -167,8 +170,18 @@ def StartClientInThread( role, local, remote, extra_args=[] ):
167170
"--log", local + ".verbose.log"
168171
]
169172

170-
cmdline += [ '--stun-server', "%s:%d,[%s]:%d" % (g_stun_ip, g_stun_port, g_stun_ipv6, g_stun_port) ]
171-
cmdline += [ '--turn-server', "%s:%d" % (g_stun_ip, g_stun_port) ]
173+
if stun is _DEFAULT_STUN:
174+
cmdline += [ '--stun-server', "%s:%d,[%s]:%d" % (g_stun_ip, g_stun_port, g_stun_ipv6, g_stun_port) ]
175+
elif stun is not None:
176+
cmdline += [ '--stun-server', stun ]
177+
# stun=None: omit --stun-server entirely (executable uses its built-in default)
178+
179+
if turn is _DEFAULT_TURN:
180+
cmdline += [ '--turn-server', "%s:%d" % (g_stun_ip, g_stun_port) ]
181+
elif turn is not None:
182+
cmdline += [ '--turn-server', turn ]
183+
# turn=None: omit --turn-server entirely (no relay)
184+
172185
if g_repeat > 1:
173186
cmdline += [ '--repeat', str(g_repeat) ]
174187
cmdline += extra_args
@@ -337,6 +350,104 @@ def _parse_candidate_log( filename ):
337350
return local, remote
338351

339352

353+
# Address used for "server is down" tests: valid loopback IP but no STUN/TURN listening.
354+
# Packets are sent successfully but never answered, so the connection timeout drives failure.
355+
_DEAD_SERVER = '%s:9999' % g_stun_ip
356+
357+
358+
def ClientServerExpectedFailureTest( server_extra_args=[], client_extra_args=[], ice_impl=1,
359+
stun=_DEFAULT_STUN, turn=_DEFAULT_TURN,
360+
expected_counters=None, expected_candidates=None ):
361+
"""Run a test where both sides are expected to fail to connect."""
362+
global g_failed
363+
impl_args = [ '--ice-implementation', str(ice_impl) ]
364+
fail_args = [ '--expect-failure' ]
365+
server = StartClientInThread( "server", "peer_server", "peer_client",
366+
server_extra_args + impl_args + fail_args,
367+
stun=stun, turn=turn )
368+
client = StartClientInThread( "client", "peer_client", "peer_server",
369+
client_extra_args + impl_args + fail_args,
370+
stun=stun, turn=turn )
371+
372+
server.join( timeout=30 )
373+
client.join( timeout=30 )
374+
375+
if expected_counters is not None and g_repeat == 1:
376+
for peer, thread in [ ( 'server', server ), ( 'client', client ) ]:
377+
for name, (lo, hi) in expected_counters.items():
378+
val = thread.counters.get( name, 0 )
379+
if lo is not None and val < lo:
380+
print( "ERROR: %s TEST_ICE_ctr_%s=%d, expected >= %d" % ( peer, name, val, lo ) )
381+
g_failed = True
382+
if hi is not None and val > hi:
383+
print( "ERROR: %s TEST_ICE_ctr_%s=%d, expected <= %d" % ( peer, name, val, hi ) )
384+
g_failed = True
385+
386+
if g_repeat == 1:
387+
srv_local, srv_remote = _parse_candidate_log( "peer_server.verbose.log" )
388+
cli_local, cli_remote = _parse_candidate_log( "peer_client.verbose.log" )
389+
if expected_candidates is not None:
390+
exp_srv, exp_cli = expected_candidates
391+
if exp_srv is not None and srv_local != exp_srv:
392+
print( "ERROR: server gathered %s, expected %s" % ( srv_local, exp_srv ) )
393+
g_failed = True
394+
if exp_cli is not None and cli_local != exp_cli:
395+
print( "ERROR: client gathered %s, expected %s" % ( cli_local, exp_cli ) )
396+
g_failed = True
397+
398+
399+
# Failure test cases: ( description, server_args, client_args, kwargs_for_failure_test )
400+
# 'stun' and 'turn' kwargs override the server address; None = omit entirely.
401+
# _CAND_NAT_NO_TURN = behind NAT + STUN works, but no relay (TURN not configured or failed)
402+
_CAND_NAT_NO_TURN = {'host': 1, 'srflx': 1}
403+
404+
FAILURE_TEST_CASES = [
405+
# STUN is unavailable: no srflx gathered, no relay, host candidates can't cross NAT subnets.
406+
# The STUN binding request retransmits 4 times (5 total sends) before giving up at ~5.3s,
407+
# then the connection timeout fires at 10s.
408+
( 'STUN unavailable (full-cone NAT, no TURN)',
409+
_nat( _SRV_INT, _SRV_GW, 'full-cone' ),
410+
_nat( _CLI_INT, _CLI_GW, 'full-cone' ),
411+
dict( stun=_DEAD_SERVER, turn=None,
412+
expected_counters={
413+
'allocate_send': (0, 0),
414+
'data_ind_recv': (0, 0),
415+
'binding_req_retx': (4, 4), # 5 sends total: 1 initial + 4 retransmits
416+
'allocate_retx': (0, 0),
417+
},
418+
expected_candidates=( {'host': 1}, {'host': 1} ) ) ),
419+
420+
# TURN not configured: symmetric NAT requires relay; without it the connection must fail.
421+
# Connectivity checks to srflx candidates retransmit 4 times before giving up.
422+
( 'TURN not configured (symmetric NAT)',
423+
_nat( _SRV_INT, _SRV_GW, 'symmetric' ),
424+
_nat( _CLI_INT, _CLI_GW, 'symmetric' ),
425+
dict( turn=None,
426+
expected_counters={
427+
'allocate_send': (0, 0),
428+
'data_ind_recv': (0, 0),
429+
'binding_req_retx': (4, 4),
430+
'allocate_retx': (0, 0),
431+
},
432+
expected_candidates=( _CAND_NAT_NO_TURN, _CAND_NAT_NO_TURN ) ) ),
433+
434+
# TURN server unreachable: allocation requests are sent but never answered.
435+
# STUN works so srflx is gathered, but symmetric NAT blocks direct paths and relay fails.
436+
# Both the srflx connectivity checks and the TURN allocation each retransmit 4 times.
437+
( 'TURN unreachable (symmetric NAT)',
438+
_nat( _SRV_INT, _SRV_GW, 'symmetric' ),
439+
_nat( _CLI_INT, _CLI_GW, 'symmetric' ),
440+
dict( turn=_DEAD_SERVER,
441+
expected_counters={
442+
'allocate_send': (1, None),
443+
'data_ind_recv': (0, 0),
444+
'binding_req_retx': (4, 4),
445+
'allocate_retx': (4, 4),
446+
},
447+
expected_candidates=( _CAND_NAT_NO_TURN, _CAND_NAT_NO_TURN ) ) ),
448+
]
449+
450+
340451
# Counter constraint dicts: map short counter name -> (min, max), None = no bound.
341452
# Applied to both server and client after each test.
342453
# Only the relay-path counters are pinned; binding_req counts vary with retransmit timing.
@@ -605,7 +716,7 @@ def _check_subset( received, gathered, receiver, gatherer ):
605716

606717
print( "Signaling server is ready, starting test clients" )
607718

608-
# Run the tests
719+
# Run the positive tests
609720
for desc, srv_args, cli_args, exp_route, ice_impl, exp_counters, exp_candidates in CLIENT_SERVER_TEST_CASES:
610721
print( "=================================================================" )
611722
print( "Test: " + desc )
@@ -614,6 +725,16 @@ def _check_subset( received, gathered, receiver, gatherer ):
614725
if g_failed:
615726
break
616727

728+
# Run the expected-failure tests
729+
if not g_failed:
730+
for desc, srv_args, cli_args, kwargs in FAILURE_TEST_CASES:
731+
print( "=================================================================" )
732+
print( "Test (expected failure): " + desc )
733+
print( "=================================================================" )
734+
ClientServerExpectedFailureTest( srv_args, cli_args, **kwargs )
735+
if g_failed:
736+
break
737+
617738
# Ignore any "failure" detected in server shutdowns.
618739
really_failed = g_failed
619740

0 commit comments

Comments
 (0)