Calls and TURN
Call media is peer to peer. That reaches most of the internet, but not all of it, and the failure is the worst possible shape: signaling succeeds, the person appears in the participant list, and there is simply silence. No error, nothing to click.
Why it happens
Section titled “Why it happens”Your laptop has no address the outside world can dial. Your router does, shared among everything behind it. WebRTC works around this by asking a STUN server what address it sees you from, then advertising that to the other side. Your outbound packet punched a hole in the NAT, so the reply comes back through it.
That assumes your router reuses one mapping for every destination. Most home routers do. Symmetric NAT does not: it allocates a different external port per destination, so the address the STUN server reports is only good for talking to the STUN server. Both sides advertise addresses that lead nowhere. This is common on corporate networks and mobile carriers.
Networks that block UDP outright fail the same way.
What TURN does
Section titled “What TURN does”A TURN server is a relay you can rent an address from. You authenticate, ask for an allocation, and it sets aside a public address that forwards to you. Because you opened that connection yourself, the return path works no matter how hostile your NAT is.
ICE still prefers a direct path and only falls back to the relay when nothing else connects, so adding TURN does not route everyone through your server.
Running one
Section titled “Running one”# in .envTURN_EXTERNAL_IP=203.0.113.10 # this machine's public IPTURN_SECRET=$(openssl rand -hex 32)
docker compose -f docker/compose.yml -f docker/compose.turn.yml \ --env-file .env up -dOpen 3478 UDP and TCP, 5349 for TLS, and the relay range
49152 to 50175 UDP in the firewall and any cloud security group. That range
is why a relay cannot sit behind nginx: it is not HTTP traffic. The bundled
coturn reuses Caddy’s certificate for turns://.
The relay range is the part people skip, because opening a block of UDP ports looks alarming next to three tidy TCP ones. Skip it and a call negotiates, connects, and stays silent: the relay hands out a port from that range and names it inside the protocol, so a blocked range fails after everything that would have warned you has already succeeded. It is 1024 ports rather than coturn’s default 16384, which is more than a self-hosted workspace will hold at once and a much smaller thing to leave open.
To use a relay you already run, set TURN_URLS and TURN_SECRET and skip the
compose file.
Credentials
Section titled “Credentials”Slick issues them per request from GET /api/ice, using coturn’s
use-auth-secret scheme: the username is <expiry>:<user-id> and the password
is base64(HMAC-SHA1(secret, username)). The shared secret never leaves the
server.
This is not ceremony. A relay with fixed credentials in your web bundle is an
open relay for anyone who opens devtools, billed to you. Setting TURN_URLS
without TURN_SECRET therefore fails at startup.
Relayed traffic is paid for twice, inbound and outbound. Calls are a full
mesh, so a participant in a four person call sends three copies of their
stream. One relay-only participant with video can mean several Mbps sustained.
docker/turnserver.conf ships with per-allocation and total quotas. Read them
before opening this to a crowd.
Checking it works
Section titled “Checking it works”Open devtools on a call and look for a candidate of type relay. If you see
401 Unauthorized against your TURN URL, the secret does not match. If you see
no relay candidate and no error, TURN_URLS is not reaching the browser: check
GET /api/ice in the network tab.