Your browser can detect your NAT type in 30 lines of JavaScript
Ever had a video call that connects fine on one network but refuses to establish peer-to-peer on another? Or a game where voice chat works for everyone except that one friend? Nine times out of ten, the culprit is the NAT sitting between you and the internet — and your browser already has everything needed to interrogate it.
The 30-line version
async function probeNat(stunUrl) {
const pc = new RTCPeerConnection({ iceServers: [{ urls: stunUrl }] });
pc.createDataChannel('probe'); // kick off ICE gathering
const srflx = await new Promise((resolve) => {
const found = [];
pc.onicecandidate = (e) => {
if (e.candidate && e.candidate.candidate.includes('srflx'))
found.push(e.candidate.candidate);
else if (!e.candidate) resolve(found); // null = gathering done
};
setTimeout(() => resolve(found), 5000);
});
pc.close();
return srflx;
}
const [a, b] = await Promise.all([
probeNat('stun:stun.l.google.com:19302'),
probeNat('stun:stun1.l.google.com:19302'),
]);
const portOf = (c) => c.split(' ')[5];
console.log(portOf(a[0]) === portOf(b[0])
? 'Cone NAT (predictable mapping)'
: 'Symmetric NAT (per-destination mapping)');
That's it. No extensions, no native code, no permissions prompt. Let me explain why this works.
ICE candidates are a free STUN client
When you create an RTCPeerConnection with STUN servers configured, the browser's ICE agent automatically sends STUN Binding Requests to each server. The responses come back to your JavaScript as ICE candidates — strings like:
candidate:8421630493 1 udp 1685987071 203.0.113.45 54321 typ srflx raddr 192.168.1.5 rport 54321
The fields that matter: 203.0.113.45 54321 is your server-reflexive address — how the STUN server saw you after your NAT translated the packet. The raddr/rport pair is your local address behind the NAT. The typ srflx marker tells you this candidate came from STUN rather than local interface enumeration (host) or a TURN relay (relay).
So the browser is effectively a STUN client already. We're just reading its output.
The core trick: compare mappings across servers
A NAT has to decide what external port to assign your traffic. There are two philosophies:
- Cone NAT: one internal address:port always maps to the same external address:port, regardless of destination. Ask two different STUN servers and both report the same external port.
- Symmetric NAT: each destination gets its own mapping. Ask two different STUN servers and you get two different external ports.
That's exactly what the snippet compares. Same port from both servers → cone family. Different ports → symmetric. This single distinction predicts most real-world connectivity pain: with symmetric NAT, the other peer can never guess which port to send to, so direct P2P hole punching fails and everything falls back to TURN relay (higher latency, server bandwidth).
What you can't tell from the browser alone
Honesty corner: this trick separates cone from symmetric, but it can't split the cone family further. Telling Full Cone apart from Address-Restricted or Port-Restricted Cone requires probing — having a second server send packets at you from an address or port you never contacted, and seeing if they get through. Browsers can't receive arbitrary unsolicited UDP, so that test needs infrastructure outside the page. Most browser-based NAT tests (including the one linked below) report the cone/symmetric verdict and treat the cone subtypes as one bucket. For diagnosing "why won't my P2P connect," that bucket is usually enough.
Why this matters beyond curiosity
Hole punching — the technique WebRTC, game netcode, and tools like Tailscale rely on — only works when at least one side has a predictable mapping. If both sides sit behind symmetric NATs, direct connection is essentially impossible and you must relay. Knowing your NAT type up front tells you whether to even attempt P2P, what TURN capacity to provision, and why that one user keeps filing "call won't connect" tickets from their mobile hotspot.
Wrapping up
The whole technique is: configure STUN servers → harvest srflx candidates → compare external ports across servers. The browser does the hard protocol work; you just read the results.
If you'd rather not wire up the comparison logic yourself, 17NAS NAT Test runs exactly this probe in your browser — it queries multiple STUN servers, compares the mappings, and reports your NAT type with the raw candidate details. Free, no sign-up, everything happens client-side.
Top comments (0)