Snowflake uses STUN (RFC 5780) to traverse NATs and allow clients to connect to proxies that can be run on volunteer home networks: in browser extensions, on mobile phones, or as a command line program on a home desktop.
Types of NATs
Not all NAT and firewall configurations are the same. Two specific peers are not necessarily guaranteed to be able to form a P2P connection using STUN. There are two different factors that determine whether the NATs are two peers are compatible: The NAT mapping behaviour and the NAT filtering behaviour.
NAT Mapping Behaviour
A NAT's mapping behaviour determines how an internal (ip, port) pair is mapped to an external (ip, port) pair. The internal pair is the address and port assigned to the application on the user's machine. The external pair is the address and port that are visible to the rest of the internet.
There are many different ways to perform this mapping, but for simplicity we will consider the following three general types:
- Address-independent (AI) mapping: internal addresses are mapped to the same external address regardless the remote (ip, port) pair they are connecting to, or there is no NAT in place
- Address-only-dependent (AO) mapping: internal addresses are mapped to a different external address depending on which remote ip address they are connecting to
- Address-and-port-dependent (AP) mapping: internal addresses are mapped to a different external address depending on which remote ip and port they are connecting to
NATs that are either address only dependent or address and port dependent are typically referred to as symmetric NATs.
NAT Filtering Behaviour
Filtering behaviour can also be generalized into the same three types:
- Address-independent (AI) filtering: incoming connections are not filtered
- Address-only-dependent (AO) filtering: incoming connections are filtered if there was not a corresponding outgoing connection to the remote IP address
- Address-and-port-dependent (AP) filtering: incoming connections are filtered if there was not a corresponding outgoing connection to the remote IP address and port pair
The following chart shows which NATs are compatible with each other. Columns and rows show the NAT type in the form: (mapping, filtering). Pairwise compatible NATs are marked with a checkmark (
Most WebRTC applications require matching a specific peer to a specific client (as is the case for a video call), and are left with no choice but to proxy the traffic between two incompatible peers through a centralized server. TURN (RFC 5766,RFC 6062) is a common way to accomplish this.
However, using centralized TURN servers isn't ideal for our censorship-resistance use case of WebRTC. A single TURN server or even a small set of TURN servers can be easily targeted and blocked by a censor. Using existing public infrastructure or domain fronting to discourage blocking through collateral damage would be expensive, potentially damaging to other applications that rely on those servers, and generally doesn't scale the way we want Snowflake to be able to scale.
Fortunately, we don't have to match specific clients to specific proxies. We can match a client with any available proxy, meaning we can choose from the set of polling proxies one that has a NAT compatible with the client.
The broker is responsible for sorting proxies into buckets, according to which types of NATs they are compatible with. When a client polls, the broker picks a proxy out of a compatible bucket to give to the client.
In practice, we don't need 9 buckets for each possible combination of mapping and filtering behaviour. We simplify this to 2 buckets:
- unrestricted: proxies in this bucket will work with all clients, even those with the most restrictive symmetric NATs and filtering behaviour. Only (AI,AI) and (AI,AO) proxies belong in this bucket.
- restricted: all proxies that aren't (AI,AI) or (AI,AO) are restricted. Most1 of the proxies in this bucket will work for clients without symmetric NATs.
Determining NAT behaviour
We determine the NAT behaviour of clients by using the tricks in RFC 5780. We worked with pion to implement the necessary functionality in their STUN library https://github.com/pion/stun. For standalone proxies written in Go, we use the same method.
To solve this, we set up a probe service that allows proxies to test their NAT compatibility by attempting to connect to symmetrically NAT'd WebRTC peer. If it is able to successfully open a datachannel with this peer, we classify it as an unrestricted proxy. If the proxy times out without opening the datachannel, we sort it into the restricted bucket.
there is an edge case here where clients with port-dependent filtering behaviour (AI,PD) won't work with symmetrically mapped proxies, i.e. proxies with (AO,X) or (AP,X) behaviours. However, symmetric mapping is relatively rare. At the time of writing this post, (AI,PD) clients work with over 80% of restricted proxies.