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:
- Starting a node and waiting for the remote node to be ready;
- Determining the external IP address and UDP port;
- Transfer of external IP-address and UDP-port to the remote host;
- Obtaining an external IP address and UDP port from a remote host;
- Organization of an IPIP tunnel;
- Connection monitoring;
- 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!