Fortsättning artiklar om hur jag lyckades organisera en direkt VPN-tunnel mellan två datorer som befinner sig bakom internetleverantörernas NAT:ar. I den tidigare artikeln beskrevs processen för att ordna en koppling med hjälp av en tredje part – en medlare (hyrd VPS som spelar rollen av något som en STUN-server och dataöverföringsnod för anslutning). I denna artikel kommer jag att berätta hur jag klara mig utan VPS, men medlarna kvarstod och de var STUN-server och Yandex.Disk...
Introduktion
När jag läste kommentarerna till det förra inlägget insåg jag att huvudnackdelen i implementeringen var användningen av en medlare – en tredje part (VPS) som angav aktuella nodparametrar, var och hur man skulle ansluta. Med tanke på rekommendationer om att använda riktig STUN (som finns i överflöd) för att bestämma aktuella anslutningsparametrar. Först beslutade jag att titta på innehållet i paketen när STUN-servern arbetade med klienterna med hjälp av TCPDump och fick helt oläsligt innehåll. Genom att söka på protokollet snubblade jag över en artikel som beskriver protokollet. Jag insåg att jag inte kunde implementera en begäran till STUN-servern själv och lade tanken åt sidan.
Teori
Nyligen var jag tvungen att installera STUN-server på Debian från paketet
# apt install stun-serveroch i beroenden såg jag paketet stun-client, men gav det inte mycket uppmärksamhet. Men senare kom jag att tänka på paketet stun-client och bestämde mig för att förstå hur det fungerar, genom att söka på Google och Yandex fick jag:
# apt install stun-client
# stun stun.ekiga.net -p 21234 -v
Som svar fick jag:
STUN-klientversion 0.97
Öppnade port 21234 med fd 3
Öppnade port 21235 med fd 4
Kodning av STUN-meddelandet:
Kodning ChangeRequest: 0Är på väg att skicka meddelande av längd 28 till 216.93.246.18:3478
Kodning av STUN-meddelandet:
Kodning ChangeRequest: 4Är på väg att skicka meddelande av längd 28 till 216.93.246.18:3478
Kodning av STUN-meddelandet:
Kodning ChangeRequest: 2Är på väg att skicka meddelande av längd 28 till 216.93.246.18:3478
Mottaget STUN-meddelande: 92 byte
MappedAddress = :2885
SourceAddress = 216.93.246.18:3478
ChangedAddress = 216.93.246.17:3479
Okänt attribut: 32800
ServerName = Vovida.org 0.98-CPC
Mottaget meddelande av typ 257 id=1
Kodning av STUN-meddelandet:
Kodning ChangeRequest: 0Är på väg att skicka meddelande av längd 28 till 216.93.246.17:3478
Kodning av STUN-meddelandet:
Kodning ChangeRequest: 4Är på väg att skicka meddelande av längd 28 till 216.93.246.18:3478
Kodning av STUN-meddelandet:
Kodning ChangeRequest: 2Är på väg att skicka meddelande av längd 28 till 216.93.246.18:3478
Kodning av STUN-meddelandet:
Kodning ChangeRequest: 0Är på väg att skicka meddelande av längd 28 till :2885
Mottaget STUN-meddelande: 28 byte
ChangeRequest = 0
Mottaget meddelande av typ 1 id=11
Kodning av STUN-meddelandet:
Kodning ChangeRequest: 0Är på väg att skicka meddelande av längd 28 till 216.93.246.17:3478
Kodning av STUN-meddelandet:
Kodning ChangeRequest: 4Är på väg att skicka meddelande av längd 28 till 216.93.246.18:3478
Kodning av STUN-meddelandet:
Kodning ChangeRequest: 2Är på väg att skicka meddelande av längd 28 till 216.93.246.18:3478
Mottaget STUN-meddelande: 92 byte
MappedAddress = :2885
SourceAddress = 216.93.246.17:3479
ChangedAddress = 216.93.246.18:3478
Okänt attribut: 32800
ServerName = Vovida.org 0.98-CPC
Mottaget meddelande av typ 257 id=10
Kodning av STUN-meddelandet:
Kodning ChangeRequest: 4Är på väg att skicka meddelande av längd 28 till 216.93.246.18:3478
Kodning av STUN-meddelandet:
Kodning ChangeRequest: 2Är på väg att skicka meddelande av längd 28 till 216.93.246.18:3478
Kodning av STUN-meddelandet:
Kodning ChangeRequest: 4Är på väg att skicka meddelande av längd 28 till 216.93.246.18:3478
Kodning av STUN-meddelandet:
Kodning ChangeRequest: 2Är på väg att skicka meddelande av längd 28 till 216.93.246.18:3478
Kodning av STUN-meddelandet:
Kodning ChangeRequest: 4Är på väg att skicka meddelande av längd 28 till 216.93.246.18:3478
Kodning av STUN-meddelandet:
Kodning ChangeRequest: 2Är på väg att skicka meddelande av längd 28 till 216.93.246.18:3478
Kodning av STUN-meddelandet:
Kodning ChangeRequest: 4Är på väg att skicka meddelande av längd 28 till 216.93.246.18:3478
Kodning av STUN-meddelandet:
Kodning ChangeRequest: 2Är på väg att skicka meddelande av längd 28 till 216.93.246.18:3478
Kodning av STUN-meddelandet:
Kodning ChangeRequest: 4Är på väg att skicka meddelande av längd 28 till 216.93.246.18:3478
Kodning av STUN-meddelandet:
Kodning ChangeRequest: 2Är på väg att skicka meddelande av längd 28 till 216.93.246.18:3478
test I = 1
test II = 0
test III = 0
test I(2) = 1
är nat = 1
mapped IP är densamma = 1
hairpin = 1
preserver port = 0
Primär: Oberoende mappning, portberoende filter, random port, kommer att hairpin
Returvärde är 0x000006
Sträng med värdet
MappedAddress = :2885
Det är precis vad som behövs! Den visade det aktuella tillståndet för anslutningen på den lokala UDP-porten 21234. Men det är bara halvvägs, frågan uppstod hur man skulle överföra dessa data till en avlägsen nod och organisera en VPN-anslutning. Att använda e-postprotokollet, eller kanske Telegram?! Det finns många alternativ, och jag bestämde mig för att använda Yandex.Disk, eftersom jag hade stött på det. en artikel om att använda Curl via WebDav med Yandex.Disk. Efter att ha tänkt ut en implementation kom jag fram till följande schema:
- Signalera att noderna är redo att installera en anslutning med hjälp av en specifik fil med tidsstämpel på Yandex.Disk;
- Om noderna är redo, hämta aktuella parametrar från STUN-servern;
- Ladda upp aktuella parametrar till Yandex.Disk;
- Kontrollera närvaron av och läs parametrarna för den avlägsna noden från filen på Yandex.Disk;
- Installera anslutningen med den avlägsna noden med hjälp av OpenVPN.
Praxis
Efter att ha tänkt på det, med hänsyn till erfarenheterna från den tidigare artikeln, skrev jag ett skript i hastigheten. Vi behöver:
# apt install openvpn stun-client curl Själva skriptet:
Den första versionen
# 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
doneFör att skriptet ska fungera krävs:
- Kopiera till urklipp och klistra in i redigeraren, till exempel:
# nano vpn8.sh - ange användarnamn och lösenord för Yandex.Disk.
- i fältet "—ifconfig 10.45.54.(1 eller 2) 255.255.255.252" ange den interna IP-adressen för gränssnittet
- skapa secret.key med kommandot:
# openvpn --genkey --secret secret.key - göra skriptet körbart:
# chmod +x vpn8.sh - starta skriptet:
# ./vpn8.sh nZbVGBuX5dtturDdär nZbVGBuX5dtturD är den genererade anslutnings-ID:n ). Ett typiskt exempel på användning:
På den avlägsna noden görs samma sak förutom att skapa secret.key och anslutnings-ID:t, dessa måste vara identiska.
Den uppdaterade versionen (för korrekt funktion måste tiden synkroniseras):
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
För att skriptet ska fungera krävs:
- Kopiera till urklipp och klistra in i redigeraren, till exempel:
# nano vpn10.sh - ange användarnamn (rad 2) och lösenord för Yandex.Disk (rad 3).
- ange den interna IP-adressen för tunneln (rad 4).
- göra skriptet körbart:
# chmod +x vpn10.sh - starta skriptet:
# ./vpn10.sh nZbVGBuX5dtturDdär nZbVGBuX5dtturD är den genererade anslutnings-ID:n ). Ett typiskt exempel på användning:
På den avlägsna noden görs samma sak, ange motsvarande interna IP-adress tunnel och anslutnings-ID.
För att automatiskt starta skriptet vid uppstart använder jag kommandot "nohup //vpn10.sh nZbVGBuX5dtturD > /var/log/vpn10.log 2>/dev/null &" som finns i filen /etc/rc.local
Slutsats
Skriptet fungerar, testat på Ubuntu (18.04, 19.10, 20.04) och Debian 9. Andra tjänster kan också användas som sändare, men för erfarenhetens skull använde jag Yandex.Disk.
Under experimenten upptäcktes att vissa typer av NAT-leverantörer inte tillåter att organisera en anslutning. Främst mobiloperatörer, där torrenttrafik är blockerad.
Jag planerar att förbättra det i fråga om:
- Automatisk generering av secret.key varje gång vid start, kryptering och kopiering till Yandex.Disk för överföring till en fjärrnod (beaktat i den uppdaterade versionen)
- Automatisk tilldelning av IP-adresser till gränssnitt
- Kryptering av data innan den laddas upp till Yandex.Disk
- Kodoptimering
Låt IPv6 finnas i varje hem!
Uppdaterad! Senaste filer och DEB-paket här — yandex.disk
Källa: habr.com
