Upgrading ssh to mosh with UDP hole punching to connect to a host behind NAT
There are way more tools that happily forward TCP ports than ones for UDP.
Case in point - it's usually easy to forward ssh port through a bunch of hosts and NATs, with direct and reverse ssh tunnels, ProxyCommand stuff, tools like pwnat, etc, but for mosh UDP connection it's not that trivial.
Which sucks, because its performance and input prediction stuff is exactly what's lacking in super-laggy multi-hop ssh connections forwarded back-and-forth between continents through such tunnels.
There are quite a few long-standing discussions on how to solve it properly in mosh, which didn't get any traction so far, unfortunately:
- https://github.com/mobile-shell/mosh/issues/48 - first one
- https://github.com/mobile-shell/mosh/issues/623 - latest one aggregating others
One obvious way to make it work, is to make some tunnel (like OpenVPN or wireguard) from destination host (server) to a client, and use mosh over that.
But that's some extra tools and configuration to keep around on both sides, and there is much easier way that works perfectly for most cases - knowing both server and client IPs, pre-pick ports for mosh-server and mosh-client, then punch hole in the NAT for these before starting both.
How it works:
- Pick some UDP ports that server and client will be using, e.g. 34700 for server and 34701 for client.
- Send UDP packet from server:34700 to client:34701.
- Start mosh-server, listening on server:34700.
- Connect to that with mosh-client, using client:34701 as a UDP source port.
NAT on the router(s) in-between the two will see this exchange as a server establishing "udp connection" to a client, and will allow packets in both directions to flow through between these two ports.
Once mosh-client establishes the connection and keepalive packets will start bouncing there all the time, it will be up indefinitely.
mosh is generally well-suited for running manually from an existing console, so all that's needed to connect in a simple case is:
server% mosh-server new MOSH CONNECT 60001 NN07GbGqQya1bqM+ZNY+eA client% MOSH_KEY=NN07GbGqQya1bqM+ZNY+eA mosh-client <server-ip> 60001
With hole-punching, two additional wrappers are required with the current mosh version (1.3.0):
- One for mosh-server to send UDP packet to the client IP, using same port on which server will then be started: mosh-nat
- And a wrapper for mosh-client to force its socket to bind to specified local UDP port, which was used as a dst by mosh-server wrapper above: mosh-nat-bind.c
Making connection using these two is as easy as with stock mosh above:
server% ./mosh-nat 74.59.38.152 mosh-client command: MNB_PORT=34730 LD_PRELOAD=./mnb.so MOSH_KEY=rYt2QFJapgKN5GUqKJH2NQ mosh-client <server-addr> 34730 client% MNB_PORT=34730 LD_PRELOAD=./mnb.so \ MOSH_KEY=rYt2QFJapgKN5GUqKJH2NQ mosh-client 84.217.173.225 34730
(with server at 84.217.173.225, client at 74.59.38.152 and using port 34730 on both ends in this example)
Extra notes:
- "mnb.so" used with LD_PRELOAD is that mosh-nat-bind.c wrapper, which can be compiled using: gcc -nostartfiles -fpic -shared -ldl -D_GNU_SOURCE mosh-nat-bind.c -o mnb.so
- Both mnb.so and mosh-nat only work with IPv4, IPv6 shouldn't use NAT anyway.
- 34730 is the default port for -c/--client-port and -s/--server-port opts in mosh-nat script.
- Started mosh-server waits for 60s (default) for mosh-client to connect.
- Continous operation relies on mosh keepalive packets without interruption, as mentioned, and should break on (long enough) net hiccups, unlike direct mosh connections established to server that has no NAT in front of it (or with a dedicated port forwarding).
- No roaming of any kind is possible here, again, unlike with original mosh - if src IP/port changes, connection will break.
- New MOSH_KEY is generated by mosh-server on every run, and is only good for one connection, as server should rotate it after connection gets established, so is pretty safe/easy to use.
- If client is behind NAT as well, its visible IP should be used, not internal one.
- Should only work when NAT on either side doesn't rewrite source ports.
Last point can be a bummer with some "Carrier-grade" NATs, which do rewrite src ports out of necessity, but can be still worked around if it's only on the server side by checking src port of the hole-punching packet in tcpdump and using that instead of whatever it was supposed to be originally.
Requires just python to run wrapper script on the server and no additional configuration of any kind.