@@ -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
606717print ( "Signaling server is ready, starting test clients" )
607718
608- # Run the tests
719+ # Run the positive tests
609720for 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.
618739really_failed = g_failed
619740
0 commit comments