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.
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:
| Type | How it is learned | Alice's | Priority |
|---|---|---|---|
| host | the machine's own interface address | 192.168.1.10:50000 | 2130706431 |
| srflx (server-reflexive) | a STUN Binding request: the server answers with the source it saw, XOR-MAPPED-ADDRESS | 203.0.113.7:50000 | 1694498815 |
| relay | a TURN Allocate request: the server reserves a port and forwards whatever reaches it | 192.0.2.60:49152 | 16777215 |
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)
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.
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 NAT | TURN | Selected pair | Media path |
|---|---|---|---|
| port-restricted cone | on or off | #2 host ↔ srflx | direct ✓ |
| symmetric | on | #3 host ↔ relay | through TURN ✓ |
| symmetric | off | none | call 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.