通過提供商的 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.磁碟

來源: www.habr.com

添加評論