SSH port forwarding for UDP packets

Previously we described how ssh port forwarding can be used to securely access other webservers in the remote network. This technique is useful for TCP packets. If you need to transmit UDP packets this is slightly more complicated. We wrote a small bash script to solve this. Please see the example below.

How to use ssh port forwarding to exchange UDP packets with other devices in the remote network:

This example shows how to exchange UDP packets from and to other devices with applications in the remote network. If qbee.io can run on the device this is not needed. This technique describes how you can access a HVAC application that communicates over UDP with a command tool. If you usually run that tool in the local network to configure the HVAC you can now do that from anywhere, even if the device with the HVAC application is closed.

So far we have only managed to do that for Linux and Mac!

This should be possible with Windows as well but we have not managed to find a native way and it was not possible to do that with WSL1 or WSL2.

Our example use case: "Query dns via remote device using local udp port"

  • We assume that qbee is running on a device in the remote network
  • On the local machine qbee-connect is installed
  • Public/private key auth setup for the ssh user on remote system (this can be done using "System > SSH Keys" in qbee)
  • socat installed on remote system (this can be done using "System > Package management" in qbee)
  • socat installed on local system (sudo apt-get install -y socat)
  • the below script qbee-connect-udp-forward.sh is installed on the local machine
qbee-connect-udp-forward.sh
        #!/usr/bin/env bash

        # Requirements
        # 1. Public/private key auth setup for the ssh user on remote system (this can be done using "System > SSH Keys" in qbee)
        # 2. socat installed on remote system (this can be done using "System > Package management" in qbee)
        # 3. socat installed on local system (sudo apt-get install -y socat)

        declare -a SIGTERMS

        _term() {
          for pid in ${SIGTERMS[@]}; do
            kill -TERM $pid 2> /dev/null
            wait $pid 2> /dev/null
          done
          $SSH_COMMAND -S /tmp/$(basename $0).$TUNNEL_PORT.tunnel -O exit
        }

        _usage() {
          cat <<EOF
        usage: $(basename $0) -p local_ssh_port -P remote_udp_forward_port 
                              -S remote_udp_forward_host [-L local_udp_listen_port>]
                              [-u remote_ssh_username]

            -p Local ssh port tunneled by qbee connect
            -P Remote udp port to be forwarded
            -S Remote ip to forward to
            -L Local udp port to listen to (optional, defaults to remote udp port)
            -u Remote ssh user (options, defaults to "root")
        EOF
        }

        while getopts "p:S:P:u:L:h" opt; do
          case $opt in
          p)  
            QBEE_CONNECT_LOCAL_PORT=$OPTARG
            ;;  
          P)  
            UDP_FORWARD_PORT=$OPTARG
            ;;  
          S)  
            UDP_FORWARD_HOST=$OPTARG
            ;;  
          u)  
            USERNAME=$OPTARG
            ;;  
          L)  
            UDP_LOCAL_LISTEN_PORT=$OPTARG
            ;;  
          h)
            _usage
            exit 0
            ;;
          \?)
            echo "Invalid option: -$OPTARG" >&2
            _usage
            exit 1
            ;;
          esac
        done

        # Defaults
        UDP_LOCAL_LISTEN_PORT=${UDP_LOCAL_LISTEN_PORT:-$UDP_FORWARD_PORT}
        USERNAME=${USERNAME:-root}

        if [[ -z $UDP_FORWARD_HOST || -z $QBEE_CONNECT_LOCAL_PORT || -z $UDP_FORWARD_PORT ]]; then
          echo "ERROR: Missing mandatory options"
          _usage
          exit 1
        fi

        # Select a random port different than qbee-connect local port
        TUNNEL_PORT=$QBEE_CONNECT_LOCAL_PORT
        while [[ $TUNNEL_PORT -eq $QBEE_CONNECT_LOCAL_PORT ]]; do
          TUNNEL_PORT=$(( ((RANDOM<<15)|RANDOM) % 63001 + 2000 ))
        done


        trap _term SIGTERM SIGINT

        SSH_COMMAND="ssh -o StrictHostKeyChecking=no -p $QBEE_CONNECT_LOCAL_PORT $USERNAME@localhost"
        $SSH_COMMAND -Nf -M -S /tmp/$(basename $0).$TUNNEL_PORT.tunnel -L $TUNNEL_PORT:127.0.0.1:$TUNNEL_PORT

        SOCAT_CMD="socat"

        if [[ $UDP_LOCAL_LISTEN_PORT -lt 1024 ]]; then
          # Privileged ports, attempt sudo
          SOCAT_CMD="sudo socat"
        fi

        $SOCAT_CMD -T15 udp4-recvfrom:$UDP_LOCAL_LISTEN_PORT,reuseaddr,fork tcp:localhost:$TUNNEL_PORT &
        SIGTERMS=($! "${SIGTERMS[@]}")

        $SSH_COMMAND -tt "socat tcp4-listen:$TUNNEL_PORT,reuseaddr,fork UDP:$UDP_FORWARD_HOST:$UDP_FORWARD_PORT" &
        SIGTERMS=($! "${SIGTERMS[@]}")
        echo "CTRL-C to exit"
        wait $!

Now we connect the remote device via qbee-connect on the ssh port 22. This will give us a port number. In this specific case we received port number 61869. So the remote port 22 of our Raspberry Pi is mapped to localhost:61869.

Then we run the bash script on the local machine. This will setup socat and connect us with the user "pi".

./qbee-connect-udp-forward.sh -p 61869 -u pi -P 53 -S 8.8.8.8 -L 7753
It also connects the UDP port 53 to the localhost port 7753 on the local machine. Since we have no machine in the remote network running any UDP service we use the DNS server 8.8.8.8 as an example of the final target device to communicate with.

So as a last step we need to verify that we really have established a UDP bi-directional communication. This can be done be issuing the following command in another terminal on the local machine:

dig -p 7753 @localhost qbee.io
The output shows that the remote device allows us to do a DNS query through the tunnel using the remote device as the origin of the DNS UDP query. The output yields:

; <<>> DiG 9.10.6 <<>> -p 7753 @localhost qbee.io
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 45737
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;qbee.io.           IN  A

;; ANSWER SECTION:
qbee.io.        299 IN  A   46.101.255.248

;; Query time: 367 msec
;; SERVER: 127.0.0.1#7753(127.0.0.1)
;; WHEN: Sun Nov 15 15:36:31 CET 2020
;; MSG SIZE  rcvd: 52