UDP Forwarder

December 15, 2012

UPDATE: A better-implemented Node.js version available here: https://gist.github.com/donghaoren/c7bbddd2ea865b0f25c3ed778db6d7db

This is a simple UDP forwarder with obfuscation support. It listens on a specific port for incoming UDP packets, and then send them to a forward address. In addition, it remembers senders’ addresses, and delivers packets from the forward address to the correct sender. Therefore it maintains ‘connections’ between the server and UDP clients.

The source code:

#!/usr/bin/python

import socket
import threading
import sys
import time

if len(sys.argv) < 4:
    print "Usage: udp_proxy forward-ip forward-port listen-port [v6] [obfuscate] [local]"
    print "Description:"
    print "1. Listen on *:listen-port for UDP packets,"
    print "   forward them to forward-ip:forward-port."
    print "2. IPv6 support: forward-ip can be an ipv6 address,"
    print "   use option 'v6' to listen on ipv6 port."
    print "3. Use option 'obfuscate' to enable packet obfuscation."
    print "4. Use option 'local' to listen on localhost only."
    exit()

# Parameters.
FORWARD_TO = int(sys.argv[2])
FORWARD_IP = sys.argv[1]
LISTEN_ON = int(sys.argv[3])

TIMEOUT_SECONDS = 180

FORWARD_V6 = ":" in FORWARD_IP
LISTEN_V6 = False
OBFUSCATE_PACKETS = False
OBFUSCATE_SEED = 2394
LISTEN_LOCAL_ONLY = False

for opt in sys.argv:
    if opt == "v6": LISTEN_V6 = True
    if opt == "obfuscate": OBFUSCATE_PACKETS = True
    if opt == "local": LISTEN_LOCAL_ONLY = True

# Server socket.
# Bind socket to LISTEN_ON port, all interfaces.
if LISTEN_V6:
    sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
    if LISTEN_LOCAL_ONLY:
        print "Listen on [::1]:%s with IPv6" % LISTEN_ON
        sock.bind(("::1", LISTEN_ON))
    else:
        print "Listen on *:%s with IPv6" % LISTEN_ON
        sock.bind(("", LISTEN_ON))
else:
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    if LISTEN_LOCAL_ONLY:
        print "Listen on 127.0.0.1:%s with IPv4" % LISTEN_ON
        sock.bind(("127.0.0.1", LISTEN_ON))
    else:
        print "Listen on *:%s with IPv4" % LISTEN_ON
        sock.bind(("", LISTEN_ON))

# Socket threads.
sock_dict = { }

# Lock.
lock = threading.Lock()

# Obfuscate data
# Satisfy:
# 1. len(obfuscate(data)) = len(data)
# 2. obfuscate(obfuscate(data)) = data
def obfuscate(data, dir):
    if not OBFUSCATE_PACKETS: return data
    a = bytearray(data)
    g_seed = OBFUSCATE_SEED
    for i in range(len(a)):
        g_seed = ((214013 * g_seed + 2531011)) & 0xFFFFFFFF;
        a[i] ^= (g_seed >> 16) & 0xFF
    return str(a)

# One thread for each connection.
class ListenThread(threading.Thread):
    def __init__(self, info):
        threading.Thread.__init__(self)
        self.s_client = info['socket']
        # Set timeout to 180 seconds, which is the common UDP gateway timeout.
        self.s_client.settimeout(1)
        self.addr = info['addr']
        self.last_receive = time.time()
        self.should_stop = False

    def run(self):
        while not self.should_stop:
            try: data, r_addr = self.s_client.recvfrom(65536)
            except:
                if time.time() - self.last_receive > TIMEOUT_SECONDS:
                    break
                else:
                    continue
            # Reset timeout.
            self.last_receive = time.time()
            # Successfully received a packet, forward it.
            data = obfuscate(data, 1)
            sock.sendto(data, self.addr)
        lock.acquire()
        try:
            self.s_client.close()
            sock_dict.pop(self.addr)
        except: pass
        lock.release()
        print "Client released for ", self.addr

    def stop(self):
        self.should_stop = True

try:
    while True:
        data, addr = sock.recvfrom(65536) # buffer size is 1024 bytes
        data = obfuscate(data, 0)
        lock.acquire()
        try:
            if not addr in sock_dict:
                if FORWARD_V6:
                    s_client = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
                else:
                    s_client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
                item = {
                    "socket": s_client,
                    "addr": addr
                }
                print "Adding client for ", addr
                s_client.sendto(data, (FORWARD_IP, FORWARD_TO))
                t = ListenThread(item)
                t.start()
                item['thread'] = t
                sock_dict[addr] = item
            else:
                s_client = sock_dict[addr]['socket']
                s_client.sendto(data, (FORWARD_IP, FORWARD_TO))
        except: pass
        lock.release()
except: pass

# Stop all threads.
for addr in sock_dict:
    try: sock_dict[addr]['thread'].stop()
    except: pass