ProHoster > блог > адміністраванне > Прамы VPN-тунэль паміж кампутарамі праз NAT'ы правайдэраў (без VPS, з дапамогай STUN-сервера і Яндэкс.дыска)
Прамы VPN-тунэль паміж кампутарамі праз NAT'ы правайдэраў (без VPS, з дапамогай STUN-сервера і Яндэкс.дыска)
Працяг артыкулы аб тым, як мне атрымалася арганізаваць прамы VPN-тунэль паміж двума кампутарамі змешчанымі за NAT'амі правайдэраў. У мінулым артыкуле апісваўся працэс арганізацыі злучэння з дапамогай трэцяга боку – пасярэдніка (арандаваны VPS які выконвае ролю, нешта тыпу STUN-сервера і перадатчыка дадзеных вузлоў для злучэння). У гэтым артыкуле я раскажу як абыйшоўся без VPS, але пасярэднікі засталіся і імі былі STUN-сервер і Яндэкс.Дыск…
Увядзенне
Прачытаўшы каментары мінулага паста я зразумеў, што галоўным недахопам рэалізацыі было выкарыстанне пасярэдніка – трэцяга боку (VPS) якая паказвала бягучыя параметры вузла, куды і як падлучацца. Улічваючы рэкамендацыі выкарыстоўваць сапраўдны STUN (якіх вельмі шмат) для вызначэння бягучых параметраў падключэння. Перш за ўсё я вырашыў паглядзець пры дапамозе TCPDump змесціва пакетаў пры працы STUN-сервера з кліентамі і атрымаў зусім нечытэльнае змесціва. Пагугліўшы пратакол натыкнуўся на артыкул з апісаннем пратакола. Я зразумеў што самастойна рэалізаваць запыт да STUN-серверу я не магу і прыбраў задумку ў "далёкую скрыню".
Тэорыя
Нядаўна мне прыйшлося ўсталёўваць STUN-сервер на Debian з пакета.
# apt install stun-server
і ў залежнасцях я ўбачыў пакет stun-client, але неяк не надаў гэтаму значэння. Але пазней я ўспомніў пра пакет stun-client і вырашыў разабрацца як ён працуе, трошкі і пояндексив я атрымаў:
STUN client version 0.97
Opened port 21234 with fd 3
Opened port 21235 with fd 4
Encoding stun message:
Encoding ChangeRequest: 0
About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 4
About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 2
About to send msg of len 28 to 216.93.246.18:3478
Вылучаны штун message: 92 bytes
MappedAddress = <Мой IP>:2885
SourceAddress = 216.93.246.18:3478
ChangedAddress = 216.93.246.17:3479
Unknown attribute: 32800
ServerName = Vovida.org 0.98-CPC
Атрыманы нумар тыпу 257 id=1
Encoding stun message:
Encoding ChangeRequest: 0
About to send msg of len 28 to 216.93.246.17:3478
Encoding stun message:
Encoding ChangeRequest: 4
About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 2
About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 0
About to send msg of len 28 to <Мой IP>:2885
Вылучаны штун message: 28 bytes
ChangeRequest = 0
Атрыманы нумар тыпу 1 id=11
Encoding stun message:
Encoding ChangeRequest: 0
About to send msg of len 28 to 216.93.246.17:3478
Encoding stun message:
Encoding ChangeRequest: 4
About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 2
About to send msg of len 28 to 216.93.246.18:3478
Вылучаны штун message: 92 bytes
MappedAddress = <Мой IP>:2885
SourceAddress = 216.93.246.17:3479
ChangedAddress = 216.93.246.18:3478
Unknown attribute: 32800
ServerName = Vovida.org 0.98-CPC
Атрыманы нумар тыпу 257 id=10
Encoding stun message:
Encoding ChangeRequest: 4
About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 2
About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 4
About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 2
About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 4
About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 2
About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 4
About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 2
About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 4
About to send msg of len 28 to 216.93.246.18:3478
Encoding stun message:
Encoding ChangeRequest: 2
About 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
Радок са значэннем
MappedAddress = <Мой IP>:2885
якраз тое, што трэба! Яна адлюстроўвала бягучы стан для злучэння на лакальным UDP порце 21234. Але гэта ўсяго толькі падлога справы, паўстала пытанне як перадаць гэтыя дадзеныя выдаленаму вузлу і арганізаваць VPN-злучэнне. Выкарыстанне паштовага пратакола, а можа Telegram?! Варыянтаў шмат і вырашыў выкарыстоўваць Яндекс.дыск, бо траплялася мне артыкул аб працы Curl праз WebDav з Яндэкс.дыскам. Падумаўшы над рэалізацыяй я прыйшоў да такой схемы:
Сігналізаваць аб гатоўнасці вузлоў да ўстаноўкі злучэння наяўнасцю пэўнага файла з часовай пазнакай на Яндэкс.дыску;
Калі вузлы гатовы, то атрымліваць бягучыя параметры ад STUN-сервера;
выгружаць бягучыя параметры на Яндекс.дыск;
Правяраць наяўнасць і счытваць параметры выдаленага вузла з файла на Яндэкс.дыску;
Ўстаноўка злучэння з выдаленым вузлом з дапамогай 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
Для працы скрыпту трэба:
Скапіяваць у буфер і ўставіць у рэдактар, напрыклад:
# nano vpn8.sh
пазначыць лагін і пароль ад Яндекс.дыска.
у полі "—ifconfig 10.45.54.(1 або 2) 255.255.255.252" пазначыць унутраны IP-адрас інтэрфейсу
На выдаленым вузле вырабіць усё тое ж самае, паказаць які адпавядае ўнутраны IP-адрас тунэля і ID-злучэнні.
Для аўтазапуску скрыпту пры ўключэнні я выкарыстоўваю каманду "nohup /<шлях да скрыпту>/vpn10.sh nZbVGBuX5dtturD > /var/log/vpn10.log 2>/dev/null &" якая змяшчаецца ў файле /etc/rc.local
Заключэнне
Скрыпт працуе, правераны на Ubuntu (18.04, 19.10, 20.04) і Debian 9. У якасці перадатчыка можна выкарыстоўваць любы іншы сэрвіс, але для досведу я выкарыстаў Яндэкс.дыск.
У ходзе эксперыментаў было выяўлена, што некаторыя тыпы NAT правайдэраў не дазваляюць арганізаваць злучэнне. У асноўным у сотавых аператараў, дзе заблакаваны торэнты.
Планую дапрацаваць у плане:
Аўтаматычнай генерацыі secret.key кожны раз пры старце, шыфраванні і капіяванні на Яндекс.диск для перадачы на выдалены вузел (Улічана ў абноўленым варыянце)
Аўтаматычнага прызначэння IP-адрасоў інтэрфейсаў
Шыфраванні даных перад выгрузкай на Яндекс.диск
Аптымізацыя кода
Ды будзе IPv6 у кожным доме!
Абноўлена! Апошнія файлы і DEB-пакет тут - yandex.disk