A STUN server does one thing: it tells you what your address looks like from the outside. There is no session, no account, no state kept between requests — a request arrives, the server reads the source address off the packet, and it writes that address into a reply. The entire protocol fits comfortably in an afternoon of reading.
That simplicity is why STUN is free to run at internet scale, why public servers exist at all, and why it fails in ways that look mysterious until you know what it is and is not doing. This article covers what actually goes over the wire, one widely-taught piece of STUN that has been deprecated for over fifteen years, and how to tell a broken STUN server apart from a broken network.
The problem STUN exists to solve
Your laptop believes its address is something like 192.168.1.47. That address is meaningful only inside your home or office network. When a packet leaves for the internet, your router rewrites the source address to its own public IP and picks a port to represent your device — a NAT binding. Replies to that public IP and port get translated back and delivered to you.
The consequence for peer-to-peer communication is that your device has no way to learn its own public address by looking inward. Nothing in the operating system knows it. The router does not tell you. The only way to find out is to ask something on the other side of the NAT what it saw.
That is STUN. Session Traversal Utilities for NAT, currently specified in RFC 8489. It is a mirror.
One round trip, byte by byte
Every STUN message opens with the same 20-byte header:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0 0| STUN Message Type | Message Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Magic Cookie (0x2112A442) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Transaction ID (96 bits) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
A binding request is message type 0x0001 with no attributes at all — a bare 20-byte packet. The success response is 0x0101 and carries the answer.
Three details in that header earn their keep:
- The two leading zero bits let a receiver demultiplex STUN from RTP on the same socket. This matters enormously later: during a call, connectivity checks and media share one port, and those bits are how the stack tells them apart.
- The magic cookie is a fixed constant. It marks the packet as modern (RFC 5389 or later) rather than the 2003-era original, and it doubles as the XOR key described below.
- The transaction ID is 96 random bits matching a response to its request — the only state involved, and it lives on the client.
Why the address is XORed
The response reports your public address in an attribute called XOR-MAPPED-ADDRESS, not in plaintext. The port is XORed with the top 16 bits of the magic cookie and the IPv4 address with the whole cookie.
This is not security — the key is a published constant, so anyone can undo it. It exists because of NAT devices that "helpfully" inspect packet payloads and rewrite any IP address they recognise, a behaviour inherited from FTP-style application-layer gateways. A plaintext address in the payload got silently mangled by those devices, and the client received a confidently wrong answer. XORing makes the address unrecognisable to a naive scanner. The original MAPPED-ADDRESS attribute still exists for backward compatibility and should be ignored when the XORed form is present.
The NAT-type discovery you have read about is deprecated
Search for STUN and you will find, on a great many pages, a classification of NATs into full-cone, restricted-cone, port-restricted-cone, and symmetric, together with a decision tree for detecting which one you are behind using multiple STUN requests from different server addresses.
That algorithm comes from RFC 3489, published in 2003. RFC 5389 removed it in 2008, and RFC 8489 has not brought it back. The reason is that it did not work reliably: real NATs do not fall cleanly into four categories, many change behaviour under load or per-destination, and a device could be classified as one type and then behave as another moments later. Building connectivity logic on the result produced software that was confidently wrong.
What replaced it is ICE, which does not classify anything. ICE gathers every candidate address it can and then empirically tests which pairs actually carry traffic. It is less elegant and far more robust — you do not need to know what kind of NAT you are behind if you simply try the paths and keep the one that answers. See the ICE framework explained.
Practical takeaway: if a tool or article offers to tell you your "NAT type," treat that as a rough diagnostic hint, not as something to branch on. The one distinction that still carries real weight is whether your NAT is symmetric, because symmetric NAT is what forces a relay — and the reliable way to find that out is to observe that direct connections keep failing while relayed ones succeed. NAT traversal challenges covers that case.
STUN is used twice, and the second time surprises people
Nearly every explanation stops after "the client asks the server for its public address." But STUN messages appear at two entirely different moments in a WebRTC call, and conflating them makes packet captures unreadable.
| Gathering | Connectivity checks | |
|---|---|---|
| Who is asked | Your configured STUN server | The other peer, directly |
| Purpose | Learn your srflx candidate | Prove a candidate pair carries traffic |
| Authentication | None | USERNAME and MESSAGE-INTEGRITY keyed on the SDP ice-ufrag/ice-pwd |
| Volume | A handful of packets, once | Many, and continuing as keepalives for the life of the call |
So a call that is fully established is still exchanging STUN binding requests between the peers as consent freshness and keepalive traffic. If you are staring at a capture wondering why STUN traffic persists long after gathering finished, that is why — and it is not going to your STUN server.
Using STUN in WebRTC
Configuration is one line, which is part of why it gets so little thought:
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
});
Some notes that are not obvious from that snippet:
- A STUN entry takes no credentials. If you find yourself supplying
usernameandcredentialalongside astun:URL, they are ignored — you were probably reaching for TURN. - Browsers use UDP for STUN. The
stuns:(TLS) scheme is defined in RFC 7064, but browser support for it iniceServersis effectively absent. On a network that blocks outbound UDP entirely, no amount of STUN configuration helps; that is a TURN-over-TLS situation. - More STUN servers is not better. Each one adds candidates and therefore adds candidate pairs to check, which slows gathering. Browsers also apply their own internal limits on how many configured servers they will actually query, and those limits have changed across versions. One reliable STUN server, plus TURN for the cases STUN cannot solve, covers essentially every real deployment.
- Default port is 3478. Google's public servers are the well-known exception at
19302.
Verifying it works from JavaScript
You do not need a full call to check whether STUN is answering. Create a peer connection, add a throwaway data channel to force gathering, and watch what candidate types appear:
function checkStun(url, timeoutMs = 5000) {
return new Promise((resolve) => {
const pc = new RTCPeerConnection({ iceServers: [{ urls: url }] });
const timer = setTimeout(() => { pc.close(); resolve(false); }, timeoutMs);
pc.onicecandidate = ({ candidate }) => {
if (candidate && candidate.type === 'srflx') {
clearTimeout(timer);
pc.close();
resolve(true); // server answered with our public address
}
};
pc.createDataChannel('probe');
pc.createOffer().then((o) => pc.setLocalDescription(o));
});
}
The success condition is exactly one thing: an srflx candidate appeared. Host candidates always show up and prove nothing; their absence of a public address is the whole reason you asked. This is the same check the tester on this site runs, which is deliberate — it runs in your browser on your network, so the answer reflects the path your users actually take rather than a data centre's clean one.
Public STUN servers: fine for testing, thin for production
Two widely used public options:
stun.l.google.com:19302— Google, withstun1throughstun4as siblings.stun.cloudflare.com:3478— Cloudflare.
Their most valuable use is as a control. If Google's STUN server returns an srflx candidate from your network and yours does not, the fault is your server. If neither returns one, the fault is your network — outbound UDP is blocked, or a VPN is capturing the traffic. That single comparison resolves a large share of "STUN is broken" reports in under a minute.
As a production dependency they are weaker than they look. There is no uptime commitment, no support channel, no notice before an address changes, and no recourse when a corporate network blocklists the hostname. The cost argument for depending on them is also thinner than people assume: STUN is a stateless request-response with no media path, so running your own is cheap in a way TURN emphatically is not. A minimal coturn instance serving STUN only:
# /etc/turnserver.conf — STUN only, no relay, no credentials
listening-port=3478
external-ip=YOUR_PUBLIC_IP
# Refuse TURN allocations entirely; this box is a mirror, not a relay.
stun-only
That server holds no per-client state and answers a 20-byte packet with a small one. The full walkthrough, including the relay side, is in setting up a coturn server.
What STUN cannot do for you
The limits are sharp, and most STUN disappointment traces to expecting one of these:
- It does not forward traffic. A STUN server never sees your media. When peers cannot reach each other directly, STUN has nothing further to offer and you need a TURN relay.
- It does not open ports. The binding your outbound request created is the only thing that exists, and it expires. Inbound traffic from an address the NAT has not seen may still be dropped, depending on NAT behaviour.
- It does not help behind symmetric NAT. A symmetric NAT allocates a different external port per destination, so the address STUN reports is valid only for the STUN server itself and useless to your peer. This is the single most common reason a "working" STUN setup still fails to connect.
- A passing STUN test does not predict a passing TURN test. They are different protocols with different ports, different authentication, and different firewall exposure. Verify both.
Failure modes, and what each one means
| Symptom | Likely cause | Check |
|---|---|---|
| No srflx candidate at all | Outbound UDP blocked, VPN tunnelling, server down | Retry against a public STUN server from the same network |
Only .local host candidates visible | Normal — mDNS candidate privacy | Not a fault; look for whether srflx also appeared |
| srflx appears, direct connection still fails | Symmetric NAT on one or both sides | Confirm a TURN relay is configured and reachable |
| Works on your laptop, fails for users | Different network policy | Have them run the check on their own network |
| Gathering takes several seconds | Too many configured servers, or an unreachable one timing out | Reduce to one STUN server plus TURN |
A note on that fourth row, because it is the expensive one: STUN behaviour is a property of the network, not of your code. Testing from an office connection tells you almost nothing about a hotel Wi-Fi or a corporate VPN. The full triage sequence for connections that fail only in the field is in debugging WebRTC connections.
Check Your Own Servers
A STUN server is only useful if it returns a server-reflexive candidate. Point our free tester at your STUN URL to confirm it reflects your public IP and port back correctly.
Open the Free ICE Server Tester →