The idea: two browsers, two NATs, no shared address

A WebRTC call wants to send audio and video directly between two browsers, over UDP. But Alice and Bob are each behind a home router doing NAT: their own addresses (192.168.1.10, 10.0.0.5) are private, and their routers drop any packet nobody inside asked for. So before a single audio packet flows, the two browsers must find out which addresses could reach them, swap those addresses, and test which ones actually work. That process is ICE (Interactive Connectivity Establishment, RFC 8445), helped by a STUN server and, when nothing direct works, a TURN relay.

Alice behind NAT A and Bob behind NAT B; a signalling server carries the offer and answer, a STUN server tells each side its public address, a TURN server can relay media; the three candidate kinds host, server-reflexive and relay are marked
Signalling carries the offer and answer; STUN gives the server-reflexive address; TURN gives a relay address for when no direct path works.

Signalling and the SDP offer/answer

The browsers first need some way to talk at all. WebRTC leaves this signalling channel to the application: usually a WebSocket to the app's server, which forwards messages between the two users. Over it, the caller sends an offer and the callee replies with an answer. Both are SDP (Session Description Protocol) text: codecs, ICE username fragment and password, the DTLS certificate fingerprint, and the candidates.

v=0
m=audio 9 UDP/TLS/RTP/SAVPF 111
a=ice-ufrag:F7gI
a=ice-pwd:x9cml/YzichV2+XlhiMu8g
a=fingerprint:sha-256 D1:2C:...:9A
a=candidate:1 1 udp 2130706431 192.168.1.10 50000 typ host
a=candidate:2 1 udp 1694498815 203.0.113.7 50000 typ srflx raddr 192.168.1.10 rport 50000
a=candidate:3 1 udp 16777215 192.0.2.60 49152 typ relay raddr 203.0.113.7 rport 50000

In the browser: createOffer() and setLocalDescription() on Alice's side, setRemoteDescription() and createAnswer() on Bob's, and setRemoteDescription() again on Alice's when the answer comes back. The signalling state goes have-local-offer / have-remote-offer → stable. (Demo 4: Signalling only)

Gathering candidates

A candidate is an address where a browser might receive packets. Each side gathers three kinds, all from the same UDP socket:

TypeHow it is learnedAlice'sPriority
hostthe machine's own interface address192.168.1.10:500002130706431
srflx (server-reflexive)a STUN Binding request: the server answers with the source it saw, XOR-MAPPED-ADDRESS203.0.113.7:500001694498815
relaya TURN Allocate request: the server reserves a port and forwards whatever reaches it192.0.2.60:4915216777215

The priority is 224·type + 28·local + (256 − component), with type preference 126 for host, 100 for srflx and 0 for relay: direct paths first, the relay last. A TURN server also only relays from peers its client has allowed with CreatePermission (per IP address), so Bob installs one for Alice's public IP. Real browsers usually trickle candidates (send each one as soon as it is found) instead of waiting for all of them as the page does.

Candidate pairs and connectivity checks

Alice is the controlling agent. She pairs each of her candidates with each of Bob's and sorts the pairs by 232·min(G,D) + 2·max(G,D) + (G>D ? 1 : 0), where G and D are the two candidates' priorities. Her srflx candidate is dropped from the pairs: it sends from the same socket as her host candidate, so pairing both would test the same thing twice. That leaves 6 pairs, host ↔ host first and relay ↔ relay last.

A connectivity check is a STUN Binding request sent on a pair, with the ICE password as a message integrity key. The pair works when the request and its success response both get through. Both sides check, and that is what punches the holes: Alice's first request to Bob's srflx address is dropped by Bob's NAT, but it added a row in Alice's NAT, so Bob's request the other way gets in. Alice answers and retries (a triggered check), and now both NATs have rows for each other. (Demo 1: Cone NATs)

Time-down diagram: Alice's check to Bob's public port is dropped by NAT B, Bob's check to Alice's public port is let in by NAT A because of the row Alice's check created, then Alice's retry gets through NAT B
The first check is dropped but leaves a row in Alice's NAT; Bob's check uses it, and after that both directions are open.

When a pair succeeds, Alice nominates it (one more check with the USE-CANDIDATE flag) and the ICE state becomes connected. Then the browsers run a DTLS handshake on that path, check the certificate against the fingerprint in the SDP, and send media as SRTP. A STUN check every few seconds (consent freshness) keeps the NAT rows from expiring.

Why a symmetric NAT forces TURN

Hole punching depends on one thing: the public port Bob's NAT used towards the STUN server is the same port it uses towards Alice. That is endpoint-independent mapping (a "cone" NAT). A symmetric NAT picks a new public port for every destination. So Bob's srflx candidate 198.51.100.9:62000 is only valid for the STUN server: Bob's check to Alice leaves on :62003, which Alice's NAT has never seen, and Alice's check to :62000 hits a row that only accepts the STUN server. Every direct pair fails.

Left: a cone NAT uses public port 62000 for both the STUN server and Alice, so the STUN answer is valid. Right: a symmetric NAT uses 62000 for STUN and 62003 for Alice, so the address STUN reported is wrong for Alice
A cone NAT reuses one public port for every destination; a symmetric NAT does not, so the address STUN reported is useless to the peer.

The relay pair still works: Alice sends to Bob's relay address 192.0.2.60:49153, which Alice's NAT treats as one more remote, the TURN server checks its permission and forwards the packet inside a Data indication to Bob's TURN connection, which Bob's NAT already allows. Bob answers with a Send indication the other way. (Demo 2: Symmetric NAT) With no TURN server configured, the call fails. (Demo 3: no TURN)

Bob's NATTURNSelected pairMedia path
port-restricted coneon or off#2 host ↔ srflxdirect ✓
symmetricon#3 host ↔ relaythrough TURN ✓
symmetricoffnonecall fails ✗

What TURN costs

A relayed call sends every media packet through the TURN server and back out, so the operator pays for the bandwidth twice and the path is longer. That is why relay candidates have the lowest priority and are only used when nothing else works. In practice a minority of calls need TURN (corporate networks, mobile carriers with symmetric carrier-grade NAT, UDP-blocking firewalls), but a WebRTC service without one fails for exactly those users. TURN over TCP or TLS on port 443 gets through firewalls that block UDP; see TCP vs UDP for why media prefers UDP when it can.

What the page leaves out

Trickle ICE, the 401 challenge and long-term credentials before a TURN Allocate, peer-reflexive candidates (learned from a check that arrives from an unknown address), IPv6 candidates, the check pacing timer (Ta) and retransmissions, frozen pairs and foundations, ICE restarts, consent expiry, RTCP and multiple components (the page uses one, as BUNDLE and rtcp-mux do), TURN over TCP/TLS, Alice's own TURN permissions, and the signalling server's own login and room logic.