Monday, June 6, 2011

NAT Holepunch solution in Java

As I needed a solution for sending and receiving data in my videochat application over the internet I came across the problem with firewalls and NAT routers.

While searching the internet for existing solutions I bounced over the so called "Nat Holepunch".
See the article on wikipedia .This actually made that much sense to me, I started developing my own proof of concept.

Details

At first I will describe the Server, as it is the basic component. This is the so called "Relay Server".

RelayServer.java

package de.boehme.app.natholepunch;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.HashSet;
import java.util.Set;

import de.boehme.app.natholepunch.dto.NATDevice;

public class RelayServer {

private Set<NATDevice> inquiredComputers = new HashSet<NATDevice>();

public static void main(String[] args) {
RelayServer rs = new RelayServer();
try {
rs.startServer();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

private void startServer() throws IOException, InterruptedException {
DatagramSocket serverSocket = new DatagramSocket(12345);
byte[] receiveData = new byte[50];

while (true) {
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
System.out.println("Listening...");
serverSocket.receive(receivePacket);
String data = new String(receivePacket.getData());

//NAT Device 1
InetAddress incomingIPAddress = receivePacket.getAddress();
int port = receivePacket.getPort();
NATDevice startComp = new NATDevice();
startComp.setPortOfNAT(port);
startComp.setPublicIpOfNAT(incomingIPAddress);

//NAT Device 2
String accordingIP = data.substring(0, data.indexOf(0));
// String accordingPort = data.substring(data.indexOf(':')+1, data.indexOf(0));
NATDevice destinationComp = new NATDevice();
// destinationComp.setPortOfNAT(Integer.valueOf(accordingPort));
destinationComp.setPublicIpOfNAT(InetAddress.getByName(accordingIP));

System.out.println("Checking: "+startComp.getPublicIpOfNAT()+":"+startComp.getPortOfNAT()+" TO: "+destinationComp.getPublicIpOfNAT());
//check here if a matching entry is already present
NATDevice matchComp = checkMatchingNATDevice(destinationComp);
if(matchComp != null){
System.out.println("Already present.. now sending packets to both of them...");
//send to the socket from previously saved first NAT Device infos from the second NAT Device
sendPacket(serverSocket, matchComp, startComp);
Thread.sleep(1000);
sendPacket(serverSocket, startComp, matchComp);

inquiredComputers.remove(startComp);
inquiredComputers.remove(matchComp);
} else{
System.out.println("Adding "+startComp.getPublicIpOfNAT()+":"+startComp.getPortOfNAT()+" AND "+destinationComp.getPublicIpOfNAT()+" for later matching");
inquiredComputers.add(startComp);
}
}

// serverSocket.close();
}

private synchronized NATDevice checkMatchingNATDevice(NATDevice comp){
for(NATDevice c : inquiredComputers){
if(c.getPublicIpOfNAT().equals(comp.getPublicIpOfNAT())){
return c;
}
}
return null;
}

private synchronized void sendPacket(DatagramSocket socket, NATDevice natDeviceHome, NATDevice natDeviceRemote) throws IOException{

byte[] sendData = new byte[50];
InetAddress homeIPAddress = natDeviceHome.getPublicIpOfNAT();
int homePort = natDeviceHome.getPortOfNAT();
//now the remote destination
InetAddress destIPAddress = natDeviceRemote.getPublicIpOfNAT();
int destPort = natDeviceRemote.getPortOfNAT();
String data = destIPAddress.getHostAddress()+":"+destPort+"-"+homePort;
sendData = data.getBytes();
DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, homeIPAddress, homePort);
socket.send(sendPacket);
}

}


Explanation of the code:
The server should be run on a machine that can accept connections on port 12345. This is because the server listens on this specific port number.
Note: As this is only for development purposes the port number was hardcoded, i.e. when in production systems you would create it dynamically.

Going onwards to the most important method: startServer();
In the while loop we can see that on every connection attempt we differentiate between NAT Device 1 and NAT Device 2. How does this work? From the received packet we get a port number and an IP Adress. Both are needed to create a new object "NatDevice". In the data part of the packet we can extract the information we need to get the according second NAT Device.

The combination of NAT Device 1 to NAT Device 2 will be then added to an internal HashSet. That means when now NAT Device 2 connects to the server and the according NAT Device 1 is already present both NAT Devices get informed with the connection credentials of their connection peer.

How does the Client Side look like?

ClientBehindNAT.java

package de.boehme.app.natholepunch;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;

public class ClientBehindNAT {

private String relayServerIP;
private String destinationIP;
private InetAddress clientIP;

private int socketTimeout;

public static void main(String[] args) {
ClientBehindNAT c = new ClientBehindNAT();
boolean result = c.parseInput(args);

if(result == true){
try {
c.createHole(true);
} catch (SocketTimeoutException e) {
System.out.println("Timeout occured... Try again?");
} catch (IOException e) {
e.printStackTrace();
}
} else{
System.err.println("Usage: <Relay Server IP> (e.g. 192.168.40.1) <Destination IP> (e.g. 192.168.40.128) <Timeout> (in milliseconds)");
}

}

/**
* Main method used for outer access
* @param standAlone
* @return the free NAT port of the Firewall
* @throws IOException
*/
public synchronized int createConnectionToPeer(String relayServerIP, String destinationIP, int socketTimeout) throws SocketTimeoutException, IOException{
this.relayServerIP = relayServerIP;
this.destinationIP = destinationIP;
this.socketTimeout = socketTimeout;
return createHole(false);
}

private boolean parseInput(String[] args){
if(args.length < 3){
return false;
} else if(args[0].equals("") || args[1].equals("") || args[2].equals("")){
return false;
}else{
relayServerIP = args[0];
destinationIP = args[1];
socketTimeout = Integer.valueOf(args[2]);
return true;
}
}

private int createHole(boolean standAlone) throws SocketTimeoutException, IOException{
// InetAddress serverIPAddress = InetAddress.getByName("192.168.40.1");
clientIP = InetAddress.getLocalHost();
byte[] addrInByte = createInternetAddressFromString(relayServerIP);//{(byte) 192, (byte) 168, 40, 1};
InetAddress serverIPAddress = InetAddress.getByAddress(addrInByte);
int port = 12345;
String data = destinationIP;
System.out.println("Try to send: "+data+" TO: "+serverIPAddress+" ON PORT: "+port);
//first send UDP packet to the relay server
DatagramSocket clientSocket = new DatagramSocket();
clientSocket.setSoTimeout(socketTimeout);
sendPacket(clientSocket, serverIPAddress, port, data);
//now wait for the answer of the server
String peer = receivePacket(clientSocket);
clientSocket.close();
//need to make sure all had the chance to receive packets with infos
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return initPeerConnection(peer, standAlone);
}

private byte[] createInternetAddressFromString(String address){
byte[] inetAddressBytes = new byte[4];
int index = address.indexOf('.');
int i=0;
while((index = address.indexOf('.')) != -1){
int part = Integer.valueOf(address.substring(0, index));
inetAddressBytes[i] = (byte) part;
address = address.substring(index+1);
i++;
}
inetAddressBytes[i] = (byte) Integer.valueOf(address).intValue();
return inetAddressBytes;
}

private synchronized void sendPacket(DatagramSocket socket, InetAddress destIPAddress, int port, String data) throws IOException{
byte[] sendData = new byte[data.length()];
sendData = data.getBytes();
DatagramPacket sendPacket = new DatagramPacket(sendData,
sendData.length, destIPAddress, port);
socket.send(sendPacket);
}

private synchronized String receivePacket(DatagramSocket socket) throws SocketTimeoutException, IOException{
byte[] receiveData = new byte[50];
DatagramPacket receivePacket = new DatagramPacket(receiveData,
receiveData.length);
socket.receive(receivePacket);

String answerFromServer = new String(receivePacket.getData());
answerFromServer = answerFromServer.substring(0, answerFromServer.indexOf(0));
System.out.println(clientIP+" GOT FROM SERVER:" + answerFromServer);
return answerFromServer;
}

private synchronized int initPeerConnection(String peer, boolean standAlone) throws IOException{
String accordingIP = peer.substring(0, peer.indexOf(':'));
String accordingPort = peer.substring(peer.indexOf(':')+1, peer.indexOf('-'));
InetAddress destIPAddress = InetAddress.getByName(accordingIP);
int remotePort = Integer.valueOf(accordingPort);
int natPort = Integer.valueOf(peer.substring(peer.indexOf('-')+1));
//this only applies in stand-alone mode
if(standAlone){
Thread receiver = new Thread(new PeerReceiveThread(natPort));
receiver.start();
Thread sender = new Thread(new PeerSendThread(destIPAddress, remotePort));
sender.start();
}

return natPort;
}

private class PeerReceiveThread implements Runnable{

int natPort;

public PeerReceiveThread(int port){
this.natPort = port;
}

public void run() {
try {
DatagramSocket serverSocket = new DatagramSocket(natPort);
byte[] receiveData = new byte[50];
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
System.out.println(clientIP+" listening on port: "+natPort);
while(true){
serverSocket.receive(receivePacket);
String data = new String(receivePacket.getData());
System.out.println(clientIP+" FROM "+destinationIP+" WITH "+data);
}
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}

private class PeerSendThread implements Runnable{

InetAddress destIPAddress;
int port;

public PeerSendThread(InetAddress destIPAddress, int port) throws UnknownHostException{
this.destIPAddress = destIPAddress;
this.port = port;
}

public void run() {
try {
DatagramSocket clientSocket = new DatagramSocket();
while(true){
sendPacket(clientSocket, destIPAddress, port, "Hello from "+clientIP);
Thread.sleep(2000);
}
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

}

The basic principle works like this: (code taken from own videochat application)
ClientBehindNAT c = new ClientBehindNAT();
dataPort = c.createConnectionToPeer(relayServerIP, destAddress, 15000);
controlPort = c.createConnectionToPeer(relayServerIP, destAddress, 15000);

At first we create a new ClientBehindNAT object and then calling createConnectionToPeer() method. This leads to a connection attempt to the listening RelayServer. As we have defined a timeout of 15 seconds the Client won't wait for too long, in case the other peer didn't get connected to the server or any other unforeseen problem occured.

The most important part is the createHole() method. Inside of this method we define the set up to the relay server by sending an initial packet to it. After that we wait until a packet comes back or the timeout occurs.

When everything went well, we extract the valid port number from the data part of the received message and return it back to the method caller.

Download Source Code:
complete Maven Project
natholepunch.zip

9 comments:

  1. i'm not sure this is a udp hole punching implementation. I think the relay server should only give ip:port of client1 to client2 (and vice versa) so they can communicate without any relay server, by "punching" the nat with udp packets ip1:port1 <-> ip2:port2. On Client1, by punching paquets to ip2:port2 of client2, he can receive response from client2 because the client1's nat thinks that these packets are response from client1 paquets (same behaviour for client2's nat). So i think your code is only a kind of udp proxy : paquets are always sent to the relay server.
    Correct me if i'm wrong. Sorry for my english, i'm french.

    ReplyDelete
  2. Hi, Download link is down ? any chances of getting natholepunch.zip ?

    ReplyDelete
  3. Link is down! Please email file on vitrag007@gmail.com

    ReplyDelete
  4. Please email me source on phanthai.uit@gmail.com thanks!

    ReplyDelete
  5. Link is down... :( i need natholepunch.zip file
    Please email on 0verman2ya@gmail.com
    Thanks all !!!

    ReplyDelete
  6. Link is down... :( i need natholepunch.zip file
    Please email on msshin@iges.kr
    Thanks all !!!

    ReplyDelete
  7. Link is down... :( i need natholepunch.zip file
    Please email on msshin@iges.kr
    Thanks all !!!

    ReplyDelete