UDP Hole Punching With STUN
<< Home

UDP Hole Punching With STUN

Photo by Iswanto Arif on Unsplash! Let’s get started!


STUN is a neat NAT (Network Address Translation) traversal technique. It is also very, very useful when it comes to P2P connections (even though it has its limitations). But why do we need them anyway? Well first, we gotta see what NAT is.

So, back in the good old days, when IPs are still abundant, everybody has an IP on the Internet, and all of them are directly routable:


Nobody has a problem, and everyone was happy.

But soon, evil mega corps rose, and the Internet become super popular. IPs have become a treasure or whatever, and was squabbled over (I was just making this up). Anyway, NAT was introduced, and now whenever the host behind a NAT needs to send data, the data needs to go through the router first. The router will then pack it up, and pretend the data was actually sent from it.


As Machine C does not actually have a public IP address, When the data was sent from Machine C, say to Machine B, the data will first go to router. The router then will replace the origin IP and the port in the packet ( to the router’s IP (, and (perhaps) a random port, and open the port in order to receive response. because if it doesn’t do that, the Internet won’t be able to respond (as the origin IP is behind a NAT). In this way, Machine C will think the data comes from the router, instead of Machine C. So when Machine C sends the response, it will send the response to the router. The router then will route the data according to the port. When it finds the port matches Machine B, it will then forward data to it. It’s really useful: saves IP, creates intranet, yada yada yada. But you know, not so much for P2P connection.

The problem gets worse when both machines are behind a NAT. As that means there will be practically no chance of them to connect to each other. And this is exactly the moment when all sorts of hole punching method comes to shine!

Double NAT


So now, we are going to talk about STUN, because that’s the only method I know right now. Yeah I know, lame, right? But it’s still cool! So, to start, we need a public server which has direct connection to the Internet.

Server C

And when the process begins, both A and B sends a datagram packet to server C.

As the data was sent out from A, it will first go through the router (duh), and the router, in order to receive data from the Internet (otherwise how will A know there is a reply?), will bind a random port and start listening for data. The data sent from A will then be forwarded from that port, and into server C. The same goes for B. So now C will have both router’s IP address, and their respective port. And when C sends data through that port, the data will be forwarded by the router and eventually reach A/B. It’s kinda like an indirect connection, or a proxy.

Indirect connection

And that’s cool and all. Now what?

Well now, C shares the data with A and B! C sends the IP:PORTA to B, and IP:PORTB to A. Now A knows how to contact B and B knows how to contact A! But alas, it’s not that easy. Assuming A is sending a message to B, which will go through Router 2 first, Router 2 will cut that message. as B only sent message to C, Router 2 will assume B only expects message from C. Message from A will be simply discarded.


However, this still needs to be done. And by that I mean after A getting the info of B and B getting the info of A, A still needs to send a direct message to B. Why? That message is gonna be discarded! Well, because as now A has sent a message to B, Router 1 will be starting to listen for incoming replies from Router 2! Router 2 is now officially in Router 1’s NAT table. Horray! And that means messages coming from B can now reach A! Not the other way around though. So what’s next? Next, B sends a message to A. Now Router 1 is in Router 2’s NAT table. A P2P connection is now officially established. You can do voice chats or whatever now.



  1. A and B binds themselves to a random local port
  2. A and B sends a random packet to C
  3. C gets their IP:PORT, and share them with A and B
  4. A sends a packet to B, which will never be received. However by doing so, A is now picking up messages from B.
  5. B sends a packet to A. A probably acknowledges it, and BOOM! P2P connection!
  6. Profit?

Code Examples

Here’s an incredibly easy code example you can tamper with. It really doesn’t implement STUN itself, just a simple UDP sending utility. It is so easy you can write one on your own!

//  main.cpp
//  UDP.Manipulator
//  Created by 42yeah on 2020/4/6.
//  Copyright © 2020 aiofwa. All rights reserved.

#include <iostream>
#include <random>
#include <thread>
#include <cstring>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#ifndef SERVER
#define PORT distrib(dev)
#define PORT 12345

bool running;

void receive(int sock) {
    sockaddr_in sin;
    socklen_t slen = sizeof(sin);
    while (running) {
        char buf[512] = { 0 }; // No bigger than the MTU
        ssize_t len = recvfrom(sock, buf, (int) sizeof(buf), 0, (sockaddr *) &sin, &slen);
        if (len < 0) { continue; }
        char *ip = inet_ntoa(sin.sin_addr);
        std::cout << "Received: " << ip << ":" << ntohs(sin.sin_port) << " [" << len << "]: " << std::string(buf) << std::endl;

sockaddr_in sinify(char *addr) {
    char *ip = strtok(addr, ":");
    char *port = strtok(nullptr, ":");
    sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_port = htons(atoi(port));
    sin.sin_addr.s_addr = inet_addr(ip);
    return sin;

int main(int argc, const char * argv[]) {
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    std::random_device dev;
    std::uniform_int_distribution<> distrib(49152, 65535);
    int port = PORT;
    running = true;

    sockaddr_in sin;
    sin.sin_port = htons(port);
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = INADDR_ANY;
    int res = bind(sock, (sockaddr *) &sin, sizeof(sin));
    std::cout << "The binding result is " << res << ". Port bound at " << port << std::endl;
    std::thread receiver(receive, sock);
    while (running) {
        std::cout << "> ";
        std::string in;
        std::getline(std::cin, in, '\n');
        const char *raw = in.c_str();
        char *copy = new char[512];
        std::memcpy(copy, raw, in.length() + 1);
        char *addr = strtok(copy, " ");
        char *msg = strtok(nullptr, " ");
        sockaddr_in sin = sinify(addr);
        std::string str(msg);
        std::cout << "Sending message [" << str.length() << "] " << msg << " to " << inet_ntoa(sin.sin_addr) << ":" << ntohs(sin.sin_port) << std::endl;
        sendto(sock, msg, str.length(), 0, (sockaddr *) &sin, sizeof(sin));
        delete[] copy;
    return 0;

You can play this by compiling it. One size fits all! When you are compiling the server version, remember to add -DSERVER as compiling argument. Also you need -lpthread on *nix. And --std=c++11, too. As I am using Berkeley socket, you gotta implement Winsock version on your own. So how does this work? Well, you compile it, and then you run it, and you can send datagram packets by inputting <IP:PORT> <SPACE-LESS-MESSAGES>. Such as:

123.456.78.9:12345 helloworld!

and a datagram packet will be sent to there. When this application picks up datagram, it will display its source ip and port. Try it out on your own!


STUN is an old-timey, yet elegant way to exploit the NAT. It might not work as expected nowadays, things like symmetric NATs will bind to a random new port whenever a new message was sent from the router. There are advanced hole punching methods too, such as ICE. It is also incredibly useful in multiplayer game programming. So hold on to it! And until next time, cheerio!


  1. STUN, Wikipedia
  2. UDP Hole Punching, Wikipedia
  3. ICE, Wikipedia
  4. VoIP, a voice chat protocol often utilises hole punching techniques
  5. Multiplayer Game Programming, a very neat book
  6. Peer to peer, Wikipedia