How to use STUN in applications

This document has been reviewed for maemo 3.x.

This tutorial shows how to use the libjingle API to create peer-to-peer connections between parties that are behind NAT routers.

Network Address Translation (NAT)

NAT routers were born because of the imminent IP address exhaustion. The currently most used Internet address, called IPv4, is 32 bits wide, which means around 4 billion address available.

4 billion addresses seem to be an abundant resource, but not all addresses can be assigned to hosts, as not all telephone number permutations can be assigned to users, since IP addresses also carry routing information, analog to the prefix of a telephone number that identifies country, region, city...

The definitive solution for the problem is to increase the address length, that is what IPv6 does, by offering 128-bit addresses. But IPv6 would take (and it is taking) time to be deployed, so an intermediate solution was found: NAT - Network Address Translation.

A NAT router acts like a switchboard between the public Internet and a private network. The NAT router needs to have at least one valid Internet address in order to be "seen" by the rest of the Internet. The computers in the private network have private address in the ranges 10.0.0.0/8, 192.168.0.0/16 or 172.16.0.0/12, that are not routable in the Internet.

When a private computer e.g. address 10.0.0.1 tries to connect a public server e.g. 200.215.89.79, the network packet must pass through the NAT router. The NAT router knows that the source address 10.0.0.1 is not routable. So the NAT router replaces 10.0.0.1 with its own valid address (e.g. 64.1.2.3) and sends the packet forward.

The remote server 200.215.89.79 will "see" a connection coming from the 64.1.2.3 host, and will reply to that host.

When the response packet comes to NAT router, it must know somehow that this packet is not for itself, it is for the computer in the private network. Once the NAT router relates the packet coming from the blue with the active NAT'ed connections, it can replace the destination address from 64.1.2.3 to 10.0.0.1 and deliver the packet in the private network.

In more technical depth, NAT is possible because absolutely every network connection in the Internet has a unique tuple built from the following values:

  • Client address (the host that initiates the connection)
  • Server address (the passive side that receives the initiation packet)
  • Client port number
  • Server port number
  • The transport protocol e.g. TCP, UDP, SCTP...

If the NAT router replaces the client address by its own, but also replaces the client port number when necessary to avoid clash with another active connection, the uniqueness of each connection is not broken. NAT allows for a virtually unlimited number of computers in a private network to access the Internet via only one NAT router with only one public IP address.

NAT problems

NAT is not without some disvantages. First, the NAT router needs to keep connection states in memory, which partially denies a cornerstone Internet philosophy ("dumb routers, smart hosts"). If a NAT router is reset, all connections will be terminated, while a regular router could be reset without harming any connection.

Second, the hosts in private network cannot easily offer a service to the public Internet, that is, these hosts cannot be the "passive" side in connections, since the initiation packet will come to the NAT router and it will have no related connection in its memory.

A partial solution for that problem is to open a "hole" in the NAT for specific ports, for example any connection to the port 8000 of the NAT router should be redirected to the machine 10.0.0.2 port 80. That works, but it does not scale (the number of ports is limited, and some protocols work only on a very specific port number) and demands configuring the NAT router.

Since the bulk of the Internet traffic is HTTP and initiated from the private network, NATs are fine for most users.

NAT peer-to-peer circumvention techniques

The most problematic services to deploy in presence of NATs are of peer-to-peer (P2P) style, when two clients of the service make direct connections to each other, without sending data through an intermediate server. This is the case of SIP-based VoIP, most P2P file sharing networks etc.

If one of the P2P parties has a routable IP address, the problem is solved: the party behind the NAT router must initate the connection, and that's it. The remaining problematic case (unfortunately the most common one) is when both P2P parties are behind NATs.

Several techniques have been proposed to solve this case. The most elegant solutions require both software updates in the NAT router itself and explicit support in the client software. It is the case of UPnP IGD (Internet Gateway Device) protocol, where a host can explicitely request a "hole" to be open in the NAT router, so a service is acessible from the Internet.

IGD is nice and has been enjoying good support from NAT router vendors, but it is still not ubiquitous and has a major flaw: if there are two or more NAT routers in the packet traffic way (a rather common situation), IGD ceases to be effective.

It is opportune to remember that none of the NAT circumvention techniques, not even the most elegant, solve completely the problem. There still must be a signaling protocol for the parties to exchange the connection parameters. In other words, It does not suffice to be able to open a "hole" in the NAT router; there must be a way to communicate the address and port of the hole to the remote party. A pure P2P service can only be achieved when all parties possess public addresses, which is expected to happen when IPv6 is widely deployed.

Another way is simply to open a NAT hole via router configuration. Most home/SOHO routers allow this configuration to be easily done. It is a very popular technique among gamers (some routers even label the NAT holes as "game ports"). The main advantage of this technique is no need of explicit NAT-piercing support in the client software itself. The software must only be "well-behaved", not trying to guess the local IP address by itself (which would return a private address instead of the router public address).

The main disvantage of manual configuration of NAT holes is that they are, well, manual: one explicit administrative intervention is needed for each new protocol. It it not good enough if the user has no access to NAT configuration (the most common case in collective and corporate networks).

STUN, TURN and ICE protocols

There have been proposed NAT circumvention techniques that do not demand router software updates. STUN (Simple Transversal of UDP through NATs) is the main one. STUN exploits the connectionless nature of UDP, as well as a security weakness of some NAT implementations. The technique is bound to UDP features, so STUN can not be used for e.g. TCP connections.

The STUN protocol demands a STUN server with a well-known public IP address in the Internet. STUN stores the private address/port in an UDP payload and sends the packet to the STUN server. If packet goes through a NAT router, the address/port will be changed in the IP header -- but not in the payload.

By comparing the payload with the IP header, and sending back the results to the client, STUN can deduce if there are any NAT routers in the way. If there are none, the host knows that it can directly receive connections from parties behind NATs.

Some NATs have poor implementations that, in conjunction with STUN, allow for incoming UDP flow. The "full cone" NAT always translates the same source address/source port tuple to the same NAT router port; this relieves the NAT from storing full connection state. Any packet coming from Internet to that router port will be delivered to the host in the private network, no matter it came from.

This allows for the private host to easily "punch a hole" in the NAT router. The first packet that opens the hole goes to STUN server. In turn, the private host learns from the STUN server which is the IP address and port number of the hole. These parameters are sent to the remote peer via signaling protocol. The remote peer can then send UDP data directly.

Unfortunately, most NAT routers are not that naive; they are "symmetric", that is, they store full connection state and do not allow incoming packets from anyone but the party that was first contacted (in our case, the STUN server). In this scenario, STUN is not enough to allow P2P communication in presence of NAT.

TURN (Traversal Using Relay NAT) comes for the rescue. TURN employs the same protocol as STUN, but the TURN server acts as a relayer in which both parties behind NATs can connect to. Since the relay server adds latency and needs to be maintained (and its bandwidth costs money), TURN should be used only as a last resort.

ICE (Interactive Connectivity Establishment), a draft specification that is employed by Google Talk, is not a protocol by itself; it is a collection of techniques like STUN and TURN that finds all possible ways to establish a P2P connection and selects the best one.

ICE works by finding all possible P2P connection candidates and sending this data to the remote peer via a signaling protocol (which is not specified by ICE, so the mechanism can be employed by any existing signaling protocol). Both parties do that simultaneously, so there are candidate messages flowing in both directions. When the parties agree on the best way to make the P2P connection, they can begin to exchange data directly.

The biggest advantage of STUN, TURN and ICE is that they do not depend on specific support by the NAT routers, and ICE will work even if there are several routers in the network path. The downside is the need of public STUN and TURN (relay) servers. This disvantage is dilluted to some extent by the fact that every P2P service demands a signaling server anyway, so the same entity that offers the signaling will certainly offer the auxiliary STUN/TURN services.

Another minor disvantage of ICE is that the signaling protocol will have to accomodate the "connection candidate" message, either by a explicit provision in the protocol (e.g. XMPP) or by some kludge.

NAT transversal API in maemo

In maemo platform, the developer does not need to worry about protocols or anything. maemo includes the libjingle library (the same as Google Talk) which offers an API for P2P connections.

Nothing better than living examples to show how the maemo developer can take advantage on this API. Our example P2P client is very simple: it requests a service at random times and also processes requests from other P2P clients. The "service" is nothing more than adding 2 byte values.

As already stated, every P2P server must have a signaling protocol over which the parties can exchange initial P2P connection parameters, so we need to build a very simple server as well as the signaling protocol. Since it is just an example, our server architecture does not attribute IDs for the clients, and therefore can handle only two simultaneous clients.

The signaling protocol is very simple and has only one message: the connection candidate that one peer sends to the other. The message is simply forwarded to the other peer. Apart from encoding/decoding, these messages are completely handled by libjingle, so we don't need to understand them in depth.

Once the connection parameters have been exchanged via the signaling server, the P2P connection happens and the parties communicate directly without any further signaling. Albeit very simple, this protocol simulates all the basic steps of every real-world P2P service.

If you are interested in using XMPP/Google Talk as the signaling service, Libjingle source also contains examples of P2P communication that employ XMPP/Google Talk accounts and servers.

Example: P2P client

Follows a C++ example of P2P client based on Libjingle APIs. It is the smallest possible demonstration of NAT-piercing capability, so it does not handle network errors and overflows very well. A production implementation must improve in these directions.

First, there is some boilerplate code: includes and prototypes.

#define POSIX
#define SIGSLOT_USE_POSIX_THREADS

#include <libjingle/talk/base/thread.h>

#include <libjingle/talk/base/network.h>
#include <libjingle/talk/base/socketaddress.h>
#include <libjingle/talk/base/physicalsocketserver.h>
#include <libjingle/talk/p2p/base/sessionmanager.h>
#include <libjingle/talk/p2p/base/helpers.h>
#include <libjingle/talk/p2p/client/basicportallocator.h>

#include <libjingle/talk/p2p/client/socketclient.h>

#include <string>
#include <vector>

#include <sys/socket.h>
#include <netinet/in.h>

#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <sys/time.h>
#include <time.h>

#include <signal.h>

void signaling_init(const char* ip, unsigned int port);
int signaling_wait(unsigned int timeout);
void signaling_sendall(const char* buffer, unsigned int len);

SocketClient* socketclient_init(const char *stun_ip, unsigned int stun_port, 
				const char *turn_ip, unsigned int turn_port);

char* socketclient_add_remote_candidates(SocketClient *sc, char* buffer); 
void socketclient_add_remote_candidate(SocketClient *sc, const char *candidate);

bool socketclient_is_writable (SocketClient *sc);

void socketclient_send(SocketClient *sc, const char *data, unsigned int len);

void randomize();

For the sake of simplicity, we keep some data in global variables. A production implementation would probably move those data into objects.

The p2p_state shows whether the P2P connection is up or down. signaling_socket is a TCP socket that allows data exchange via the signaling server before the P2P connection is up. main_thread contains a libjingle's Thread object; libjingle is itself multithreaded and employs one thread per P2P connection.

bool p2p_state = false;
int signaling_socket = -1;

cricket::Thread *main_thread = 0;

This is the main program loop. It sets up the signaling connection, forwards signaling data to LibJingle until the P2P connection is up. The P2P connection is simply used to send bytes at random intervals. If the P2P connection breaks, the loop returns to signaling phase. The program only stops when killed or when some unexpected error occurs.

Note that we call main_thread->Loop(10) from time to time. In a "real" application, we would have this method called on idle time (e.g. via GLib's g_idle_add().

We have some IP addresses hardcoded: signaling server, STUN server (if any) and TURN server (if any). Please update those addresses for your particular environment.

int main(int argc, char* argv[])
{
	signal(SIGPIPE, SIG_IGN);
	randomize();
	
	// P2P signaling server
	const char* signaling_ip = "200.184.118.140";
	int signaling_port = 14141;

	// STUN server if any, set to NULL if there are none
	const char* stun_ip = "200.184.118.140";
	// const char* stun_ip = 0;
	unsigned int stun_port = 7000;

	// TURN server if any, set to NULL if there are none
	const char* turn_ip = "200.184.118.140";
	// const char* turn_ip = 0;
	unsigned int turn_port = 5000;

	signaling_init(signaling_ip, signaling_port);

	SocketClient* sc = socketclient_init(stun_ip, stun_port, turn_ip, turn_port);
	
        sc->getSocketManager()->StartProcessingCandidates();

	while (1) {
		char buffer[10000];
		char *buffer_p = buffer;
		char *buffer_interpreted = buffer;

		while (! p2p_state) {
			main_thread->Loop(10);
	
			if (! signaling_wait(1)) {
				printf("-- tick --\n");
				continue;
			}
	
			int n = recv(signaling_socket, buffer_p, sizeof(buffer) - (buffer_p - buffer), 0);
			if (n < 0) {
				printf("Signaling socket closed with error\n");
				exit(1);
			} else if (n == 0) {
				printf("Signaling socket closed\n");
				exit(1);
			}
			buffer_p += n;
			buffer_interpreted = socketclient_add_remote_candidates(sc, buffer_interpreted);
		}
	
		// P2P connection is up by now.
		
		while (p2p_state) {
			// sends a byte via P2P connection
			unsigned char data = random() % 256;
			socketclient_send(sc, (char*) &data, 1);
			sleep(random() % 15 + 1);
			main_thread->Loop(10);
		}

		// P2P connection is broken, restart handling connection candidates
	}
}

Seeds the random() with some "random" value, otherwise the two peers may end up with exactly the same bytes and time intervals during P2P data exchange.

void randomize()
{
	struct timeval tv;
	struct timezone tz;
	gettimeofday(&tv, &tz);
	srandom(tv.tv_usec);
}

This function creates the signaling socket -- just a boring and ordinary TCP connection to the P2P signaling server.

void signaling_init(const char* ip, unsigned int port)
{
	struct sockaddr_in sa;

	signaling_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	
	bzero(&sa, sizeof(sa));
	sa.sin_family = AF_INET;
	sa.sin_port = htons(port);
	inet_aton(ip, &(sa.sin_addr));

	if (connect(signaling_socket, (struct sockaddr*) &sa, sizeof(sa)) < 0) {
		printf("Error in signaling connect()\n");
		exit(1);
	}
}

This function waits for something to happen with the singaling socket, until the specified timeout. It is important that the timeout is not too large, since the P2P connection may have been brought up meanwhile.

int signaling_wait(unsigned int timeout) {
	fd_set rfds;
	struct timeval tv;
	int retval;

	FD_ZERO(&rfds);
	FD_SET(signaling_socket, &rfds);

	tv.tv_sec = timeout;
	tv.tv_usec = 0;

	retval = select(signaling_socket+1, &rfds, NULL, NULL, &tv);
	if (retval == -1)
		printf("error in select()");

	return (retval > 0);
}

Libjingle is C++ and employs a signal architecture to notify the client application about changes in P2P connection status. These two classes accomodate the methods that will be called back when something happens.

Since we are not using XMPP as the signaling protocol, we need to provide the signaling protocol handling by ourselves, so all signaling handling is carried out separatedly from those signals.

class SignalListener1 : public sigslot::has_slots<>
{
private:
	SocketClient *sc;

public:
	SignalListener1(SocketClient *psc);
	void OnCandidatesReady(const std::vector<Candidate>& candidates);
	void OnNetworkError();
	void OnSocketState(bool state);
};

class SignalListener2 : public sigslot::has_slots<>
{
private:
	SocketClient *sc;

public:
	SignalListener2(SocketClient *psc);
	void OnSocketRead(P2PSocket *socket, const char *data, size_t len);
};

SignalListener1::SignalListener1(SocketClient* psc)
{
	sc = psc;
}

SignalListener2::SignalListener2(SocketClient* psc)
{
	sc = psc;
}

void SignalListener1::OnNetworkError()
{
	printf ("Network error encountered at SocketManager");
	exit(1);
}

The first signal callback method. It is called when the P2P socket changes state. We update the p2p_state global variable with the reported state, and this will drive our main loop behavior.

void SignalListener1::OnSocketState(bool state)
{
	printf("Socket state changed to %d\n", state);
	p2p_state = state;
	if (state) {
		printf("Writable from %s:%d to %s:%d\n", 
			sc->getSocket()->best_connection()->local_candidate().address().IPAsString().c_str(),
			sc->getSocket()->best_connection()->local_candidate().address().port(),
			sc->getSocket()->best_connection()->remote_candidate().address().IPAsString().c_str(),
			sc->getSocket()->best_connection()->remote_candidate().address().port());
	}
}

This function packages all P2P socket creation bureaucracy. It creates the socket object, the socket listeners (whose classes have been defined above by us) and connects the signal callbacks.

SocketClient* socketclient_init(const char *stun_ip, unsigned int stun_port, 
                                const char *turn_ip, unsigned int turn_port)
{
	cricket::SocketAddress *stun_addr = NULL;
	if (stun_ip) {
		stun_addr = new cricket::SocketAddress(std::string(stun_ip), stun_port);
	}

	cricket::SocketAddress *turn_addr = NULL;
	if (turn_ip) {
		turn_addr = new cricket::SocketAddress(std::string(turn_ip), turn_port);
	}

	cricket::PhysicalSocketServer *ss = new PhysicalSocketServer();
	main_thread = new Thread(ss);
	cricket::ThreadManager::SetCurrent(main_thread);

	SocketClient *sc = new SocketClient (stun_addr, turn_addr);

	// Note that signal connections pass the SignalListener1 object as well as the
	// method. Since a new SocketListener1 is created for every new SocketClient,
	// we have the guarantee that each SocketListener1 will be called back only
	// in behalf of its related SocketClient.

	sc->sigl1 = new SignalListener1(sc);
	sc->sigl2 = new SignalListener2(sc);
	sc->getSocketManager()->SignalNetworkError.connect(sc->sigl1, &SignalListener1::OnNetworkError);
	sc->getSocketManager()->SignalState_s.connect(sc->sigl1, &SignalListener1::OnSocketState);
	sc->getSocketManager()->SignalCandidatesReady.connect(sc->sigl1, &SignalListener1::OnCandidatesReady);
  	sc->CreateSocket(std::string("foobar"));
	sc->getSocket()->SignalReadPacket.connect(sc->sigl2, &SignalListener2::OnSocketRead); 

	return sc;
}

This method is called back when LibJingle has some local candidates for connection, that should be sent to the remote site via the signaling protocol.

The beauty of ICE protocol is that parties will be able to agree on a P2P connection without having to exchange request/response messages. They just send connection candidates to each other. Each side selects the "best" way to send data based on received candidades; being both sides able to send data directly to the remote party, we have a P2P bidirectional channel.

void SignalListener1::OnCandidatesReady(const std::vector<Candidate>& candidates)
{
	printf("OnCandidatesReady called with %d candidates in list\n", candidates.size());
  	
	for(std::vector<Candidate>::const_iterator it = candidates.begin(); it != candidates.end(); ++it) {
		char *marshaled_candidate;

		asprintf(&marshaled_candidate, "%s %d %s %f %s %s %s\n",
			(*it).address().IPAsString().c_str(), (*it).address().port(), (*it).protocol().c_str(),
    			(*it).preference(), (*it).type().c_str(), (*it).username().c_str(), 
			(*it).password().c_str() );

		printf("Candidate being sent: %s", marshaled_candidate);

		signaling_sendall(marshaled_candidate, strlen(marshaled_candidate));
		
		free(marshaled_candidate);

	}
}

An auxiliary function to send a data buffer through the signaling TCP channel. It does not return until all data has been sent.

void signaling_sendall(const char* buffer, unsigned int len)
{
	unsigned int sent = 0;
	while (sent < len) {
		int just_sent;
		just_sent = send(signaling_socket, buffer+sent, len-sent, 0);
		if (just_sent < 0) {
			printf("Signaling socket closed with error.\n");
			exit(1);
		} else if (just_sent == 0) {
			printf("Signaling socket closed.\n");
			exit(1);
		}
		sent += just_sent;
	}
}

This function is called by the main loop when some signaling data arrives. It will find out if there is a complete P2P connection candidate in the buffer. If there is one, it is decoded.

// extracts remote candidates from a buffer, returns a pointer to the rest of the buffer

char* socketclient_add_remote_candidates(SocketClient *sc, char* buffer)
{
	char *n;
	char candidate[1024];

	while (1) {
		n = strchr(buffer, '\n');
		if (! n) {
			return buffer;
		}
		strncpy(candidate, buffer, n-buffer+1);
		socketclient_add_remote_candidate(sc, candidate);
		buffer = n+1;
	}
}

Here, the P2P connection candidate is decoded and made known to LibJingle. Since LibJingle has its own thread and sockets, all further processing of P2P candidates is fortunately outside the scope of our code.

// Inform candidates received from the signaling network to LibJingle

void socketclient_add_remote_candidate(SocketClient *sc, const char* remote_candidate)
{
	std::vector<Candidate> candidates;

	char ip[100];
	unsigned int port;
	char protocol[100];
	float preference;
	char type[100];
	char username[100];
	char password[100];

	// WARNING: using fixed-size buffers and sscanf is utterly unsafe. 
	// Real implementations must be more robust about data coming from the network!

	sscanf(remote_candidate, "%s %d %s %f %s %s %s\n", ip, &port, protocol, &preference, type, username, password);

	printf("Received new candidate: %s:%d pref %f\n", ip, port, preference);

	Candidate candidate;
	candidate.set_name("rtp");
	candidate.set_address(SocketAddress(std::string(ip), port));
	candidate.set_username(std::string(username));
	candidate.set_password(std::string(password));
	candidate.set_preference(preference);
	candidate.set_protocol(protocol);
	candidate.set_type(type);
	candidate.set_generation(0);
	
	candidates.push_back(candidate);

	sc->getSocketManager()->AddRemoteCandidates(candidates);
}

Simple helper function that shows whether a P2P socket is writable (which means that the P2P connection is up).

bool socketclient_is_writable(SocketClient *sc)
{
	return sc->getSocketManager()->writable();
}

Method that is called back when data arrives from the P2P connection. In our example, it is just a byte of data.

void SignalListener2::OnSocketRead(P2PSocket *socket, const char *data, size_t len)
{
	printf("Received byte %d from remote P2P\n", data[0]);
}

Auxiliary function that sends data via P2P connection. Not really difficult.

void socketclient_send(SocketClient* sc, const char *data, unsigned int len)
{
	sc->getSocket()->Send(data, len);
	printf("Sent byte %d to remote P2P\n", data[0]);
}

The signaling server

As already said, our signaling server is incredibly simple and works more like a network pipe, forwarding data from one side to another. The P2P parties ogree on a P2P connection through this channel.

from select import select
import socket
import time

read_socks = []
port = 14141

server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  
server_sock.bind(("", port))
server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_sock.listen(5)

read_socks.append(server_sock)

# We accept two parties only
buffer = ""

while True:
	rd, wr_dummy, ex_dummy = select(read_socks, [], [], 10)
	
	if not rd:
		print "-- tick --"

		continue

	rd = rd[0]

	if rd is server_sock:
		# incoming new connection
		newsock, address = rd.accept()
		print "New connection from %s" % str(address)
		if len(read_socks) > 3:
			# we only accept two parties at the most
			newsock.close()
			continue
		read_socks.append(newsock)
		if buffer:
			# we already have data to be sent to the new party
			newsock.sendall(buffer)
			print "	sent buffered data"
			buffer = ""
		continue

	data = rd.recv(999999)

	if not data:
		# socket closed, remove from list
		print "Connection closed"

		del read_socks[read_socks.index(rd)]
		buffer = ""
		continue

	if len(read_socks) < 3:
		print "Buffering data"
		# the other party has not connected; bufferize
		buffer += data
		continue

	print "Forwarding data"
	for wr in read_socks:
		if wr is not rd and wr is not server_sock:
			wr.sendall(data)

STUN and relay servers

The maemo libjingle-utils package includes both a STUN server and a relay server for testing purposes. To run a test server, do the following:

$ stunserver &

$ relayserver &

The relay server will print console messages when a P2P connection is flowing through it. If you need more detailed feedback (e.g. if you are debugging a P2P application), use a network sniffing tool like tcpdump or Ethereal to monitor UDP packets.



Improve this page