Suite article sur la façon dont j'ai réussi à organiser un tunnel direct entre deux ordinateurs situés derrière des NAT de fournisseurs. Dans le précédent article, je décrivais le processus d'établissement d'une connexion via un intermédiaire — un VPS loué agissant comme quelque chose comme un serveur STUN et un transmetteur de données pour la connexion. VPNDans cet article, je vais expliquer comment je me suis passé d'un VPS, mais les intermédiaires sont restés, et ils étaient un serveur STUN et Yandex.Disk... VPS Après avoir lu les commentaires du dernier post, j'ai compris que le principal inconvénient de la mise en œuvre était l'utilisation d'un intermédiaire — une tierce partie (VPS) qui spécifiait les paramètres actuels du nœud, où et comment se connecter. Tenant compte des recommandations d'utiliser un véritable STUN (
Introduction
dont il y en a beaucoup) pour déterminer les paramètres de connexion actuels. Tout d'abord, j'ai décidé d'examiner avec TCPDump le contenu des paquets lors du fonctionnement du serveur STUN avec les clients et j'ai obtenu un contenu complètement illisible. En cherchant des informations sur le protocole, je suis tombé surun article décrivant le protocole. J'ai compris que je ne pouvais pas réaliser une requête au serveur STUN moi-même et j'ai mis ce projet dans un « tiroir lointain ».Récemment, j'ai dû installer un serveur STUN sur Debian à partir du paquet
Théorie
et dans les dépendances, j'ai vu le paquet stun-client, mais je n'y ai pas trop prêté attention. Mais plus tard, je me suis souvenu du paquet stun-client et j'ai décidé de comprendre comment il fonctionnait, en cherchant sur Google et Yandex j'ai obtenu :
# apt install stun-serverEn réponse, j'ai reçu :
# apt install stun-client
# stun stun.ekiga.net -p 21234 -v
Version du client STUN 0.97
Port 21234 ouvert avec fd 3
Port 21235 ouvert avec fd 4
Encodage du message STUN :
Encodage ChangeRequest : 0
Sur le point d'envoyer un msg de len 28 à 216.93.246.18:3478Encodage ChangeRequest : 4
Encodage ChangeRequest : 0
Encodage ChangeRequest : 2Encodage ChangeRequest : 4
Encodage ChangeRequest : 0
Message STUN reçu : 92 octetsEncodage ChangeRequest : 4
MappedAddress = :2885
SourceAddress = 216.93.246.18:3478
ChangedAddress = 216.93.246.17:3479
Attribut inconnu : 32800
ServerName = Vovida.org 0.98-CPC
Message reçu de type 257 id=1
Sur le point d'envoyer un msg de len 28 à 216.93.246.17:3478
Encodage ChangeRequest : 0
Sur le point d'envoyer un msg de len 28 à 216.93.246.18:3478Sur le point d'envoyer un msg de len 28 à :2885
Encodage ChangeRequest : 0
Encodage ChangeRequest : 2Encodage ChangeRequest : 4
Encodage ChangeRequest : 0
Message STUN reçu : 92 octetsEncodage ChangeRequest : 4
Encodage ChangeRequest : 0
Sur le point d'envoyer un msg de len 28 à 216.93.246.18:3478Message STUN reçu : 28 octets
ChangeRequest = 0
Message reçu de type 1 id=11
SourceAddress = 216.93.246.17:3479
Encodage ChangeRequest : 0
Sur le point d'envoyer un msg de len 28 à 216.93.246.18:3478Sur le point d'envoyer un msg de len 28 à :2885
Encodage ChangeRequest : 0
Encodage ChangeRequest : 2Encodage ChangeRequest : 4
Encodage ChangeRequest : 0
Message STUN reçu : 92 octetsEncodage ChangeRequest : 4
MappedAddress = :2885
SourceAddress = 216.93.246.18:3478
ChangedAddress = 216.93.246.18:3478
Message reçu de type 257 id=10
ServerName = Vovida.org 0.98-CPC
Message reçu de type 257 id=1
test I = 1
Encodage ChangeRequest : 0
Encodage ChangeRequest : 2Encodage ChangeRequest : 4
Encodage ChangeRequest : 0
Message STUN reçu : 92 octetsEncodage ChangeRequest : 4
Encodage ChangeRequest : 0
Encodage ChangeRequest : 2Encodage ChangeRequest : 4
Encodage ChangeRequest : 0
Message STUN reçu : 92 octetsEncodage ChangeRequest : 4
Encodage ChangeRequest : 0
Encodage ChangeRequest : 2Encodage ChangeRequest : 4
Encodage ChangeRequest : 0
Message STUN reçu : 92 octetsEncodage ChangeRequest : 4
Encodage ChangeRequest : 0
Encodage ChangeRequest : 2Encodage ChangeRequest : 4
Encodage ChangeRequest : 0
Message STUN reçu : 92 octetsEncodage ChangeRequest : 4
Encodage ChangeRequest : 0
Encodage ChangeRequest : 2Encodage ChangeRequest : 4
Encodage ChangeRequest : 0
Message STUN reçu : 92 octetsEncodage ChangeRequest : 4
test II = 0
test III = 0
test I(2) = 1
is nat = 1
mapped IP same = 1
hairpin = 1
preserver port = 0
Primary : Mapping Indépendant, Filtre Dépendant du Port, port aléatoire, va hairpin
La valeur de retour est 0x000006
Une chaîne avec la valeur
Строка со значением
SourceAddress = 216.93.246.18:3478
exactement ce qu'il faut ! Elle affichait l'état actuel de la connexion sur le port UDP local 21234. Mais ce n'est que la moitié du chemin, la question s'est posée de savoir comment transmettre ces données à un nœud distant et organiser une connexion VPN. Utiliser un protocole de messagerie, ou peut-être Telegram ?! Les options ne manquent pas et j'ai décidé d'utiliser Yandex.Disk, car j'ai trouvé cela utile. un article sur l'utilisation de Curl via WebDav avec Yandex.Disk. En réfléchissant à la mise en œuvre, j'en suis arrivé à un schéma comme celui-ci :
- Signaler la disponibilité des nœuds pour établir une connexion par la présence d'un certain fichier avec un horodatage sur Yandex.Disk ;
- Si les nœuds sont prêts, récupérer les paramètres actuels du serveur STUN ;
- Télécharger les paramètres actuels sur Yandex.Disk ;
- Vérifier la disponibilité et lire les paramètres du nœud distant à partir du fichier sur Yandex.Disk ;
- Établir une connexion avec le nœud distant à l'aide d'OpenVPN.
Pratique
Après avoir réfléchi, en tenant compte de l'expérience de l'article précédent, j'ai rapidement rédigé un script. Nous aurons besoin de :
# apt install openvpn stun-client curl Voici le script :
Version initiale
# 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
donePour faire fonctionner le script, il faut :
- Copier dans le presse-papiers et coller dans un éditeur, par exemple :
# nano vpn8.sh - indiquer le nom d'utilisateur et le mot de passe de Yandex.Disk.
- dans le champ «—ifconfig 10.45.54.(1 ou 2) 255.255.255.252» indiquer l'adresse IP interne de l'interface
- créer secret.key avec la commande :
# openvpn --genkey --secret secret.key - rendre le script exécutable :
# chmod +x vpn8.sh - lancer le script :
# ./vpn8.sh nZbVGBuX5dtturDoù nZbVGBuX5dtturD est l'ID de connexion généré ici
Sur le nœud distant, effectuer tout de même, à l'exception de la génération de secret.key et de l'ID de connexion, qui doivent être identiques.
Version mise à jour (pour un fonctionnement correct, le temps doit être synchronisé) :
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
Pour faire fonctionner le script, il faut :
- Copier dans le presse-papiers et coller dans un éditeur, par exemple :
# nano vpn10.sh - indiquer le nom d'utilisateur (2ème ligne) et le mot de passe de Yandex.Disk (3ème ligne).
- indiquer l'adresse IP interne du tunnel (4ème ligne).
- rendre le script exécutable :
# chmod +x vpn10.sh - lancer le script :
# ./vpn10.sh nZbVGBuX5dtturDoù nZbVGBuX5dtturD est l'ID de connexion généré ici
Sur le nœud distant, effectuer la même chose, indiquer l'ID de connexion et l'adresse IP interne correspondante. Adresse IP tunnel et ID de connexion.
Pour l'exécution automatique du script lors du démarrage, j'utilise la commande «nohup //vpn10.sh nZbVGBuX5dtturD > /var/log/vpn10.log 2>/dev/null &» contenue dans le fichier /etc/rc.local
Conclusion
Le script fonctionne, testé sur Ubuntu (18.04, 19.10, 20.04) et Debian 9. Tout autre service peut être utilisé comme transmetteur, mais pour l'expérience, j'ai utilisé Yandex.Disk.
Pendant les expériences, il a été découvert que certains types de NAT des fournisseurs ne permettent pas d'organiser une connexion. Principalement chez les opérateurs de téléphonie mobile, où les torrents sont bloqués.
Je prévois d'améliorer dans le domaine :
- Génération automatique de secret.key à chaque démarrage, cryptage et copie sur Yandex.Drive pour transmission à un nœud distant (pris en compte dans la version mise à jour)
- Attribution automatique des adresses IP des interfaces
- Cryptage des données avant l'exportation vers Yandex.Drive
- Optimisation du code
Qu'il y ait des IPv6 dans chaque maison !
Mis à jour ! Les derniers fichiers et le paquet DEB ici — yandex.disk
Source : habr.com
