UDP Firewall Traversal: STUN and Hole Punching
The most surprising thing about UDP firewall traversal is that it often relies on the firewall not being stateful for UDP, or on a specific, allowed state being created by an external service.
Let’s see this in action. Imagine two clients, Alice and Bob, behind different NAT devices, wanting to communicate directly via UDP. Alice’s application starts by sending a UDP packet to Bob’s known public IP and port. Bob’s NAT, however, has no record of an outgoing connection to Alice’s IP/port, so it drops the packet. Bob’s application never sees it.
This is where STUN (Session Traversal Utilities for NAT) comes in. Alice’s application, before trying to connect to Bob, sends a STUN binding request to a public STUN server. This STUN server, being on the public internet, receives the request and, importantly, records Alice’s public IP address and the source port her NAT assigned for this outgoing connection. The STUN server then sends a STUN binding success response back to Alice. This response contains Alice’s perceived public IP and port. Crucially, this outgoing UDP packet from the STUN server to Alice’s NAT creates a temporary state in Alice’s NAT: "Allow incoming UDP from STUN server’s IP/port to Alice’s internal IP/port."
Now, Alice knows her public IP/port. She can tell Bob (via a signaling server, perhaps) what they are. Bob, similarly, might have used a STUN server to discover his own public IP/port.
The "hole punching" part is the magic. Alice sends a UDP packet to Bob’s public IP and port (which she learned, possibly from Bob telling her his STUN-discovered IP). Bob’s NAT receives this packet. If Bob’s NAT is a "cone NAT" (which is common), it sees an outgoing UDP packet from Bob’s internal IP/port to Alice’s public IP/port. It creates a state: "Allow incoming UDP from Alice’s public IP/port to Bob’s internal IP/port."
Now, for the punchline: Bob’s application needs to send a packet back to Alice. Bob’s application sends a UDP packet to Alice’s public IP and port. Alice’s NAT, having just received a packet from the STUN server (or potentially from Bob if Bob sent a packet first), now has a temporary state allowing incoming UDP from that specific source. It allows Bob’s packet through. The same logic applies if Bob sent his packet first and Alice’s NAT created the state.
The mental model is that NAT devices, for UDP, are often not as strict as they are for TCP. They often create a temporary mapping based on the first outgoing UDP packet they see. If a packet arrives from the same public IP and port that an outgoing packet was sent to, the NAT assumes it’s part of a "conversation" and lets it through. STUN helps clients discover their public endpoint so they can initiate this process and inform the other party.
What most people don’t realize is that the success of hole punching is highly dependent on the type of NAT. "Symmetric NATs" are the killer. A symmetric NAT assigns a different public port for each distinct destination IP and port. So, if Bob sends a packet to Alice’s STUN server, his NAT might open a port. But if Bob then sends a packet directly to Alice’s public IP/port, his symmetric NAT might assign a completely different public port, and Alice’s NAT will never have a state to allow it. STUN can detect this (by sending requests to multiple STUN servers or to the same server multiple times with different destinations), but it cannot overcome a symmetric NAT.
The next hurdle you’ll face is handling symmetric NATs, which might require relay servers.