From bf586d5d1168a0009c97769203c7fd8bc2220f7f Mon Sep 17 00:00:00 2001 From: Ibn-Butt-Toota <283850630+Ibn-Butt-Toota@users.noreply.github.com> Date: Thu, 29 Oct 2026 22:42:26 -0400 Subject: [PATCH 1/3] TURN: failover should skip wrong addr family --- .../steamnetworkingsockets_ice_client.cpp | 28 +++++++++++++++++-- tests/test_p2p.py | 13 +++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_ice_client.cpp b/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_ice_client.cpp index e5a48147..360c4169 100644 --- a/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_ice_client.cpp +++ b/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_ice_client.cpp @@ -67,6 +67,30 @@ static bool IsRemoteAddressOnLocalSubnet( const netadr_t &localAddr, int nPrefix return false; } +// Interface sockets are bound to a single address family. A server of a +// different family can't be reached from them. Returns the index of the first +// server after idxCurrent with the same family as localAddr, or -1. +// bWrapAround allows for keepalive rotation as it wraps around to the +// start of the list (ex: 0, 1, 2, 3, with idxCurrent as 2 will check 3, 0, 1, then 2). +// Otherwise it stops at the end (ex: 0, 1, 2, 3, with idxCurrent as 3, will return -1). +static int FindNextServerForInterface( const std_vector &vecServers, int idxCurrent, const netadr_t &localAddr, bool bWrapAround ) +{ + if ( idxCurrent < 0 ) + return -1; + + const int nServers = len( vecServers ); + const int nToCheck = bWrapAround ? nServers : nServers - idxCurrent - 1; + + for ( int i = 0; i < nToCheck; ++i ) + { + const int idx = ( idxCurrent + 1 + i ) % nServers; + if ( vecServers[idx].GetType() == localAddr.GetType() ) + return idx; + } + + return -1; +} + /* After the STUN header are zero or more attributes. Each attribute @@ -2090,8 +2114,8 @@ void CSteamNetworkingICESession::STUNRequestCallback_AllocateRelay( const RecvST // Timed out -- try the next TURN server if available. const int nTURNServerIdx = index_of( m_vecTURNServers, info.m_pRequest->m_remoteAddr ); - const int nNextTURNServerIdx = nTURNServerIdx + 1; - if ( nTURNServerIdx < 0 || nNextTURNServerIdx >= len( m_vecTURNServers ) ) + const int nNextTURNServerIdx = FindNextServerForInterface( m_vecTURNServers, nTURNServerIdx, pIntf->m_boundAddr, false ); + if ( nNextTURNServerIdx < 0 ) { // Exhausted all TURN servers. Mark failed. pIntf->m_addrTURNServer = info.m_pRequest->m_remoteAddr; diff --git a/tests/test_p2p.py b/tests/test_p2p.py index 358b8094..f4f4e3b9 100755 --- a/tests/test_p2p.py +++ b/tests/test_p2p.py @@ -498,6 +498,19 @@ def ClientServerExpectedFailureTest( server_extra_args=[], client_extra_args=[], }, expected_candidates=( _CAND_NAT_NO_TURN, _CAND_NAT_NO_TURN ) ) ), + # The dead TURN server is followed by an IPv6 one, but the adapters are IPv4 only. + # Failover must skip servers of the other address family, so there is one allocate + # attempt and then relay discovery gives up. + ( 'TURN unreachable, other server is a different address family (symmetric NAT)', + _nat( _SRV_INT, _SRV_GW, 'symmetric' ), + _nat( _CLI_INT, _CLI_GW, 'symmetric' ), + dict( turn='%s,[%s]:%d' % ( _DEAD_SERVER, g_stun_ipv6, g_stun_port ), + expected_counters={ + 'allocate_send': (1, 1), + 'allocate_retx': (4, 4), + }, + expected_candidates=( _CAND_NAT_NO_TURN, _CAND_NAT_NO_TURN ) ) ), + # TURN wrong password: the server sends a 401 challenge; the client retries with a # bad HMAC (wrong password) and gets a second 401, marking relay as failed. # allocate_send=2: initial (no auth) + one retry (wrong credentials). From 406de32812c1ab3cf46b8c2d4bc0422ea44ce4cb Mon Sep 17 00:00:00 2001 From: Ibn-Butt-Toota <283850630+Ibn-Butt-Toota@users.noreply.github.com> Date: Thu, 29 Oct 2026 22:47:14 -0400 Subject: [PATCH 2/3] STUN: failover should skip wrong addr family --- .../clientlib/steamnetworkingsockets_ice_client.cpp | 5 +++-- tests/test_p2p.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_ice_client.cpp b/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_ice_client.cpp index 360c4169..a8f9ac4f 100644 --- a/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_ice_client.cpp +++ b/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_ice_client.cpp @@ -2327,8 +2327,8 @@ void CSteamNetworkingICESession::STUNRequestCallback_ServerReflexiveCandidate( c // Timed out to this STUN server -- try the next one if available. const int nSTUNServerIdx = index_of( m_vecSTUNServers, info.m_pRequest->m_remoteAddr ); - const int nNextSTUNServerIdx = nSTUNServerIdx + 1; - if ( nSTUNServerIdx < 0 || nNextSTUNServerIdx >= len( m_vecSTUNServers ) ) + const int nNextSTUNServerIdx = FindNextServerForInterface( m_vecSTUNServers, nSTUNServerIdx, pIntf->m_boundAddr, false ); + if ( nNextSTUNServerIdx < 0 ) { // Exhausted all STUN servers. Mark failed so Think_DiscoverServerReflexiveCandidates // does not retry this interface indefinitely. @@ -2338,6 +2338,7 @@ void CSteamNetworkingICESession::STUNRequestCallback_ServerReflexiveCandidate( c } // Try the next server + ++TEST_ICE_ctr_srflx_send; pIntf->QueueBindRequest( m_vecSTUNServers[nNextSTUNServerIdx], &CSteamNetworkingICESession::STUNRequestCallback_ServerReflexiveCandidate, m_nEncoding ); } diff --git a/tests/test_p2p.py b/tests/test_p2p.py index f4f4e3b9..aa66397f 100755 --- a/tests/test_p2p.py +++ b/tests/test_p2p.py @@ -457,6 +457,19 @@ def ClientServerExpectedFailureTest( server_extra_args=[], client_extra_args=[], expected_counters={ 'binding_req_retx': (4, 4) }, expected_candidates=( {'host': 1}, {'host': 1} ) ) ), + # The dead STUN server is followed by an IPv6 one, but the adapters are IPv4 only. + # Failover must skip servers of the other address family, so there is one srflx + # attempt and then discovery gives up. + ( 'STUN unavailable, other server is a different address family', + _nat( _SRV_INT, _SRV_GW, 'full-cone' ), + _nat( _CLI_INT, _CLI_GW, 'full-cone' ), + dict( stun='%s,[%s]:%d' % ( _DEAD_SERVER, g_stun_ipv6, g_stun_port ), turn=None, + expected_counters={ + 'srflx_send': (1, 1), + 'binding_req_retx': (4, 4), + }, + expected_candidates=( {'host': 1}, {'host': 1} ) ) ), + # TURN not configured: symmetric NAT requires relay; without it the connection must fail. # Connectivity checks to srflx candidates retransmit 4 times before giving up. ( 'TURN not configured (symmetric NAT)', From 8e6c0a999764379af51cb148ee86dfe52c773310 Mon Sep 17 00:00:00 2001 From: Ibn-Butt-Toota <283850630+Ibn-Butt-Toota@users.noreply.github.com> Date: Thu, 29 Oct 2026 22:49:43 -0400 Subject: [PATCH 3/3] STUN: keepalive rotation should skip wrong addr family --- .../clientlib/steamnetworkingsockets_ice_client.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_ice_client.cpp b/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_ice_client.cpp index a8f9ac4f..ecfb5996 100644 --- a/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_ice_client.cpp +++ b/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_ice_client.cpp @@ -2363,7 +2363,9 @@ void CSteamNetworkingICESession::STUNRequestCallback_ServerReflexiveKeepAlive( c return; const int nSTUNServerIdx = std::max( 0, index_of( m_vecSTUNServers, info.m_pRequest->m_remoteAddr ) ); - const int nNextSTUNServerIdx = ( nSTUNServerIdx + 1 ) % len( m_vecSTUNServers ); + const int nNextSTUNServerIdx = FindNextServerForInterface( m_vecSTUNServers, nSTUNServerIdx, pIntf->m_boundAddr, true ); + if ( nNextSTUNServerIdx < 0 ) + return; pIntf->QueueBindRequest( m_vecSTUNServers[ nNextSTUNServerIdx ], &CSteamNetworkingICESession::STUNRequestCallback_ServerReflexiveKeepAlive, m_nEncoding ); }