UDP Reverse-Tunnel with standard tools
Background
I like to run my stuff on a self-hosted server at home. Mostly because I think its fun, gives me flexibility and saves some money. Unfortunately providers for residential connections won’t give you a fixed IPv4, might change your IPv6 prefix, or even put you behind a CG-NAT to save on IPv4 addresses.
To encounter this I like to rent a small VPS with a fixed IPv4 and IPv6 to forward all traffic through it. For TCP based services this is a easy task solved with SSH reverse tunnels. Setting up a service that forwards connections from the VPS to a specific service within my home network is as easy as running
ssh -R *:[remote_port]:[local_ip]:[local_port] user@vps
on my home server.
But recently I encountered the need to expose a service relying on UDP.
This sounds like a standard problem with a standard solution
After a quick search on the Web I wasn’t happy with the results. Many recommend a dedicated tool like rtun or udp-reverse-tunnel from prof7bit, which might be a good solution, but I wanted to stick to common and readily available tools.
After some more research, I found that ssh`s ability to create linux tun devices in combination with socat could be the solution to my problem.
Using tun devices with ssh
SSH has functionality to forward IP trafic through linux tun devices.
To create a tun device on the client and server with “ssh -w [TUN_LOCAL]:[TUN_REMOTE]” you have to add “PermitTunel yes” to the server configuration (/etc/ssh/sshd_config).
If you want to use this from inside a container like LXC, further steps might be necessary to make tun devices available inside your container. For Proxmox users, the following two lines of configuration for a container are necessary (as pointed out in this forum thread):
lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.mount.entry: /dev/net dev/net none bind,create=dir
Next step is to assign an IP on both sides of the tunnel and bringing up the the link. For the remote side, these can be added to the ssh command, so ssh will execute them on the host.
ip addr add [IP]/[IP_MASK] dev [TUN_DEVICE]
ip link set [TUN_DEVICE] up
Using socat to forward udp connections
To forward incoming connections from the remote server to the service, we can use socat (SOcket CAT) which similary to netcat can accept incoming connections on a address and forward them to a second address. Unlike netcat, socat can accept connections on a socket, fork off the connection and reuse the listening socket.
socat -T120 UDP-LISTEN:[PORT],reuseaddr,fork UDP4:[ADDRESS]:[PORT]
Also forwarding from one protocol to another is possible (e.g. forwarding incoming IPv6 connections to IPv4):
socat -T120 UDP6-LISTEN:[PORT],reuseaddr,fork UDP4:[ADDRESS]:[PORT]
Note: Adding a timeout with “-T” is a good idea for protocols like UDP.
Otherwise established connections aren’t closed when they are not used anymore, since UDP has no native mechanism to signal this.
Combining things
To put everything together, I started with a script Marc Fargas published in the post Ip Tunnel Over Ssh With Tunon his blog.
I added the options “-tt” and “-o ExitOnForwardFailure=true” to the ssh invocation.
Unlike Marc I don’t want to establish a route between networks on both sides, so I removed that,
but added the port forwarding with socat to the local and remote side.
#!/bin/sh
HOST=user@remote.example # ssh host
HOST_PORT=12345 # Remote ssh port
TUN_LOCAL=0 # tun device number here
TUN_REMOTE=0 # tun device number there
IP_LOCAL=192.168.111.1 # IP Address for tun here
IP_REMOTE=192.168.111.2 # IP Address for tun there
IP_MASK=24 # Mask of the tun ips above.
LOCAL_SERVICE=192.168.0.123 # Service to forward trafic to on the local side
UDP_PORT=4242 # UDP port to forward
echo "Starting VPN tunnel ..."
# Start ssh and create a tun device (`-w`), run commands to assign a ip and bring it up,
# afterwards execute socat for IPv4&IPv6.
# `-tt` is used to force the assignment of a tty, this is needed to kill the socat processes when ssh exits.
ssh -w ${TUN_LOCAL}:${TUN_REMOTE} -o ExitOnForwardFailure=true -tt ${HOST} -p ${HOST_PORT} "\
ip addr add ${IP_REMOTE}/${IP_MASK} dev tun${TUN_REMOTE} \
&& ip link set tun${TUN_REMOTE} up \
&& exec socat -T120 UDP-LISTEN:${UDP_PORT},reuseaddr,fork UDP4:192.168.111.1:${UDP_PORT} \
& exec socat -T120 UDP6-LISTEN:${UDP_PORT},reuseaddr,fork UDP4:192.168.111.1:${UDP_PORT}" &
# Save the PID of the ssh process to later wait on it.
PID=$!
# Sleep a while to ensure the tun device is ready, then repeate: assign IP, bring interface up, start socat to forward trafic.
sleep 3
ip addr add ${IP_LOCAL}/${IP_MASK} dev tun${TUN_LOCAL}
ip link set tun${TUN_LOCAL} up
socat -T120 UDP4-LISTEN:${UDP_PORT},bind=${IP_LOCAL},fork,reuseaddr UDP4:${LOCAL_SERVICE}:${UDP_PORT} &
echo "... done."
wait $PID
The PID of the ssh process is saved to later wait on it.
This ensures, that the script returns when the connection is lost.
The “-tt” option is neccessary to allocate a tty on the remote.
Without, the spawned socat processes won’t get signals from its parent and keep executing when the parent is terminated.
Note: the forwarding on the local side can be omitted, if the target service can accept connections directly on the tun’s interface. Setting up routes like Marc did would be a alternative to target the services IP directly on the remote.