WebRTC establishes peer-to-peer connections between web browsers. To do that, it uses a set of techniques known as Interactive Connectivity Establishment or ICE. ICE allows clients behind certain types of routers that perform Network Address Translation, or NAT, to establish direct connections. (See the WebRTC glossary entry for a good introduction.) One of the first problems is for a client to find what its public IP address is. To do so, the client asks a STUN server for its IP address.
NATs are boxes (physical or virtual) that connect our local private networks to the public internet. They do so by translating the internal IP addresses we use to public ones. They work differently from one another, which ends up requiring WebRTC to rely on both STUN and TURN in order to connect calls. For background on these, check out some of our past posts on this topic like this one and this one.
While watching the “NAT Traversal” lesson of Tsahi Levent-Levi’s WebRTC Architecture course I (re)learned the definition of a symmetric NAT from this slide:
If you ask two different STUN servers for your public IP address a symmetric NAT will give you the same IP address (hopefully) but different ports. We need a TURN server to get out of this mess as in general, symmetric NATs don’t allow the establishment of a direct connection.
A couple years ago I discussed some techniques for improving connectivity rates during a Tokbox talk (video and slides). Tsahi’s talk made me wonder if one could also test for the symmetric NAT scenario where you get a different port returned for each STUN binding?
After some tests I verified that yes, yes we can.
The first step is to ask two STUN servers for our IP address by passing it the following configuration:
1 2 3 4 5 6 |
var pc = new RTCPeerConnection({iceServers: [ {urls: ‘stun:stun1.l.google.com:19302’}, {urls: ‘stun:stun2.l.google.com:19302’} ]}) |
We create a data channel to make the peer connection only generate a single local candidate.
1 |
pc.createDataChannel("webrtchacks") |
Then we look at the onicecandidate event and parse any candidates we get (using a helper function from my SDP module). If the candidate is of type srflx we note both the port – the port after translation by the NAT device – and the relatedPort – the port before translation.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
pc.onicecandidate = function(e) { if (e.candidate && e.candidate.candidate.indexOf('srflx') !== -1) { var cand = parseCandidate(e.candidate.candidate); if (!candidates[cand.relatedPort]) candidates[cand.relatedPort] = []; candidates[cand.relatedPort].push(cand.port); } else if (!e.candidate) { if (Object.keys(candidates).length === 1) { var ports = candidates[Object.keys(candidates)[0]]; console.log(ports.length === 1 ? 'cool nat' : 'symmetric nat'); } } }; |
Last but not least we call createOffer and setLocalDescription to start gathering:
1 2 |
pc.createOffer() .then(offer => pc.setLocalDescription(offer)) |
Once we’re done gathering candidates we get an icecandidate event with event.candidate not being set. We then look at the candidates we got. There are only 3 options:
- If we only got a single candidate, the browser did not want to bother us with the response from the second STUN server as it contained the same port. Which means we are not behind a symmetric NAT.
- If we got two candidates with the same relatedPort and different ports then we are behind a symmetric NAT.
- And if we did not get a srflx candidate at all UDP is blocked. This means we need a TURN/TCP or TURN/TLS server (like this one).
How do you test it? The personal hotspot on the iPhone is doing symmetric NAT which helps a lot. Test it online in this fiddle:
Does the knowledge that we’re behind a symmetric NAT help us? Probably not much. We can now build a NAT type detector. I am not sure if this has much practical use since WebRTC’s ICE mechanism is designed to find a connection without you worrying about the details, but who knows. Nonetheless, it is a fun hack that will help you understand the mysteries of NAT traversal in WebRTC.
{“author”: “Philipp Hancke“}
Aswath Rao says
As you point out the cost of symmetric NAT (at BOTH the ends) is the requirement of a TURN. At various times, people have proposed different ways to avoid use of a relay server. For example, https://tools.ietf.org/id/draft-takeda-symmetric-nat-traversal-00.txt proposes some heuristics to predict the rule the NAT uses for port mapping, thereby avoiding use of a relay. Like wise https://tools.ietf.org/html/rfc5780 suggests some other scheme.
As far as I know, these efforts didn’t get much traction possibly because most are service providers and they see other benefits in routing the traffic through their infrastructure.
Octavian says
I remember doing 2 way video/audio using Flash and FMS or Red5. All the traffic was relayed. We just estimated the usage and moved on.
Octavian says
>The personal hotspot on the iPhone is doing symmetric NAT which helps a lot.
Out of curiosity I’ve connected an iMac to the Personal Hotspot of an iPhone 6S running iOS 10.3.2. I ran the jsfiddle and I only got “normal nat”.
Maybe in your case it’s the 3G/4G network that’s going out to the Internet through a symmetric NAT ?
Mihály Mészáros says
It is great article thanks fippo & highly appreciated!
In the fiddle there is a copy paste typo:
One server is used twice, and so the fiddle gives back “normal nat” on consloe, even in case of symmetric NAT.
Please correct this in the fiddle:
{urls: ‘stun:stun2.l.google.com:19302’},
{urls: ‘stun:stun2.l.google.com:19302’}
to
{urls: ‘stun:stun1.l.google.com:19302’},
{urls: ‘stun:stun2.l.google.com:19302’}
( Or it was planned typo? Just a quick check that we really understand the article? 🙂 )
Thanks!
Misi
Philipp Hancke says
Fixed, thanks Misi!
Misi says
In the example Firefox 56 gives back false positive (symetric nat).
My proposal is to replace in the fiddle
candidates[cand.relatedPort].push(cand.port);
With this simple udp protocol check.
if (cand.protocol == “udp”) {
candidates[cand.relatedPort].push(cand.port);
}
michau says
I’ve got two times ‘srflx’. What’s going on?
Philipp Hancke says
Hard to say without seeing the candidates. Maybe ipv4 and ipv6?
michau says
RTCIceCandidate: candidate:1171761481 1 udp 1685987071 188.146.38.178 15017 typ srflx raddr 192.168.8.123 rport 55252 generation 0 ufrag OCBk network-id 1 network-cost 10 stun:64.233.165.127:19302″
RTCIceCandidate: candidate: 1171761481 1 udp 1685987071 188.146.38.178 15023 typ srflx raddr 192.168.8.123 rport 62609 generation 0 ufrag OCBk network-id 1 network-cost 10 stun:64.233.165.127:19302″
Harvinder says
foundation property (in your case 1171761481) is a string which uniquely identifies the candidate across multiple transports. It seems that are actuall the same candidates
esser50k says
On a Mac I get normal nat on Chrome and Firefox.
But then I get ‘symmetric nat’ on Safari..
All these results are consistent regardless if it is my home wifi on iPhone hotspot.
I don’t really understand these results. But it really does happen that Safari won’t establish a connection while other browsers do.
SP says
That’s because Safari probably had iCloud Private Relay enabled. You get “symmetric” with private relay and “normal” without (assuming you have a normal NAT router).