Simple UDP relay with NAT latching in Python

When you’re building a VOIP server you soon encounter the problem that a client is behind a NAT (instead of a directly reachable public IP). In this scenario the server can’t send packets directly to a client.

However there is a way around this and this is called ‘NAT latching’. Most NAT configurations automatically forward any reply that is addressed to the same port number that was used in sending back to the right client automatically.

So by configuring our application to receive on the same port number as it is using for sending UDP, once one packet is sent out from the client to the server we can set up bi-directional communication with this client (as long as the NAT binding stays open) by remembering which public ip/port combination it was sending from.

On the server side, we need something called a ‘relay channel’. This channel is nothing more that a pair of sockets that remember the origin of each data stream and use that as a destination for forwarding packets to the other side. It works like this (we use RTP in this example but it can be any UDP protocol):

Precondition: Client A and B are behind a NAT (so they have a non-public IP).

  1. Client A starts sending RTP from a specific UDP port X and simultaneously binds on this same port number X to receive RTP.
  2. Client B starts sending RTP from a specific UDP port Y and simultaneously binds on this same port number Y to receive RTP.
  3. Client A sends at least one packet from port X to the ‘left’ side of the relay channel.
  4. The relay server remembers the ip/port combination that the packet originated from (external_ip_of_client_a/port_X).
  5. Client B sends at least one packet from port Y to the ‘right’ side of the relay channel.
  6. The relay server remembers the ip/port combination that the packet originated from (external_ip_of_client_b/port_Y).

Now, if a packet comes in on the ‘left’ side of the relay channel, the server knows that it can be forwarded to external_ip_of_client_b/port_Y. And vice versa, if a packet comes in on the ‘right’ side of the relay channel, the server knows that it can be forwarded to external_ip_of_client_A/port_X.

The whole trick here is that a client needs to send at least 1 packet and then things will work fine :)

Because it can be a hassle to set up a full relay server when developing, I wrote this python script that implements the same functionality. It’s not recommended for production use but for development it works fine! Make sure to run it on a server that has a public IP.

#!/usr/bin/env python

# Simple script that implements an UDP relay channel
# Assumes that both sides are sending and receiving from the same port number
# Anything that comes in on left side will be forwarded to right side (once right side origin is known)
# Anything that comes in on right side will be forwarded to left side (once left side origin is known)

# Inspired by https://github.com/EtiennePerot/misc-scripts/blob/master/udp-relay.py

import sys, socket, select

def fail(reason):
        sys.stderr.write(reason + '\n')
        sys.exit(1)

if len(sys.argv) != 2 or len(sys.argv[1].split(':')) != 2:
        fail('Usage: udp-relay.py leftPort:rightPort')

leftPort, rightPort = sys.argv[1].split(':')

try:
        leftPort = int(leftPort)
except:
        fail('Invalid port number: ' + str(leftPort))
try:
        rightPort = int(rightPort)
except:
        fail('Invalid port number: ' + str(rightPort))

try:
        sl = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sl.bind(('', leftPort))
except:
        fail('Failed to bind on port ' + str(leftPort))

try:
        sr = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sr.bind(('', rightPort))
except:
        fail('Failed to bind on port ' + str(rightPort))


leftSource = None
rightSource = None
sys.stderr.write('All set.\n')
while True:
        ready_socks,_,_ = select.select([sl, sr], [], [])
        for sock in ready_socks:
                data, addr = sock.recvfrom(32768)
                if sock.fileno() == sl.fileno():
                        print "Received on left socket from " , addr
                        leftSource = addr;
                        if rightSource is not None:
                                print "Forwarding left to right ", rightSource
                                sr.sendto(data, rightSource)
                else :
                        if sock.fileno() == sr.fileno():
                                print "Received on right socket from " , addr
                                rightSource = addr;
                                if leftSource is not None:
                                        print "Forwarding right to left ", leftSource
                                        sl.sendto(data, leftSource)

For added convenience, you can download the file here. Happy hacking!