Vervolg artikel over hoe ik erin slaagde een directe VPN-tunnel op te zetten tussen twee computers die achter de NAT van providers zitten. In het vorige artikel beschreef ik het proces van het opzetten van een verbinding via een derde partij — een tussenpersoon (gehuurd VPS die een rol speelt, iets als een STUN-server en gegevensdoorgever voor de verbinding). In dit artikel vertel ik hoe ik zonder VPS omging, maar de tussenpersonen bleven en dat waren de STUN-server en Yandex.Disk...
Inleiding
Na de opmerkingen van de vorige post te hebben gelezen, begreep ik dat het grootste nadeel van de implementatie het gebruik van een tussenpersoon — een derde partij (VPS) was die de huidige parameters van de knooppunt aangaf, waar en hoe verbinding te maken. Gezien de aanbevelingen om echte STUN te gebruiken (waarvan er heel veel zijn) om de huidige verbindingsparameters te bepalen. Als eerste besloot ik met behulp van TCPDump de inhoud van de pakketten bij de werking van de STUN-server met cliënten te bekijken en kreeg volkomen onleesbare inhoud. Na het googelen van het protocol stuitte ik op een artikel met een beschrijving van het protocol. Ik begreep dat ik zelf geen verzoek naar de STUN-server kon implementeren en zette het idee opzij in een 'verre lade'.
Theorie
Onlangs moest ik een STUN-server op Debian installeren vanuit een pakket
# apt install stun-serveren in de afhankelijkheden zag ik het pakket stun-client, maar ik hechtte er niet veel waarde aan. Later herinnerde ik me echter het pakket stun-client en besloot uit te zoeken hoe het werkte, googelend en yandexend kreeg ik:
# apt install stun-client
# stun stun.ekiga.net -p 21234 -v
Als antwoord kreeg ik:
STUN client versie 0.97
Poort 21234 geopend met fd 3
Poort 21235 geopend met fd 4
STUN-bericht coderen:
ChangeRequest coderen: 0Gaat boodschap van lengte 28 naar 216.93.246.18:3478 verzenden
STUN-bericht coderen:
ChangeRequest coderen: 4Gaat boodschap van lengte 28 naar 216.93.246.18:3478 verzenden
STUN-bericht coderen:
ChangeRequest coderen: 2Gaat boodschap van lengte 28 naar 216.93.246.18:3478 verzenden
STUN-bericht ontvangen: 92 bytes
MappedAddress = :2885
BronAdres = 216.93.246.18:3478
ChangedAddress = 216.93.246.17:3479
Onbekend attribuut: 32800
ServerName = Vovida.org 0.98-CPC
Bericht van type 257 ontvangen id=1
STUN-bericht coderen:
ChangeRequest coderen: 0Gaat boodschap van lengte 28 naar 216.93.246.17:3478 verzenden
STUN-bericht coderen:
ChangeRequest coderen: 4Gaat boodschap van lengte 28 naar 216.93.246.18:3478 verzenden
STUN-bericht coderen:
ChangeRequest coderen: 2Gaat boodschap van lengte 28 naar 216.93.246.18:3478 verzenden
STUN-bericht coderen:
ChangeRequest coderen: 0Gaat boodschap van lengte 28 naar :2885 verzenden
STUN-bericht ontvangen: 28 bytes
ChangeRequest = 0
Bericht van type 1 ontvangen id=11
STUN-bericht coderen:
ChangeRequest coderen: 0Gaat boodschap van lengte 28 naar 216.93.246.17:3478 verzenden
STUN-bericht coderen:
ChangeRequest coderen: 4Gaat boodschap van lengte 28 naar 216.93.246.18:3478 verzenden
STUN-bericht coderen:
ChangeRequest coderen: 2Gaat boodschap van lengte 28 naar 216.93.246.18:3478 verzenden
STUN-bericht ontvangen: 92 bytes
MappedAddress = :2885
BronAdres = 216.93.246.17:3479
ChangedAddress = 216.93.246.18:3478
Onbekend attribuut: 32800
ServerName = Vovida.org 0.98-CPC
Bericht van type 257 ontvangen id=10
STUN-bericht coderen:
ChangeRequest coderen: 4Gaat boodschap van lengte 28 naar 216.93.246.18:3478 verzenden
STUN-bericht coderen:
ChangeRequest coderen: 2Gaat boodschap van lengte 28 naar 216.93.246.18:3478 verzenden
STUN-bericht coderen:
ChangeRequest coderen: 4Gaat boodschap van lengte 28 naar 216.93.246.18:3478 verzenden
STUN-bericht coderen:
ChangeRequest coderen: 2Gaat boodschap van lengte 28 naar 216.93.246.18:3478 verzenden
STUN-bericht coderen:
ChangeRequest coderen: 4Gaat boodschap van lengte 28 naar 216.93.246.18:3478 verzenden
STUN-bericht coderen:
ChangeRequest coderen: 2Gaat boodschap van lengte 28 naar 216.93.246.18:3478 verzenden
STUN-bericht coderen:
ChangeRequest coderen: 4Gaat boodschap van lengte 28 naar 216.93.246.18:3478 verzenden
STUN-bericht coderen:
ChangeRequest coderen: 2Gaat boodschap van lengte 28 naar 216.93.246.18:3478 verzenden
STUN-bericht coderen:
ChangeRequest coderen: 4Gaat boodschap van lengte 28 naar 216.93.246.18:3478 verzenden
STUN-bericht coderen:
ChangeRequest coderen: 2Gaat boodschap van lengte 28 naar 216.93.246.18:3478 verzenden
test I = 1
test II = 0
test III = 0
test I(2) = 1
is nat = 1
Mapped IP hetzelfde = 1
hairpin = 1
preserver port = 0
Primair: Onafhankelijke mapping, poortafhankelijke filter, willekeurige poort, zal hairpin
Retourwaarde is 0x000006
String met waarde
MappedAddress = :2885
precies wat ik nodig had! Het toonde de huidige status van de verbinding op de lokale UDP-poort 21234. Maar dit is slechts de helft van het werk; de vraag is hoe deze gegevens naar een externe node te verzenden en een VPN-verbinding op te zetten. Het gebruik van een mailprotocol, of misschien Telegram?! Er zijn veel opties en ik besloot Yandex.Disk te gebruiken, omdat ik dat tegenkwam. een artikel over het werken met Curl via WebDav met Yandex.Disk. Na te hebben nagedacht over de implementatie, kwam ik tot het volgende schema:
- Signaleer de gereedheid van de nodes voor het opzetten van de verbinding door een bepaald bestand met een tijdstempel op Yandex.Disk te plaatsen;
- Als de nodes gereed zijn, verkrijg dan de huidige parameters van de STUN-server;
- Upload de huidige parameters naar Yandex.Disk;
- Controleer op de aanwezigheid en lees de parameters van de externe node uit het bestand op Yandex.Disk;
- Stel de verbinding met de externe node in met behulp van OpenVPN.
Praktijk
Na wat nagedacht te hebben, schreef ik snel een script, rekening houdend met de ervaring van het vorige artikel. We hebben nodig:
# apt install openvpn stun-client curl Eigenlijk is hier het script:
Initiële variant
# 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
doneVoor het werken van het script is nodig:
- Kopieer naar het klembord en plak in een editor, bijvoorbeeld:
# nano vpn8.sh - geef de gebruikersnaam en het wachtwoord van Yandex.Disk op.
- in het veld "--ifconfig 10.45.54.(1 of 2) 255.255.255.252" het interne IP-adres van de interface opgeven
- creëer secret.key met het commando:
# openvpn --genkey --secret secret.key - maak het script uitvoerbaar:
# chmod +x vpn8.sh - start het script:
# ./vpn8.sh nZbVGBuX5dtturDwaar nZbVGBuX5dtturD — het gegenereerde ID van de verbinding is here
Voer aan de externe node hetzelfde uit, met uitzondering van het genereren van secret.key en het verbinding-ID; deze moeten identiek zijn.
Bijgewerkte variant (voor correcte werking moet de tijd gesynchroniseerd zijn):
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
Voor het werken van het script is nodig:
- Kopieer naar het klembord en plak in een editor, bijvoorbeeld:
# nano vpn10.sh - geef de gebruikersnaam op (regel 2) en het wachtwoord van Yandex.Disk (regel 3).
- geef het interne IP-adres van de tunnel op (regel 4).
- maak het script uitvoerbaar:
# chmod +x vpn10.sh - start het script:
# ./vpn10.sh nZbVGBuX5dtturDwaar nZbVGBuX5dtturD — het gegenereerde ID van de verbinding is here
Voer hetzelfde uit op de externe node, geef het overeenkomstige interne IP-adres tunnel- en verbinding-ID op.
Voor het automatisch starten van het script bij het aanzetten gebruik ik het commando "nohup //vpn10.sh nZbVGBuX5dtturD > /var/log/vpn10.log 2>/dev/null &" dat in het bestand /etc/rc.local staat.
Conclusie
Het script werkt, getest op Ubuntu (18.04, 19.10, 20.04) en Debian 9. Je kunt elke andere service als zender gebruiken, maar voor de ervaring heb ik Yandex.Disk gebruikt.
Tijdens experimenten werd ontdekt dat sommige types NAT van providers geen verbinding mogelijk maken. Dit komt vooral voor bij mobiele operators, waar torrents zijn geblokkeerd.
Ik ben van plan om verbeteringen aan te brengen in de volgende opzichten:
- Automatische generatie van secret.key elke keer bij opstarten, encryptie en kopiëren naar Yandex.disk voor overdracht naar een externe node (inbegrepen in de bijgewerkte versie)
- Automatische toewijzing van IP-adressen voor interfaces
- Encryptie van gegevens voordat ze naar Yandex.disk worden geüpload
- Optimalisatie van de code
Laat er IPv6 in elk huis zijn!
Bijgewerkt! Laatste bestanden en DEB-pakket hier — yandex.disk
Bron: habr.com
