udp-holepunch - Python script for UDP holepunching using a webserver. With an example of SSH using this and sctp-echo.

git clone https://benconnors.ca/git-repos/udp-holepunch

About | Log | Files | Refs

UDP Holepunching

This is a simple Python program for performing UDP holepunching using an intermediate webserver. The intention is to conduct holepunching with as few open ports as possible, and those backed by reliable, well-tested systems:

  • HTTP port(s) on the intermediate server
  • SSH port on the intermediate server

Using tcpdump on the intermediate server we can read incoming packets without opening a firewall port. This could easily be altered by allowing an open UDP port on the webserver and using that instead.

Limitations

We do not use a sophisticated form of UDP holepunching: this is intended to be run on single friendly NATs; the main purpose is to establish the connection without opening any ports on the firewall.

Our UDP packets do not attempt to mimic any legitimate protocol: any firewall with some sort of restrictive DPI will likely block these. UDP holepunching in general will always be susceptible to this since the established communication link between client and server will almost certainly be using non-standard UDP ports since applications do not in general have any control over what external port the NAT assigns.

THe IP/port posted on the webserver are signed using an SSH key but are not encrypted to avoid pulling in any dependencies other than ssh-keygen on the client and server. This can be mitigated by choosing something long and random for the location of the file on the webserver.

Requirements

Intermediate

  • Webserver
  • SSH server
  • Root access
  • tcpdump

Client and Server

  • Modern Python with requests
  • cron or similar on the server
  • SSH client (with ssh-keygen)

Overview

On the network, the general procedure for holepunching is:

  1. Periodically, Server polls a specific Page on Intermediate (via udp_holepunch.py server ...)
  2. Client connects to Intermediate via SSH
  3. Client begins UDP holepunching with server
  4. Client writes its UDP external port and IP to a webpage on Intermediate
  5. Client waits for an incoming UDP connection
  6. Server polls the Page, sees the information, and sends packets to Client
  7. Client sends packets to Server; holepunch is established
  8. Client deletes the Page from the webserver

Once holepunching has been established, both sides print out (if successful):

<local UDP port> <remote IP> <remote UDP port>

The downstream applications on either end should open a UDP socket with port local UDP port and send messages to remote IP:remote UDP port.

Setup

  • Decide on a path for the file to poll on the webserver
  • Decide on a UDP port to send to on the server (or 0 to use the client's internal UDP port on the client, and the client's external UDP port on the server)

Server

  • Setup cron to run udp_holepunch.py server ... periodically. A typical invocation would be:
    udp_holepunch.py server https://example.com/poll-file
    

Client

  • Run udp_holepunch.py client .... A typical invocation would be:
    udp_holepunch.py client example-com-ssh-name /var/www/poll-file
    

Security

The client can sign the information it puts to the webserver using an SSH key (specified via -k). The server can accept a list of allowed public keys passed as a comma-separated list to -k in the same form as found in the key's .pub file. The server will exit with an error if a missing or invalid signature is found when polling.

Signing and verifying with SSH keys can be done via ssh-keygen without any external tools like GnuPG, so this does not pull in any additional dependencies.

Example: SSH without open ports

We can use holepunching and sctp-echo to establish a connection to an SSH server without opening/forwarding any ports on the server side.

The ideal simple solution is, once UDP holepunching is established as above, to use socat on each end:

client: socat -b 1150 - UDP:<remote ip>:<remote port>,sourceport=<my port>
server: socat UDP-LISTEN:<my port> TCP-CONNECT:127.0.0.1:<ssh port>

where the client socat reads from stdin, for use with SSH's ProxyCommand function. This will work for about a minute (the -b 1150 option on the client is important: larger options tend to break faster, smaller break SSH). The difficulty lies with socat and is irreparable. We have three connections:

client SSH --A-> client socat --B-> server socat --C-> server SSH

Connections A and C are both entirely reliable: A uses file descriptors and C uses TCP. Connection B, however, uses UDP and is not reliable. The result is that the whole connection client SSH -> server SSH is unreliable, yet since SSH is designed to run over TCP it has no facilities for dealing with this. At the first lost, corrupted, etc. packet the SSH session will be terminated.

We fix connection B using SCTP-over-UDP via sctp-echo. For the reasons described there, we require the use of usrsctp rather than the kernel implementations of SCTP.

The two files ssh_holepunch_client.py and ssh_holepunch_server.py implement the two sides of this. We use ssh_holepunch_server.py just as udp_holepunch.py server, except it will open a connection to the SSH server once holepunching has been established.

ssh_holepunch_client.py is intended to be called by SSH itself so that on the client side this whole process happens transparently. Inside ssh_config:

Host <server name>
    ProxyCommand "<path to ssh_holepunch_client.py> ... %h"
    ConnectTimeout 65

setting ConnectTimeout long enough in relation to the frequency that ssh_holepunch_server.py is run on the server (a minute or two unless you are very patient).