What breaks when you self-host a TURN server (coturn — symptom, cause, fix)
DEV Community

What breaks when you self-host a TURN server (coturn - symptom, cause, fix)

The first article in this series was the traps inside the code. The second was the layer underneath - the candidates that lie, the UDP firewall nobody looks at, the reverse proxy that reaps an idle socket. This one is about the piece people reach for when those two are not enough: the relay. TURN is the part of WebRTC that gets configured last, in a hurry, by copying a config off a blog post. That is exactly how you end up with a relay that passes every "is it running?" check and still produces no video. The failures below are all coturn behaviours you can read in the config and the log. What makes them expensive is not that they are subtle - it is that most of them fail without an error message anywhere near where you are looking. First: is TURN even your problem? It often is not. Adding a relay to a system whose real fault is elsewhere hides the fault and makes the next debugging session worse. Three checks, in this order. Look at which candidate type won. In chrome://webrtc-internals (or about:webrtc in Firefox), find the selected candidate pair and read the local candidate's type. If it says relay , TURN is in the path and working, and your problem is somewhere else entirely. If it says srflx or host while the connection fails, TURN is not being used at all - which is a different bug from TURN being broken, and it lives in your ICE configuration. Force the relay. In a test page, build the peer connection with iceTransportPolicy: 'relay' . Now TURN is the only permitted path. If this connects while your normal config does not, your relay is fine. If this also fails, you are actually debugging TURN. Check the two failures from the last article first. A server advertising an unroutable address and a closed UDP media range both produce the same visible symptom as a dead relay: signaling green, ICE stuck checking, then failed. They cost nothing to rule out and they are far more common than a broken coturn. It says it started and is bound to nothing Symptom. systemctl start coturn returns success. systemctl status coturn says active. Nothing works. Cause. On Debian and Ubuntu the package ships /etc/default/coturn with the enable switch commented out, and the start path honours that file. Depending on the version and how the package was installed, that gate is either the init script or a unit that wraps it - and either way you get a clean exit with no error message. A service that declined to start and a service that started and could not bind look identical from systemctl . This is also where a config file mismatch hides: coturn reads /etc/turnserver.conf by default, but if something passes -c to a different path, your edits are being ignored and nothing tells you. Check. Do not ask whether the service is running. Ask whether anything is bound. ss -lunp | grep -E '3478|5349' journalctl -u coturn -n 50 --no-pager If the ports are not in the ss output, it is not running in any sense that matters. Note that curl is useless here - TURN is not HTTP, so an HTTP request to 3478 will connect and then produce something unhelpful. It proves nothing either way. Fix. TURNSERVER_ENABLED=1 in /etc/default/coturn , restart, re-run the check. If it is still not bound, you are now looking at a real startup error and the log will have it. The relay address points inward Symptom. The allocation succeeds. The client gets a relayed candidate. The candidate is a 10.x or 172.16-31.x address. ICE never gets past checking, from the outside it looks like the relay is broken, and on the server everything is green. Cause. A cloud VM's network interface has a private address and the public IP exists in the provider's NAT layer in front of it. The machine has no idea what its public address is. coturn advertises the address it is bound to, so it advertises the private one, and nobody outside can route to it. This is the same class of bug as the host candidates in the previous article, one layer up: the service is telling the truth about an interface that is a lie. Fix. Both halves of the mapping: external-ip=203.0.113.10/10.0.0.4 Public first, private second, slash between them. Setting only the public address is a common half-fix that works until coturn needs to bind locally. Verify. Re-read the relayed candidate. It should now be the public address, and it should be reachable - which is a different claim, and the next section is why. You opened the control port and called it done Symptom. Allocation succeeds. The relayed candidate is correct and public. Media still never flows. Or: it works for one client and not another, with nothing different about them. Cause. 3478 is the control port. The media does not go over it. coturn allocates a relay port per client from a range - min-port to max-port , default 49152 to 65535 - and the actual RTP flows there, over UDP. This one is worse than the others because the successful allocation proves your firewall work is partly right, which is exactly the wrong amount of right. You conclude the network is fine and go looking in your application code. Fix. Open the relay range in both firewalls: the provider's security group and the host's own ufw/iptables. On most cloud providers the security group is the one that actually decides whether a packet arrives and the host firewall is a red herring; on a VPS with a full iptables ruleset it is the reverse. Open UDP. Opening only TCP gets you nothing, because browsers will use UDP for TURN whenever it is available. Do not narrow the range "for security." This is the trap hidden in a reasonable-sounding hardening step. Each client using the relay takes a port out of the range, and one page can open more than one allocation. A forty-port range is a lab with a handful of users and nothing else, and the way you find out is under load, months later, as an intermittent failure that does not reproduce. Keep the default range, and if you must narrow it, size it against the number of concurrent clients you expect rather than the number of streams you are picturing. Nobody can authenticate, and only the log will say why Symptom. Every allocation is refused. The client retries, or gives up and reports ICE failure. The relay is bound, reachable, and simply will not talk to you. Cause. coturn has two credential modes and they are alternatives, not layers: lt-cred-mech is static long-term credentials, with users created through turnadmin . If you enable it and never create a user, every request is refused and there is nothing wrong with your configuration as such. use-auth-secret with a static-auth-secret is the time-limited REST scheme. The client generates a username of : and a password of base64(HMAC-SHA1(secret, username)) . Every part of that has to be exact - the HMAC variant, the encoding, the units of the expiry. Get any of it slightly wrong and you get a refusal that looks exactly like a wrong password, because it is one. A config that sets neither is, in effect, an unauthenticated relay. That is not a neutral default: it is an open relay, someone will find it, and their traffic will be on your bill and your IP's reputation. Fix. Pick one mode, set it explicitly, and turn on verbose so the log shows you the request instead of leaving you to infer it. Then generate the credential the way the server expects: # username = : ; password = base64(hmac-sha1(secret, username)) EXP=$(( $(date +%s) + 3600 )) USER="$EXP:demo" PASS=$(printf '%s' "$USER" | openssl dgst -binary -sha1 -hmac "$SECRET" | openssl base64) printf 'username=%s\npassword=%s\n' "$USER" "$PASS" Read the response code, not the symptom. TURN tells you exactly what is wrong if you look at the code in the log: - 401 - the challenge. On a first request this is normal and expected; the client is supposed to retry with credentials. Seeing 401s is not itself a fault, and treating it as one sends people down a long road. - 441 - wrong credentials. Your HMAC or your expiry is off. - 438 - stale nonce. The client cached a nonce past its lifetime; a conforming client retries and you should not see it persist. - 403 - forbidden, including a denied peer. See the blocklist section. - 486 - allocation quota reached. See the last section. coturn ships with turnutils_uclient , which does a real allocation against your server and prints what came back. It is the fastest way to test the relay without a browser in the loop at all - check turnutils_uclient -h for the current flags. The certificate is valid and coturn still cannot read it Symptom. turn: on 3478 works. turns: on 5349 does not. Or coturn fails to start with a message about a file that definitely exists. Cause. turns: needs a certificate, coturn runs as its own unprivileged user, and Let's Encrypt's /etc/letsencrypt/live/ directory is symlinks into archive/ behind permissions that user does not have. The certificate is valid, your path is correct, and the process cannot open it. Fix. Give the turnserver user read access to the cert tree, or have the renewal hook copy the pair somewhere readable with the right ownership. Both work; the second survives a permissions change on the original. And the part worth more than the fix: turn: on 3478 does not need TLS at all. If what you need is UDP relay for browsers, you can leave turns: out entirely and this whole failure mode stops existing. There is a second assumption worth killing while you are here: UDP TURN from an HTTPS page is not mixed content. The browser will not block it, and you do not need turns: to satisfy a security policy. It is a common reason people add a certificate they did not need and then spend an evening on permissions. The blocklist is doing exactly what it was designed to do Symptom. Allocation succeeds, the candidate is right, the ports are open, and media still does not flow. The log mentions the peer. Cause. coturn ships with denied-peer-ip entries covering loopback, the private ranges, link-local, multicast and reserved space. This is deliberate and good: it is what stops your relay being us

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.