Skip to content

Commit 95bf738

Browse files
ralyodioclaude
andcommitted
fix(sfu): use TCP TURN relay, not UDP, so multi-homed hosts don't drop
A UDP TURN relay rides UDP between the client and the TURN server, which NAT-rebinds and silently dies a couple minutes into a stream on a multi-homed / dead-NIC host. With "Force relay" (relay-only) there's no fallback, so the publisher fails with "could not establish pc connection" — even though the relay connected fine initially. Append ?transport=tcp to plain turn: URLs so the client→TURN leg is a stable TCP connection (coturn listens TCP on 3478; verified solid). turns: (TLS) is left as a last-resort fallback only — its handshake is intermittently flaky on this coturn. Server-side, so existing clients pick it up on the next connect (no app release). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f1ab493 commit 95bf738

1 file changed

Lines changed: 16 additions & 4 deletions

File tree

‎apps/web/src/lib/ice-servers.ts‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,26 @@ export function getIceServers(): IceServer[] {
1717
const username = process.env.TURN_SERVER_USERNAME ?? process.env.NEXT_PUBLIC_TURN_USERNAME;
1818
const credential = process.env.TURN_SERVER_CREDENTIAL ?? process.env.NEXT_PUBLIC_TURN_CREDENTIAL;
1919

20-
// Prefer TURNS (TLS/443-style) first — it tunnels over TCP and survives the
21-
// NAT rebinding / multi-homing that kills plain UDP. Then UDP TURN, then a
22-
// raw-IP fallback for when DNS is the problem.
20+
// Force TCP transport on plain turn: URLs. A UDP TURN relay still rides UDP
21+
// between the client and the TURN server, so on a multi-homed / dead-NIC host
22+
// it NAT-rebinds and silently drops a minute or two into a stream — and with
23+
// "Force relay" (relay-only) there is no fallback candidate, so the publisher
24+
// dies with "could not establish pc connection". A TCP relay survives the
25+
// rebind. coturn listens TCP on 3478 and the TCP relay path is solid (the
26+
// TLS :5349 path is intermittently flaky, so we keep turns: only as a last
27+
// resort). turns: already rides TCP, so leave it untouched.
28+
const toTcp = (u: string): string =>
29+
u.startsWith('turns:') || u.includes('transport=')
30+
? u
31+
: `${u}${u.includes('?') ? '&' : '?'}transport=tcp`;
32+
2333
const turnUrls = [
2434
process.env.NEXT_PUBLIC_TURNS_URL,
2535
process.env.TURN_SERVER_URL ?? process.env.NEXT_PUBLIC_TURN_URL,
2636
process.env.TURN_SERVER_IP_URL ?? process.env.NEXT_PUBLIC_TURN_IP_URL,
27-
].filter((u): u is string => Boolean(u));
37+
]
38+
.filter((u): u is string => Boolean(u))
39+
.map(toTcp);
2840

2941
if (turnUrls.length > 0 && username && credential) {
3042
servers.push({ urls: turnUrls, username, credential });

0 commit comments

Comments
 (0)