Simple UDP hole punching using an IPIP tunnel as an example

Good time of day!



In this article I want to tell you how I implemented a ( one more ) Bash script to connect two computers behind NAT using UDP hole punching technology using the example of Ubuntu / Debian OS.



Establishing a connection consists of several steps:



  1. Starting a node and waiting for the remote node to be ready;
  2. Determining the external IP address and UDP port;
  3. Transfer of external IP-address and UDP-port to the remote host;
  4. Obtaining an external IP address and UDP port from a remote host;
  5. Organization of an IPIP tunnel;
  6. Connection monitoring;
  7. If the connection is lost, delete the IPIP tunnel.


I thought for a long time and still think that it can be used to exchange data between nodes, the simplest and fastest for me at the moment is to work through Yandex.Disk.



  • First, it's ease of use - you need 3 steps: create, read, delete. With curl this is:

    Create:



    curl -s -X MKCOL --user "$usename:$password" https://webdav.yandex.ru/$folder


    Read:

    curl -s --user "$usename:$password" -X PROPFIND -H "Depth: 1" https://webdav.yandex.ru/$folder


    Delete:



    curl -s -X DELETE --user "$usename:$password" https://webdav.yandex.ru/$folder
  • Secondly, it is easy to install:



    apt install curl


To determine the external IP address and UDP port, use the stun-client command:



stun stun.sipnet.ru -v -p $1 2>&1 | grep "MappedAddress"


Installation with the command:



apt install stun-client


To organize the tunnel, the standard OS tools from the iproute2 package are used. There are many tunnels that can be set up by standard means (L2TPv3, GRE, etc.), but I chose IPIP because it creates minimal additional load on the system. I tried L2TPv3 over UDP and was disappointed, the speed dropped by a factor of 10, but it may be various restrictions related to providers or something else. Since the IPIP tunnel operates at the IP level, the FOU tunnel is used to operate at the UDP port level. To organize an IPIP tunnel, you need to:



- load the FOU module:



modprobe fou


- listen to local port:



ip fou add port $localport ipproto 4


- create a tunnel:



ip link add name fou$name type ipip remote $remoteip local $localip encap fou  encap-sport $localport encap-dport $remoteport


- bring up the tunnel interface:



ip link set up dev fou$name


- assign internal local and internal remote tunnel IP address:



ip addr add $intIP peer $peerip dev fou$name


Delete tunnel:



ip link del dev fou$name


ip fou del port $localport


The tunnel status is monitored by periodically pinging the internal IP address of the tunnel of the remote host using the command:



ping -c 1 $peerip -s 0


Periodic ping is needed primarily to maintain the channel, otherwise, if the tunnel is idle on the routers, the NAT tables can be cleared and then the connection will be broken.



If the ping is lost, the IPIP tunnel is dropped and waits for the remote host to be ready.



The script itself:



#!/bin/bash
username="username@yandex.ru"
password="password"
folder="vpnid"
intip="10.0.0.1"
localport=`shuf -i 10000-65000 -n 1`
cid=`shuf -i 10000-99999 -n 1`
tid=`shuf -i 10-99 -n 1`
function yaread {
        curl -s --user "$1:$2" -X PROPFIND -H "Depth: 1" https://webdav.yandex.ru/$3 | sed 's/></>\n</g' | grep "displayname" | sed 's/<d:displayname>//g' | sed 's/<\/d:displayname>//g' | grep -v $3 | grep -v $4 | sort -r
}
function yacreate {
        curl -s -X MKCOL --user "$1:$2" https://webdav.yandex.ru/$3
}
function yadelete {
        curl -s -X DELETE --user "$1:$2" https://webdav.yandex.ru/$3
}
function myipport {
        stun stun.sipnet.ru -v -p $1 2>&1 | grep "MappedAddress" | sort | uniq | awk '{print $3}' | head -n1
}
function tunnel-up {
	modprobe fou
	ip fou add port $4 ipproto 4
	ip link add name fou$7 type ipip remote $1 local $3 encap fou encap-sport $4 encap-dport $2
	ip link set up dev fou$7
	ip addr add $6 peer $5 dev fou$7
}
function tunnel-check {
	sleep 10
        pings=0
        until [[ $pings == 4 ]]; do
                if ping -c 1 $1 -s 0 &>/dev/null;
                        then    echo -n .; n=0
                        else    echo -n !; ((pings++))
                fi
		sleep 15
        done
}
function tunnel-down {
	ip link del dev fou$1
	ip fou del port $2
}
trap 'echo -e "\nDisconnecting..." && yadelete $username $password $folder; tunnel-down $tunnelid $localport; echo "IPIP tunnel disconnected!"; exit 1' 1 2 3 8 9 14 15
until [[ -n $end ]]; do
    yacreate $username $password $folder
    until [[ -n $ip ]]; do
        mydate=`date +%s`
        timeout="60"
        list=`yaread $username $password $folder $cid | head -n1`
        yacreate $username $password $folder/$mydate:$cid
        for l in $list; do
                if [ `echo $l | sed 's/:/ /g' | awk {'print $1'}` -ge $(($mydate-65)) ]; then
			#echo $list
                        myipport=`myipport $localport`
                        yacreate $username $password $folder/$mydate:$cid:$myipport:$intip:$tid
                        timeout=$(( $timeout + `echo $l | sed 's/:/ /g' | awk {'print $1'}` - $mydate + 3 ))
                        ip=`echo $l | sed 's/:/ /g' | awk '{print $3}'`
                        port=`echo $l | sed 's/:/ /g' | awk '{print $4}'`
                        peerip=`echo $l | sed 's/:/ /g' | awk '{print $5}'`
			peerid=`echo $l | sed 's/:/ /g' | awk '{print $6}'`
			if [[ -n $peerid ]]; then tunnelid=$(($peerid*$tid)); fi
                fi
        done
        if ( [[ -z "$ip" ]] && [ "$timeout" -gt 0 ] ) ; then
                echo -n "!"
                sleep $timeout
        fi
    done
    localip=`ip route get $ip | head -n1 | sed 's|.*src ||' | cut -d' ' -f1`
    tunnel-up $ip $port $localip $localport $peerip $intip $tunnelid
    tunnel-check $peerip
    tunnel-down $tunnelid $localport
    yadelete $username $password $folder
    unset ip port myipport
done
exit 0


The username , password and folder variables must be the same on both sides, but the intip must be different, for example: 10.0.0.1 and 10.0.0.2. The time at the nodes must be synchronized. You can run the script like this:



nohup script.sh &


I draw your attention to the fact that the IPIP tunnel is insecure from the point of view of the fact that the traffic is not encrypted, but this is easily solved using IPsec for this article , it seemed to me simple and straightforward.



I have been using this script to connect to a working PC for several weeks and have not noticed any problems. Convenient in terms of setting up and forgetting.



Perhaps you will have comments and suggestions, I will be glad to hear.



Thanks for your attention!



All Articles