Repository navigation
ICE (native client): relay-only connections time out against a real TURN server; host-socket checks starve the relay pairs #452
Description
Version: master at d534c19 (1.6.x), native ICE client (steamnetworkingsockets_ice_client.cpp, ENABLE_ICE=ON, USE_STEAMWEBRTC=OFF), custom signaling.
Setup: k_ESteamNetworkingConfig_P2P_Transport_ICE_Enable = k_nSteamNetworkingConfig_P2P_Transport_ICE_Enable_Relay, no STUN servers, one TURN server (coturn 4.6.3, use-auth-secret) with P2P_TURN_ServerList/UserList/PassList set. Both peers relay-only, on different machines, both allocate on the same TURN server.
What happens: allocations and CreatePermission succeed on both sides, the relay candidates are exchanged, and then the connection fails after the initial timeout with 5008: Timed out attempting to negotiate rendezvous ("Guessed ICE failure to be 5009: NAT traversal failed"). Verbose logging shows only ICE: STUN request 0x1 to <peer relay addr> timed out for every remote candidate. A packet capture on the TURN server shows no Send indications at all after CreatePermission: only binding requests sent directly from each peer's own socket to the other peer's relayed address, which the TURN server (correctly) drops, as it has no permission for the sender's address.
Why (as far as I can tell):
Think_TestPeerConnectivity()always adds the interface's host socket as a local candidate (// Host candidate -- always present.), even whenm_nPermittedCandidateTypespermits only relay candidates. So every remote relay candidate gets a host-socket pair, besides the relay pair. In relay-only mode those host pairs can't succeed: they send straight to a relayed address.- Pairs are unfrozen one per remote foundation at a time (
vecFoundationsUsed, keyed onm_remoteCandidate.m_sFoundationonly), and host pairs sort first. So the relay pair for a remote candidate waits until every host pair for it (one per local interface) has timed out, which takes longer than the connection's initial timeout. (RFC 8445 6.1.2.6 makes a pair's foundation from both the local and the remote candidate's foundations, so host and relay pairs to the same remote candidate wouldn't share one.)
Why it's easy to miss: with both peers (or a peer and the TURN server) on one machine, the relayed address is a local address, so a "direct" host-socket check can be sent from that address and passes the TURN server's permission check as if it were relayed. Everything works until the peers are on different machines.
Repro on one machine: run coturn with its relays on an address that's also local (e.g. ip addr add 5.5.5.5/32 dev lo, --relay-ip=5.5.5.5), and drop packets that arrive on lo for the relay ports from anything but the relay ports (nft ... iifname "lo" ip daddr 5.5.5.5 udp dport 49160-49659 udp sport != 49160-49659 drop), as a real network would. Two relay-only peers then never connect; with the patch below they connect at once.
Patch (what we ship): don't add the host local candidate when only relay candidates are permitted.
--- a/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_ice_client.cpp
+++ b/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_ice_client.cpp
@@ -2385,8 +2385,12 @@ void CSteamNetworkingICESession::Think_TestPeerConnectivity()
ICELocalCandidate localCandidates[2];
int nLocalCandidates = 0;
- // Host candidate -- always present.
- localCandidates[nLocalCandidates++] = { pIntf.get(), {} };
+ // Host candidate -- unless only relay candidates are permitted. Then checks from the
+ // host socket would go straight to the peer's relay candidates, whose TURN server drops
+ // them (no permission for this address), and they'd hold up the relay pairs to the same
+ // remote candidates (one pair per foundation at a time) until the connection times out.
+ if ( m_nPermittedCandidateTypes & ~k_EICECandidate_Any_Relay )
+ localCandidates[nLocalCandidates++] = { pIntf.get(), {} };The foundation point (2) probably deserves a fix of its own: in "all" mode it still delays the relay fallback behind every host pair to the same remote candidate.
Happy to open a PR with the patch if that's useful.
Activity