Nastavak članaka o tome kako sam uspeo da organizujem direktni VPN-tunel između dva računara koji se nalaze iza NAT-ova provajdera. U prethodnom članku opisan je proces uspostavljanja veze uz pomoć treće strane — posrednika (iznajmljeni VPS koji deluje kao nešto poput STUN servera i prenosi podatke čvorova za povezivanje). U ovom članku ću pričati kako sam prošao bez VPS-a, ali posrednici su ostali, a to su bili STUN server i Yandex.Disk…
Uvod
Pročitao sam komentare na prethodni post i shvatio da je glavni nedostatak implementacije bila upotreba posrednika — treće strane (VPS) koja je ukazivala na trenutne parametre čvora, kako i gde se povezivati. Uzimajući u obzir preporuke za korišćenje pravog STUN (kojih ima mnogo) za određivanje trenutnih parametara povezivanja. Prvo što sam uradio bilo je da pogledam uz pomoć TCPDump sadržaj paketa tokom rada STUN servera sa klijentima i dobio sam potpuno nečitljiv sadržaj. Pretražujući protokol, naišao sam na članak sa opisom protokola. Shvatio sam da samostalno implementirati zahtev ka STUN serveru ne mogu i stavio sam ideju u "daleki fiok".
Teorija
Nedavno sam morao da instaliram STUN server na Debian iz paketa
# apt install stun-serveri u zavisnostima sam video paket stun-client, ali nisam mu poklanjao pažnju. Ali kasnije sam se setio paketa stun-client i odlučio da istražim kako on funkcioniše, pretražujući i googlajući dobio sam:
# apt install stun-client
# stun stun.ekiga.net -p 21234 -v
U odgovoru sam dobio:
STUN client version 0.97
Opened port 21234 with fd 3
Opened port 21235 with fd 4
Encoding stun message:
Encoding ChangeRequest: 0About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 4About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 2About to send msg of len 28 to 216.93.246.18:3478
Received stun message: 92 bytes
MappedAddress = :2885
SourceAddress = 216.93.246.18:3478
ChangedAddress = 216.93.246.17:3479
Unknown attribute: 32800
ServerName = Vovida.org 0.98-CPC
Received message of type 257 id=1
Encoding stun message:
Encoding ChangeRequest: 0About to send msg of len 28 to 216.93.246.17:3478
Encoding stun message:
Encoding ChangeRequest: 4About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 2About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 0About to send msg of len 28 to :2885
Received stun message: 28 bytes
ChangeRequest = 0
Received message of type 1 id=11
Encoding stun message:
Encoding ChangeRequest: 0About to send msg of len 28 to 216.93.246.17:3478
Encoding stun message:
Encoding ChangeRequest: 4About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 2About to send msg of len 28 to 216.93.246.18:3478
Received stun message: 92 bytes
MappedAddress = :2885
SourceAddress = 216.93.246.17:3479
ChangedAddress = 216.93.246.18:3478
Unknown attribute: 32800
ServerName = Vovida.org 0.98-CPC
Received message of type 257 id=10
Encoding stun message:
Encoding ChangeRequest: 4About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 2About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 4About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 2About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 4About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 2About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 4About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 2About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 4About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 2About to send msg of len 28 to 216.93.246.18:3478
test I = 1
test II = 0
test III = 0
test I(2) = 1
is nat = 1
mapped IP same = 1
hairpin = 1
preserver port = 0
Primary: Independent Mapping, Port Dependent Filter, random port, will hairpin
Return value is 0x000006
String with value
MappedAddress = :2885
Baš ono što nam treba! Prikazivala je trenutačno stanje za vezu na lokalnoj UDP portu 21234. No, to je samo pola posla, postavilo se pitanje kako prenijeti ove podatke na udaljeni čvor i organizirati VPN vezu. Koristiti mail protokol, ili možda Telegram?! Opcija je mnogo, a odlučio sam koristiti Yandex.Disk, jer sam ga ranije koristio. članak o radu Curl-a putem WebDav-a s Yandex.Disk-om. Razmišljajući o implementaciji, došao sam do ovakve sheme:
- Signalizirati spremnost čvorova za uspostavljanje veze prisutnošću određenog datoteke s vremenskim obilježjem na Yandex.Disk-u;
- Ako su čvorovi spremni, primiti trenutačne parametre od STUN poslužitelja;
- Izvaditi trenutačne parametre na Yandex.Disk;
- Provjeriti prisutnost i učitati parametre udaljenog čvora iz datoteke na Yandex.Disk-u;
- Uspostaviti vezu s udaljenim čvorom pomoću OpenVPN-a.
Praksa
Malo sam razmišljao, uzimajući u obzir iskustvo iz prošlog članka, napisao sam skriptu na brzinu. Trebat će nam:
# apt install openvpn stun-client curl Here is the script itself:
Prvotni varijanta
# 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
doneZa rad skripte potrebno je:
- Kopirati u međuspremnik i umetnuti u uređivač, na primjer:
# nano vpn8.sh - navesti korisničko ime i lozinku za Yandex.Disk.
- u polju „—ifconfig 10.45.54.(1 ili 2) 255.255.255.252” navesti unutarnju IP adresu sučelja
- napraviti secret.key komandom:
# openvpn --genkey --secret secret.key - učiniti skriptu izvršnom:
# chmod +x vpn8.sh - pokrenuti skriptu:
# ./vpn8.sh nZbVGBuX5dtturDgdje je nZbVGBuX5dtturD — ID veze generiran ovdje
Na udaljenom čvoru učiniti isto osim generiranja secret.key i ID veze, oni moraju biti identični.
Ažurirana verzija (za ispravno funkcioniranje vrijeme mora biti sinkronizirano):
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
Za rad skripte potrebno je:
- Kopirati u međuspremnik i umetnuti u uređivač, na primjer:
# nano vpn10.sh - navesti korisničko ime (2. redak) i lozinku za Yandex.Disk (3. redak).
- navesti unutarnju IP adresu tunela (4. redak).
- učiniti skriptu izvršnom:
# chmod +x vpn10.sh - pokrenuti skriptu:
# ./vpn10.sh nZbVGBuX5dtturDgdje je nZbVGBuX5dtturD — ID veze generiran ovdje
Na udaljenom čvoru učiniti isto, navesti odgovarajući unutarnji IP adresa tunel i ID veze.
Za automatsko pokretanje skripte prilikom uključivanja koristim naredbu „nohup //vpn10.sh nZbVGBuX5dtturD > /var/log/vpn10.log 2>/dev/null &” koja se nalazi u datoteci /etc/rc.local.
Zaključak
Skripta radi, testirana je na Ubuntu (18.04, 19.10, 20.04) i Debian 9. Kao prijenosnik može se koristiti bilo koja druga usluga, ali iz iskustva sam koristio Yandex.Disk.
U procesu eksperimentiranja otkriveno je da neki tipovi NAT-a pružatelja ne dopuštaju uspostavljanje veze. Uglavnom kod mobilnih operatera, gdje su torrenti blokirani.
Planiram doraditi u smislu:
- Automatsko generiranje secret.key svaki put prilikom pokretanja, šifriranje i kopiranje na Yandex.disk za prijenos na udaljeni čvor (Uključeno u ažuriranu verziju)
- Automatsko dodjeljivanje IP adresa sučeljima
- Šifriranje podataka prije prijenosa na Yandex.disk
- Optimizacija koda
Neka IPv6 bude u svakoj kući!
Ažurirano! Najnoviji datoteke i DEB paket ovdje — yandex.disk
Izvor: habr.com
