Backend / Networking / 19_stun_turn_and_coturn.md

STUN, TURN and coturn

Updated 4 interview angles 7 min read source
On this page8
  1. The mechanism: what NAT does to an incoming packet
  2. TURN: a relay you have to pay for
  3. A coturn configuration that holds up
  4. Operating it
  5. Where it breaks in production
  6. The trade not taken: no TURN server
  7. Related
  8. Interview angle

STUN, TURN and coturn

Article.

A voice or video call wants a direct path between two devices, and most devices sit behind NAT and firewalls that do not allow one. STUN tells a device what its public address looks like; TURN relays the media when no direct path works; ICE tries every candidate path and keeps the best.

The interview question is why a call that works on office Wi-Fi fails on a mobile network, and what running the TURN server that fixes it costs and risks. coturn is the TURN server almost everyone runs, including next to Matrix homeservers.

The mechanism: what NAT does to an incoming packet

A NAT rewrites an outgoing packet’s source to its public address and a port, and remembers the mapping so replies come back. Whether a different remote host can use that mapping is what decides if a direct call is possible:

NAT mapping Same public port for every destination? Direct path
Endpoint-independent yes usually works with STUN
Endpoint-dependent (“symmetric”) no, a new port per destination fails; needs TURN

STUN (RFC 8489) is a one-packet question to a public server: “what address did my packet come from?” The answer arrives in XOR-MAPPED-ADDRESS. Sending a Binding Request from one local UDP socket to two STUN servers run by different providers shows which NAT you are behind:

python
request = struct.pack("!HHI12s", 0x0001, 0, MAGIC, txn)          # Binding Request
sock.sendto(request, server)
data, _ = sock.recvfrom(2048)                                       # Binding Success
port = struct.unpack("!H", val[2:4])[0] ^ (MAGIC >> 16)   # XOR-MAPPED-ADDRESS
text
local socket port 62431
  stun.l.google.com (74.125.250.129) sees us as <public IP>:58498
  stun.cloudflare.com (162.159.207.0) sees us as <public IP>:8404
  same mapping for both servers: False -> symmetric NAT, expect TURN

Run from a home network on 2026-10-15 (public IP masked): one local socket, two different public ports. The address a STUN server reports is useless to a peer, because the NAT will hand that peer yet another port. Carrier-grade NAT on mobile networks behaves the same way, which is why calls that work on office Wi-Fi fail on 4G.

ICE: try everything, keep the best

ICE (RFC 8445) gathers candidates for each side, exchanges them through the signalling channel, and runs connectivity checks between every pair:

  • host candidates: the device’s own addresses, for the same LAN;
  • server-reflexive: the public address STUN reported;
  • relay: an address allocated on a TURN server.

The cheapest working pair wins. Relay is the last resort and the one that always works, because both sides talk out to the TURN server, which NAT always allows.

TURN: a relay you have to pay for

TURN (RFC 8656) allocates a public address on the server and forwards packets between it and the client. The cost is real: every relayed call passes its full media bandwidth through your server, in and out. A 1:1 video call at around 1.5 Mbit/s each way is about 6 Mbit/s of relay traffic, and egress is what cloud providers bill. Size TURN for the share of calls that end up relayed, typically a minority but never zero.

Credentials that expire

A TURN server must not be open to anyone, but clients cannot hold a permanent password. The TURN REST scheme solves it with a secret shared between the homeserver and coturn: the server mints a username that embeds an expiry time, and the password is an HMAC that coturn can recompute:

python
secret = b"shared-with-coturn"    # turn_shared_secret = static-auth-secret
username = f"{int(time.time()) + 3600}:@alice:example.org"
digest = hmac.new(secret, username.encode(), hashlib.sha1).digest()
password = base64.b64encode(digest)

coturn checks the HMAC and rejects the username once its timestamp has passed. No database of TURN users, nothing to revoke: a leaked credential dies on its own. Synapse hands these out through GET /_matrix/client/v3/voip/turnServer when turn_shared_secret is set (VoIP calls on Matrix).

A coturn configuration that holds up

ini
# /etc/turnserver.conf
use-auth-secret
static-auth-secret=<64 random characters, shared with the homeserver>
realm=turn.example.org

listening-port=3478            # STUN/TURN over UDP and TCP
tls-listening-port=5349        # TURN over TLS; 443 gets through strict firewalls
cert=/etc/coturn/fullchain.pem
pkey=/etc/coturn/privkey.pem
min-port=49152                 # the relay range, open for UDP
max-port=65535
external-ip=203.0.113.10       # when coturn itself is behind NAT, e.g. on EC2

no-tcp-relay                   # media is UDP
no-multicast-peers
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
denied-peer-ip=192.168.0.0-192.168.255.255
denied-peer-ip=169.254.0.0-169.254.255.255    # includes the cloud metadata service
user-quota=12                  # streams per user: about three relayed calls
total-quota=1200

TURN over TLS on port 443 is what gets calls out of corporate networks that allow only HTTPS; it needs its own IP or a TLS-aware proxy if a web server already holds 443 there.

Operating it

  • Give clients several URIs. turn: over UDP for most calls, turn: over TCP and turns: on 443 for restrictive networks; ICE uses whichever works. In Synapse that is a list in turn_uris.
  • Put TURN near the users. A relayed call’s latency is the path through the relay, so one TURN server on another continent adds hundreds of milliseconds to every relayed call. Regional TURN servers, chosen by DNS or by the homeserver per user, keep relayed calls usable.
  • Keep it off the homeserver host. Relay traffic is bursty bandwidth and a wide open UDP port range; a separate host keeps a call spike from starving the homeserver and keeps the port range out of its security group.
  • Measure what is relayed. coturn’s Prometheus exporter (the prometheus option, port 9641 by default, off by default and missing from some distro builds) reports allocations and traffic; the number that matters is the share of calls that needed the relay and their bandwidth. Leave per-username labels off: with time-limited credentials every user is a new label.

Where it breaks in production

  • TURN as a door into your network. A TURN client asks the server to relay to any IP and port it names. Without denied-peer-ip rules for private ranges, loopback and 169.254.169.254, an authenticated user can reach internal services through the relay: server-side request forgery over UDP and TCP.

    The rules themselves have been bypassed: three coturn CVEs in 2026 slipped past them with IPv4-mapped IPv6 addresses (::ffff:127.0.0.1) and IPv6 range matching, fixed in releases up to 4.16.0. Run a current coturn and test the deny list with IPv6 forms.

  • Calls connect, then no audio. The relay port range is closed in the security group or the firewall, or external-ip is missing so coturn advertises its private address. Signalling works, media does not.

  • Only some users fail. The ones on symmetric NAT or strict corporate networks, because TURN is missing or reachable only on UDP 3478. Offer TLS on 443.

  • The bandwidth bill. Relayed video at scale is egress cost; watch the relayed share of calls and per-user quotas.

  • A certificate the clients reject. Synapse’s coturn guide notes that some WebRTC clients have rejected certain certificate chains on turns:; test TLS from real mobile clients, not only a browser.

The trade not taken: no TURN server

The tempting shortcut is STUN only, or a public free TURN service. STUN-only calls fail for every user behind symmetric NAT, which on mobile networks is many of them, and the failure looks like “the app’s calls are flaky”. A third party’s TURN server sees every relayed packet’s metadata and can disappear. For a product with calls, running coturn (or paying for a managed TURN service) is part of the cost of the feature, not an optimization.

Interview angle 4

  • “STUN vs TURN?” - STUN tells a device its public address and port, one request, no relaying; TURN relays the media itself when no direct path works. ICE tries host, STUN-derived and TURN candidates and keeps the best.
    • Follow-up: when does STUN alone fail? Behind symmetric NAT, where each destination gets a different public port. Measured: one socket, two STUN servers, ports 58498 and 8404, so the reported address is useless to a peer.
  • “How do you stop strangers using your TURN server?” - time-limited credentials: the homeserver mints expiry:user and an HMAC with a secret shared with coturn, which recomputes it and rejects expired usernames.
  • “What is the security risk of running TURN?” - it relays to addresses the client chooses, so it can reach your private network and the metadata service. Deny private, loopback and link-local ranges, and keep coturn current: IPv4-mapped IPv6 tricks bypassed the deny list until recent releases.
    • Follow-up: why open port 443? Strict networks allow only HTTPS; TURN over TLS on 443 gets calls out of them.
  • “Calls connect but there is no audio. Where do you look?” - the relay UDP range in the firewall, and external-ip when coturn runs behind NAT.