通过提供商 NAT 在计算机之间建立直接 VPN 隧道(无需 VPS,使用 STUN 服务器和 Yandex.disk)

延期 文章 关于我如何设法在位于 NAT 提供商后面的两台计算机之间组织直接 VPN 隧道。 上一篇文章描述了在第三方(中介)的帮助下组织连接的过程(租用的 VPS 充当 STUN 服务器和用于连接的节点数据传输器)。 在这篇文章中,我将告诉你我如何在没有 VPS 的情况下进行管理,但中介仍然存在,它们是 STUN 服务器和 Yandex.Disk...
通过提供商 NAT 在计算机之间建立直接 VPN 隧道(无需 VPS,使用 STUN 服务器和 Yandex.disk)

介绍

阅读上一篇文章的评论后,我意识到该实现的主要缺点是使用中介 - 第三方(VPS)来指示节点的当前参数、连接位置和方式。 考虑使用此 STUN 的建议(其中有很多) 确定当前连接参数。 首先,当 STUN 服务器与客户端一起工作并收到完全不可读的内容时,我决定使用 TCPDump 来查看数据包的内容。 谷歌搜索我遇到的协议 描述协议的文章。 我意识到我无法独自实现对 STUN 服务器的请求,并将这个想法放在“遥远的盒子”中。

Теория

最近我必须从软件包中在 Debian 上安装 STUN 服务器

# apt install stun-server

在依赖项中我看到了 stun-client 包,但不知何故我没有注意到它。 但后来我想起了 stun-client 包并决定弄清楚它是如何工作的,在 Google 和 Yandex 中搜索后我得到:

# apt install stun-client
# stun stun.ekiga.net -p 21234 -v

我收到的回复是:

STUN客户端版本0.97
使用 fd 21234 打开端口 3
使用 fd 21235 打开端口 4
编码眩晕消息:
编码更改请求:0

即将发送长度为 28 的消息到 216.93.246.18:3478
编码眩晕消息:
编码更改请求:4

即将发送长度为 28 的消息到 216.93.246.18:3478
编码眩晕消息:
编码更改请求:2

即将发送长度为 28 的消息到 216.93.246.18:3478
收到的眩晕消息:92 字节
映射地址 = <我的 IP>:2885
源地址 = 216.93.246.18:3478
更改地址 = 216.93.246.17:3479
未知属性:32800
服务器名称 = Vovida.org 0.98-CPC
收到类型 257 id=1 的消息
编码眩晕消息:
编码更改请求:0

即将发送长度为 28 的消息到 216.93.246.17:3478
编码眩晕消息:
编码更改请求:4

即将发送长度为 28 的消息到 216.93.246.18:3478
编码眩晕消息:
编码更改请求:2

即将发送长度为 28 的消息到 216.93.246.18:3478
编码眩晕消息:
编码更改请求:0

即将发送 len 28 的消息到 <我的 IP>:2885
收到的眩晕消息:28 字节
变更请求 = 0
收到类型 1 id=11 的消息
编码眩晕消息:
编码更改请求:0

即将发送长度为 28 的消息到 216.93.246.17:3478
编码眩晕消息:
编码更改请求:4

即将发送长度为 28 的消息到 216.93.246.18:3478
编码眩晕消息:
编码更改请求:2

即将发送长度为 28 的消息到 216.93.246.18:3478
收到的眩晕消息:92 字节
映射地址 = <我的 IP>:2885
源地址 = 216.93.246.17:3479
更改地址 = 216.93.246.18:3478
未知属性:32800
服务器名称 = Vovida.org 0.98-CPC
收到类型 257 id=10 的消息
编码眩晕消息:
编码更改请求:4

即将发送长度为 28 的消息到 216.93.246.18:3478
编码眩晕消息:
编码更改请求:2

即将发送长度为 28 的消息到 216.93.246.18:3478
编码眩晕消息:
编码更改请求:4

即将发送长度为 28 的消息到 216.93.246.18:3478
编码眩晕消息:
编码更改请求:2

即将发送长度为 28 的消息到 216.93.246.18:3478
编码眩晕消息:
编码更改请求:4

即将发送长度为 28 的消息到 216.93.246.18:3478
编码眩晕消息:
编码更改请求:2

即将发送长度为 28 的消息到 216.93.246.18:3478
编码眩晕消息:
编码更改请求:4

即将发送长度为 28 的消息到 216.93.246.18:3478
编码眩晕消息:
编码更改请求:2

即将发送长度为 28 的消息到 216.93.246.18:3478
编码眩晕消息:
编码更改请求:4

即将发送长度为 28 的消息到 216.93.246.18:3478
编码眩晕消息:
编码更改请求:2

即将发送长度为 28 的消息到 216.93.246.18:3478
测试一 = 1
测试 II = 0
测试 III = 0
测试 I(2) = 1
是 nat = 1
映射的 IP 相同 = 1
发夹 = 1
保护口 = 0
主要:独立映射、端口相关过滤器、随机端口、发夹
返回值为0x000006

带有值的字符串

映射地址 = <我的 IP>:2885

正是您所需要的! 它显示了本地 UDP 端口 21234 上连接的当前状态。但这只是成功的一半;出现了如何将此数据传输到远程主机并组织 VPN 连接的问题。 使用邮件协议,或者可能是 Telegram?! 有很多选择,我决定使用 Yandex.disk,因为我遇到了 关于通过 WebDav 与 Yandex.disk 一起使用 Curl 的文章。 经过思考实施,我提出了以下方案:

  1. 通过 Yandex.disk 上存在带有时间戳的特定文件来表明节点已准备好建立连接;
  2. 如果节点准备就绪,则从 STUN 服务器接收当前参数;
  3. 将当前设置上传到 Yandex.disk;
  4. 检查远程节点是否存在并从 Yandex.disk 上的文件读取参数;
  5. 使用 OpenVPN 与远程主机建立连接。

实践

稍微思考了一下,考虑到上一篇文章的经验,我很快就写了一个脚本。 我们会需要:

# apt install openvpn stun-client curl 

脚本本身:

初始版本

# cat vpn8.sh

#!/bin/bash
######################## Задаем цветной текст ###
WARN='33[37;1;41m'				#
END='33[0m'					#
RED='33[0;31m'         #  ${RED}		#
GREEN='33[0;32m'      #  ${GREEN}		#
#################################################
####################### Проверяем наличие необходымих приложений #########################################################
al="echo readlink dirname grep awk md5sum shuf nc curl sleep openvpn cat stun"
ch=0
for i in $al; do which $i > /dev/null || echo -e "${WARN}Для работы необходим $i ${END}"; which $i > /dev/null || ch=1; done
if (( $ch > 0 )); then echo -e "${WARN}Ой, отсутствуют необходимые для корректной работы приложения${END}"; exit; fi
#######################################################################################################################

if [[ $1 == '' ]]; then echo -e "${WARN}Введите идентификатор соединения (любое уникальное слово, должно быть одинаковое с двух сторон!) ${END} t
${GREEN}Для запуска в автоматическом режиме при включении компьютера можно прописать в /etc/rc.local строку nohup /<путь к файлу>/vpn8.sh  > /var/log/vpn8.log 2>/dev/hull & ${END}"; exit; fi
ABSOLUTE_FILENAME=`readlink -f "$0"`                                                    # полный путь до скрипта
DIR=`dirname "$ABSOLUTE_FILENAME"`                                                      # каталог в котором лежит скрипт
############################### Проверка наличия секретного ключа ##################################
key="$DIR/secret.key"
if [ ! -f "$key" ]; then
				echo -e "${WARN}Секретный ключ VPN-соединения не найден, для генерации ключа выполните: 
openvpn --genkey --secret secret.key Внимание: ключ используется для авторизации и должен 
быть одинаковым с двух сторон!!!${END}
 # ls -l secret.key
 -rw------- 1 root root 637 ноя 27 11:12 secret.key
 # chmod 600 secret.key";
				exit;
				fi
########################################################################################################################

ABSOLUTE_FILENAME=`readlink -f "$0"`                                                    # полный путь до скрипта
DIR=`dirname "$ABSOLUTE_FILENAME"`                                                      # каталог в котором лежит скрипт
name=$(uname -n | md5sum | awk '{print $1}')
vpn=$(echo $1 | md5sum | awk '{print $1}')
stun="stun.ekiga.net" 	# STUN сервер
username="Yandex"	# Логин от Яндекс.диска	
password="Password"	# Пароль от Яндекс.диска
localport=`shuf -i 20000-65000 -n 1`	# генерация локального порта

echo "$(date) Создаю папку на Яндекс.диске"
curl -X MKCOL --user "${username}:${password}" https://webdav.yandex.ru/vpn-$vpn
echo "$(date) Очищаю папку от всякого мусора"
for i in `curl --silent --user "$username:$password" -X PROPFIND -H "Depth: 1" https://webdav.yandex.ru/vpn-$vpn/ | sed 's/></n/g' | grep "d:displayname" | sed 's/d:displayname//g' | sed 's/>//g' | sed 's/<//' | sed 's////g' | grep -v $(date +%Y-%m-%d-%H-%M)`; do
	echo "$(date) Delete: $i"
	curl -X DELETE --user "${username}:${password}" https://webdav.yandex.ru/vpn-$vpn/$i
	done

until [ $c ];do

	until [[ $b ]]; do
		echo "$(date) Проверяю папку"
		date=`date +%Y-%m-%d-%H-%M`
		mydata=`curl --silent --user "${username}:${password}" -X PROPFIND -H "Depth: 1" https://webdav.yandex.ru/vpn-$vpn/ | sed 's/></>n</g' | grep $name | grep $date | grep "d:displayname"`
		if [[ -z $mydata ]]; 	then
						echo "$(date) Файл готовности создан"
					        echo "$date" > "/tmp/$date-$name-ready.txt"
					        curl -T "/tmp/$date-$name-ready.txt" --user "$username:$password" https://webdav.yandex.ru/vpn-$vpn/$date-$name-ready.txt
					else
						echo "$(date) Файл готовности уже существует - $date"
					fi
		remote=`curl --silent --user "${username}:${password}" -X PROPFIND -H "Depth: 1" https://webdav.yandex.ru/vpn-$vpn/ | sed 's/></>n</g' | grep -v $name | grep $date | grep "d:displayname"`
		if [[ -z $remote ]];	then
						echo -e "$(date) ${RED} Удаленный узел не готов ${END}"
						echo "$(date) Жду"
						sleep 20
					else
						echo -e "$(date) ${GREEN} Удаленный узел готов ${END}"
						b=1
						a=''
					fi
	done

	until [ $a ]; do
		echo "$(date) Подключение и получение данных от STUN сервера: $stun"
                mydata=`stun $stun -p $localport -v 2>&1 | grep MappedAddress | sort | uniq`
                echo -e "$(date) ${GREEN}Мои данные соединения: $mydata${END}"
                echo "$mydata" > "$DIR/mydata"
                echo "$(date) Загрузка данных на Яндекс.диск"
                curl -T "$DIR/mydata" --user "$username:$password" https://webdav.yandex.ru/vpn-$vpn/$name.txt
		echo "$(date) Получение файла данных удаленного узла"
		filename=$(curl --silent --user "${username}:${password}" -X PROPFIND -H "Depth: 1" https://webdav.yandex.ru/vpn-$vpn/ | sed 's/></n/g' | grep "d:displayname>" | grep "txt" | grep -v "$name" | grep -v "ready" | sed 's|.*d:displayname>||' | sed 's/</ /g' | awk '{print $1}')
		echo "$(date) Чтение файла данных удаленного узла: $filename"
		address=$(curl --silent --user "$username:$password" https://webdav.yandex.ru/vpn-$vpn/$filename | sort | uniq | head -n1 | sed 's/:/ /g')
		echo "$(date) Определение IP-адреса и порта"
		ip=$(echo "$address" | awk '{print $3}')
		port=$(echo "$address" | awk '{print $4}')
		if [[ -n "$ip" && -n "$port" ]]; then
			echo -e "$(date) ${GREEN} Соединение $ip $port ${END}"
       		 	openvpn --remote $ip --rport $port --lport $localport 
	       	 	    --proto udp --dev tap --float --auth-nocache --verb 3 --mute 20 
	       	 	    --ifconfig 10.45.54.2 255.255.255.252 
	       		    --secret "$DIR/secret.key" 
	       		    --auth SHA256 --cipher AES-256-CBC 
	        	    --ncp-disable --ping 10  --ping-exit 30 
	        	    --comp-lzo yes
			echo -e "$(date) ${WARN} Соединение разорвано${END}"
			a=1
			b=''
			else
			a=1
			b=''
			fi
	done
done

为了使脚本正常工作,您需要:

  1. 复制到剪贴板并粘贴到编辑器中,例如:
    # nano vpn8.sh 
  2. 指定 Yandex.disk 的用户名和密码。
  3. 在“—ifconfig 10.45.54.(1 or 2) 255.255.255.252”字段中指定接口的内部IP地址
  4. 创造 密钥 使用命令:
    # openvpn --genkey --secret secret.key 
  5. 使脚本可执行:
    # chmod +x vpn8.sh
  6. 运行脚本:
    # ./vpn8.sh nZbVGBuX5dtturD

    其中 nZbVGBuX5dtturD 是生成的连接 ID 这里

在远程节点上,除了生成secret.key和连接ID之外,执行所有相同的操作,它们必须相同。

更新版本(时间必须同步才能正确操作):

cat vpn10.sh

#!/bin/bash
stuns="stun.sipnet.ru stun.ekiga.net"   		# Список STUN серверов через пробел
username=" Login "		# Логин от Яндекс.диска
password=" Password "   	# Пароль от Яндекс.диска
intip="10.23.22.1"		# IP-адрес внутреннего интерфейса
WARN='33[37;1;41m'
END='33[0m'
RED='33[0;31m'
GREEN='33[0;32m'
al="ip echo readlink dirname grep awk md5sum openssl sha256sum shuf curl sleep openvpn cat stun"
ch=0
for i in $al; do which $i > /dev/null || echo -e "${WARN}Для работы необходим $i ${END}"; which $i > /dev/null || ch=1; done
if (( $ch > 0 )); then echo -e "${WARN}Ой, отсутствуют необходимые для корректной работы приложения${END}"; exit; fi
if [[ $1 == '' ]];
then
echo -e "${WARN}Введите идентификатор соединения (любое уникальное слово, должно быть одинаковое с двух сторон!) ${END} t
${GREEN}Для запуска в автоматическом режиме при включении компьютера можно прописать в /etc/rc.local строку nohup /<путь к файлу>/vpn10.sh  > /var/log/vpn10.log 2>/dev/hull & ${END}"
exit
fi
ABSOLUTE_FILENAME=`readlink -f "$0"`                                                    # полный путь до скрипта
DIR=`dirname "$ABSOLUTE_FILENAME"`                                                      # каталог в котором лежит скрипт
key="$DIR/secret.key"
until [[ -n "$iftosrv" ]]
do
echo "$(date) Определяю сетевой интерфейс"; iftosrv=`ip route get 8.8.8.8 | head -n 1 | sed 's|.*dev ||' | awk '{print $1}'`
sleep 5
done
timedatectl
name=$(uname -n | md5sum | awk '{print $1}')
vpn=$(echo $1 | md5sum | awk '{print $1}')
echo "$(date) Создаю папку на Яндекс.диске"
curl -X MKCOL --user "${username}:${password}" https://webdav.yandex.ru/vpn-$vpn
echo "$(date) ID на диске: $vpn"
until [ $c ];do
echo "$(date) Очищаю папку от всякого мусора"
for i in `curl --silent --user "$username:$password" -X PROPFIND -H "Depth: 1" https://webdav.yandex.ru/vpn-$vpn/ | sed 's/></n/g' | grep "d:displayname" | sed 's/d:displayname//g' | sed 's/>//g' | sed 's/<//' | sed 's////g' | grep -v $(date +%Y-%m-%d-%H-%M)`
do
echo -e "$(date)${RED} Удаляю старый файл: $i${END}"
curl -X DELETE --user "${username}:${password}" https://webdav.yandex.ru/vpn-$vpn/$i
done
echo "$(date) ID на диске: $vpn"
openvpn --genkey --secret "$key"
passwd=`echo "$vpn-tt" | sha256sum | awk '{print $1}'`
openssl AES-256-CBC -e -in "$key" -out "$DIR/file.enc" -k "$passwd" -base64
curl -T "$DIR/file.enc" --user "$username:$password" https://webdav.yandex.ru/vpn-$vpn/key.enc
rm "$DIR"/file.enc
echo -e "$(date) ${GREEN}Фаза 1 - Получение готовности удаленного узла${END}"
go=3
localport=`shuf -i 20000-65000 -n 1`    # генерация локального порта
start=''
remote=''
timeout1=''
nextcheck=''
timestart=''
until [[ $b ]]
do
echo "$(date) Проверяю папку"
date=`date +%s`
timeout1=60
echo "$(date) Создание файла готовности $date"
echo "$date" > "/tmp/ready-$date-$name.txt"
curl -T "/tmp/ready-$date-$name.txt" --user "$username:$password" https://webdav.yandex.ru/vpn-$vpn/ready-$name.txt
readyfile=`curl --silent --user "${username}:${password}" -X PROPFIND -H "Depth: 1" https://webdav.yandex.ru/vpn-$vpn/ | sed 's/></>n</g' | grep -v $name | grep "ready" | grep "d:displayname" | sed 's/<d:displayname>//g' | sed 's/</d:displayname>//g'`
if [[ -z $readyfile ]]
then
echo -e "$(date) ${RED} Удаленный узел не готов ${END}"
echo "$(date) Жду 60 секунд"
sleep $timeout1
else
remote=$(curl --silent --user "$username:$password" https://webdav.yandex.ru/vpn-$vpn/$readyfile)
echo -e "$(date) ${GREEN} Удаленный узел готов ${END}"
start=`curl --silent --user "${username}:${password}" -X PROPFIND -H "Depth: 1" https://webdav.yandex.ru/vpn-$vpn/ | sed 's/></>n</g' | grep "start" | grep "d:displayname" | sed 's/-/ /g' | awk '{print $2}'`
if [[ -z $start ]]
then
let nextcheck=$timeout1-$date+$remote
let timestart=$date+$timeout1-$nextcheck
go=$nextcheck
echo "$timestart" > "/tmp/start-$date-$name.txt"
curl -T "/tmp/start-$date-$name.txt" --user "$username:$password" https://webdav.yandex.ru/vpn-$vpn/start-$date-$name.txt
else
echo "$(date) жду $go секунд"
sleep $go
b=1
a=''
fi
fi
done
echo -e "$(date) ${GREEN}Фаза 2 - Обмен данными и установка соединения${END}"
mydata=''
filename=''
address=''
myip=''
ip=''
port=''
ex=0
until [ $a ]; do
until [[ -n "$mydata" ]]; do
k=`echo "$stuns" | wc -w`
x=1
z=`shuf -i 1-$k -n 1`
for st in $stuns; do
if [[ $x == $z ]]; then
stun=$st;
fi;
(( x++ ));
done
echo "$(date) Подключение и получение данных от STUN сервера: $stun"
sleep 5 && for pid in $(ps xa | grep "stun "$stun" 1 -p "$localport" -v" | grep -v grep | awk '{print $1}'); do kill $pid; done &
mydata=`stun "$stun" 1 -p "$localport" -v 2>&1 | grep "MappedAddress" | sort | uniq`
done
echo -e "$(date) ${GREEN}Мои данные соединения: $mydata${END}"
echo "$(date) Загрузка данных на Яндекс.диск"
echo "$mydata" > "$DIR/mydata"
echo "IntIP $intip" >> "$DIR/mydata"
curl -T "$DIR/mydata" --user "$username:$password" https://webdav.yandex.ru/vpn-$vpn/$name-ipport.txt
rm "$DIR/mydata"
sleep 5
echo "$(date) Получение файла данных удаленного узла"
filename=$(curl --silent --user "${username}:${password}" -X PROPFIND -H "Depth: 1" https://webdav.yandex.ru/vpn-$vpn/ | sed 's/></n/g' | grep "d:displayname>" | grep "ipport" | grep -v "$name" |  sed 's|.*d:displayname>||' | sed 's/</ /g' | awk '{print $1}')
if [[ -n "$filename" ]]
then
echo "$(date) Чтение файла данных удаленного узла: $filename"
address=$(curl --silent --user "$username:$password" https://webdav.yandex.ru/vpn-$vpn/$filename | grep "MappedAddress" | head -n1 | sed 's/:/ /g')
intip2=$(curl --silent --user "$username:$password" https://webdav.yandex.ru/vpn-$vpn/$filename | grep "IntIP" | head -n1 | awk '{print $2}')
echo "$(date) Определение IP-адреса и порта: $address $sesid2 $tunid2"
ip=$(echo "$address" | awk '{print $3}')
port=$(echo "$address" | awk '{print $4}')
myip=`ip route get "$ip" | head -n 1 | sed 's|.*src ||' | awk '{print $1}'`
if [[ -n "$ip" && -n "$port" && -n "$myip" && -n "$localport" ]];
then
echo -e "$(date) ${GREEN} Соединение $ip $port ${END}"
echo -e  "`date` ${GREEN} $myip:$localport -> $ip:$port ${END}"
curl --silent --user "$username:$password" https://webdav.yandex.ru/vpn-$vpn/key.enc > "$DIR/secret.enc"
openssl AES-256-CBC -d -in "$DIR/secret.enc" -out "$key" -k "$passwd" -base64
chmod 600 "$key"
rm "$DIR/secret.enc"
openvpn --remote $ip --rport $port --lport $localport 
--proto udp --dev tun --float --auth-nocache --verb 3 --mute 20 
--ifconfig "$intip" "$intip2" 
--secret "$key" 
--auth SHA256 --cipher AES-256-CBC 
--ncp-disable --ping 10 --ping-exit 20 
--comp-lzo yes
a=1
b=''
fi
else
if (( $ex >= 5 ))
then
echo "$(date) Сброс"
a=1
b=''
fi
(( ex++ ))
sleep 5
fi
done
done

为了使脚本正常工作,您需要:

  1. 复制到剪贴板并粘贴到编辑器中,例如:
    # nano vpn10.sh 
  2. 指示 Yandex.disk 的登录名(第二行)和密码(第三行)。
  3. 指定隧道的内部 IP 地址(第 4 行)。
  4. 使脚本可执行:
    # chmod +x vpn10.sh
  5. 运行脚本:
    # ./vpn10.sh nZbVGBuX5dtturD

    其中 nZbVGBuX5dtturD 是生成的连接 ID 这里

在远程节点上进行同样的操作,指定对应的隧道内部IP地址和连接ID。

要在打开时自动运行脚本,我使用文件 /etc/ 中包含的命令“nohup /<脚本的路径>/vpn10.sh nZbVGBuX5dtturD > /var/log/vpn10.log 2>/dev/null &”本地文件

结论

该脚本可以工作,并在 Ubuntu(18.04、19.10、20.04)和 Debian 9 上进行了测试。您可以使用任何其他服务作为发送器,但为了获得经验,我使用了 Yandex.disk。
在实验过程中,我们发现某些类型的 NAT 提供商不允许建立连接。 主要来自阻止种子下载的移动运营商。

我计划在以下方面进行改进:

  • 每次启动时自动生成secret.key,加密并复制到Yandex.disk以传输到远程节点(更新版本中考虑)
  • 自动分配接口IP地址
  • 在上传到 Yandex.disk 之前加密数据
  • 代码优化

让每个家庭都有IPv6!

更新! 最新的文件和DEB包在这里 - yandex.磁盘

来源: habr.com

添加评论