Tunnel VPN direct entre ordinateurs via des fournisseurs NAT (sans VPS, en utilisant un serveur STUN et Yandex.Disk)

La suite de l' article sur comment j'ai réussi à organiser un tunnel VPN direct entre deux ordinateurs situés derrière des fournisseurs NAT. Le dernier article décrit le processus d'organisation d'une connexion en utilisant un tiers - un intermédiaire (un VPS loué agissant comme quelque chose comme un serveur STUN et un émetteur de données de nœud pour une connexion). Dans cet article je vais vous dire comment faire sans VPS, mais les intermédiaires sont restés et ils étaient le serveur STUN et Yandex.Disk ...


Présentation


Après avoir lu les commentaires du dernier post, j'ai réalisé que le principal inconvénient de la mise en œuvre était l'utilisation d'un intermédiaire - un tiers (VPS) qui indiquait les paramètres actuels du nœud, où et comment se connecter. Compte tenu des recommandations d'utilisation de ce STUN (il y en a beaucoup ) pour déterminer les paramètres de connexion actuels. Tout d'abord, j'ai décidé de regarder le contenu des paquets utilisant TCPDump lorsque le serveur STUN travaillait avec des clients et recevait un contenu complètement illisible. Googler le protocole est tombé sur un article décrivant le protocole . J'ai réalisé que je ne pouvais pas implémenter la demande au serveur STUN moi-même et j'ai mis l'idée dans la «boîte éloignée».

Théorie


J'ai récemment dû installer un serveur STUN sur Debian à partir d'un package
# apt install stun-server 
et dans les dépendances, j'ai vu le paquet stun-client, mais d'une manière ou d'une autre n'y attaché aucune importance. Mais plus tard, je me suis souvenu du package stun-client et j'ai décidé de comprendre comment cela fonctionne, en recherchant Google et Poindeksiv, j'ai reçu:

 # apt install stun-client # stun stun.ekiga.net -p 21234 -v 

En réponse, j'ai reçu:

Client STUN version 0.97
Port ouvert 21234 avec fd 3
Port ouvert 21235 avec fd 4
Encodage du message d'étourdissement:
Encoding ChangeRequest: 0

Sur le point d'envoyer un msg de 28 len au 216.93.246.18 opin478
Encodage du message d'étourdissement:
Encoding ChangeRequest: 4

Sur le point d'envoyer un msg de 28 len au 216.93.246.18 opin478
Encodage du message d'étourdissement:
Encoding ChangeRequest: 2

Sur le point d'envoyer un msg de 28 len au 216.93.246.18 opin478
Message d'étourdissement reçu: 92 octets
MappedAddress = <Mon IP>: 2885
SourceAddress = 216.93.246.18//478
ChangedAddress = 216.93.246.17lla479
Attribut inconnu: 32800
ServerName = Vovida.org 0.98-CPC
Message reçu de type 257 id = 1
Encodage du message d'étourdissement:
Encoding ChangeRequest: 0

Sur le point d'envoyer un msg du len 28 au 216.93.246.17lla478
Encodage du message d'étourdissement:
Encoding ChangeRequest: 4

Sur le point d'envoyer un msg de 28 len au 216.93.246.18 opin478
Encodage du message d'étourdissement:
Encoding ChangeRequest: 2

Sur le point d'envoyer un msg de 28 len au 216.93.246.18 opin478
Encodage du message d'étourdissement:
Encoding ChangeRequest: 0

Sur le point d'envoyer un msg de len 28 à <Mon IP>: 2885
Message d'étourdissement reçu: 28 octets
ChangeRequest = 0
Message reçu de type 1 id = 11
Encodage du message d'étourdissement:
Encoding ChangeRequest: 0

Sur le point d'envoyer un msg du len 28 au 216.93.246.17lla478
Encodage du message d'étourdissement:
Encoding ChangeRequest: 4

Sur le point d'envoyer un msg de 28 len au 216.93.246.18 opin478
Encodage du message d'étourdissement:
Encoding ChangeRequest: 2

Sur le point d'envoyer un msg de 28 len au 216.93.246.18 opin478
Message d'étourdissement reçu: 92 octets
MappedAddress = <Mon IP>: 2885
SourceAddress = 216.93.246.17lla479
ChangedAddress = 216.93.246.18 {478
Attribut inconnu: 32800
ServerName = Vovida.org 0.98-CPC
Message reçu de type 257 id = 10
Encodage du message d'étourdissement:
Encoding ChangeRequest: 4

Sur le point d'envoyer un msg de 28 len au 216.93.246.18 opin478
Encodage du message d'étourdissement:
Encoding ChangeRequest: 2

Sur le point d'envoyer un msg de 28 len au 216.93.246.18 opin478
Encodage du message d'étourdissement:
Encoding ChangeRequest: 4

Sur le point d'envoyer un msg de 28 len au 216.93.246.18 opin478
Encodage du message d'étourdissement:
Encoding ChangeRequest: 2

Sur le point d'envoyer un msg de 28 len au 216.93.246.18 opin478
Encodage du message d'étourdissement:
Encoding ChangeRequest: 4

Sur le point d'envoyer un msg de 28 len au 216.93.246.18 opin478
Encodage du message d'étourdissement:
Encoding ChangeRequest: 2

Sur le point d'envoyer un msg de 28 len au 216.93.246.18 opin478
Encodage du message d'étourdissement:
Encoding ChangeRequest: 4

Sur le point d'envoyer un msg de 28 len au 216.93.246.18 opin478
Encodage du message d'étourdissement:
Encoding ChangeRequest: 2

Sur le point d'envoyer un msg de 28 len au 216.93.246.18 opin478
Encodage du message d'étourdissement:
Encoding ChangeRequest: 4

Sur le point d'envoyer un msg de 28 len au 216.93.246.18 opin478
Encodage du message d'étourdissement:
Encoding ChangeRequest: 2

Sur le point d'envoyer un msg de 28 len au 216.93.246.18 opin478
test I = 1
test II = 0
test III = 0
test I (2) = 1
est nat = 1
IP mappé identique = 1
épingle à cheveux = 1
port du conservateur = 0
Primaire: mappage indépendant, filtre dépendant du port, port aléatoire, épingle à cheveux
La valeur de retour est 0x000006

Chaîne avec valeur
MappedAddress = <Mon IP>: 2885

exactement ce dont vous avez besoin! Il a affiché l'état actuel de la connexion sur le port UDP local 21234. Mais ce n'est que la moitié de la bataille, la question s'est posée de savoir comment transférer ces données vers un hôte distant et établir une connexion VPN. En utilisant le protocole de messagerie, peut-être Telegram?! Il existe de nombreuses options et j'ai décidé d'utiliser Yandex.Disk, car je suis tombé sur un article sur le fonctionnement de Curl via WebDav avec Yandex.Disk . Après avoir réfléchi à la mise en œuvre, je suis arrivé à ce schéma:

  1. Pour signaler que les nœuds sont prêts à établir une connexion par la présence d'un fichier spécifique avec un horodatage sur Yandex.disk;
  2. Si les nœuds sont prêts, récupérez les paramètres actuels du serveur STUN;
  3. Téléchargez les paramètres actuels sur Yandex.Disk;
  4. Vérifier la disponibilité et lire les paramètres d'un site distant à partir d'un fichier sur Yandex.Disk;
  5. Établissez une connexion avec un hôte distant à l'aide d'OpenVPN.

Pratique


Après un peu de réflexion, compte tenu de l'expérience de l'article précédent, j'ai écrit un petit script. Nous aurons besoin de:
 # apt install openvpn stun-client curl 

En fait, le script lui-même:
Option initiale
 # cat vpn8.sh 

 #!/bin/bash ########################    ### WARN='\033[37;1;41m' # END='\033[0m' # RED='\033[0;31m' # ${RED} # GREEN='\033[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 

Pour exécuter le script dont vous avez besoin:
  1. Copiez dans le presse-papiers et collez-le dans l'éditeur, par exemple:
     # nano vpn8.sh 
  2. spécifiez le nom d'utilisateur et le mot de passe de Yandex.Disk.
  3. dans le champ "--ifconfig 10.45.54. (1 ou 2) 255.255.255.252" spécifiez l'adresse IP interne de l'interface
  4. créez secret.key avec la commande:
     # openvpn --genkey --secret secret.key 
  5. rendre le script exécutable:
     # chmod +x vpn8.sh 
  6. exécutez le script:
     # ./vpn8.sh nZbVGBuX5dtturD 

    où nZbVGBuX5dtturD est l'ID de connexion généré ici

Sur le nœud distant, faites de même sauf pour la génération de secret.key et la connexion ID, ils doivent être identiques.

Version mise à jour (pour un fonctionnement correct, l'heure doit être synchronisée):

 cat vpn10.sh 

 #!/bin/bash stuns="stun.sipnet.ru stun.ekiga.net" #  STUN    username=" Login " #   . password=" Password " #   . intip="10.23.22.1" # IP-   WARN='\033[37;1;41m' END='\033[0m' RED='\033[0;31m' GREEN='\033[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 exécuter le script dont vous avez besoin:
  1. Copiez dans le presse-papiers et collez-le dans l'éditeur, par exemple:
     # nano vpn10.sh 
  2. spécifiez l'identifiant (2e ligne) et le mot de passe à partir de Yandex.Disk (3e ligne).
  3. spécifiez l'adresse IP interne du tunnel (4ème ligne).
  4. rendre le script exécutable:
     # chmod +x vpn10.sh 
  5. exécutez le script:
     # ./vpn10.sh nZbVGBuX5dtturD 

    où nZbVGBuX5dtturD est l'ID de connexion généré ici

Sur le nœud distant, faites de même, spécifiez l'adresse IP interne correspondante du tunnel et la connexion ID.

Pour démarrer le script au démarrage, j'utilise la commande "nohup / <chemin d'accès au script> /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 et Debian 9. Vous pouvez utiliser n'importe quel autre service comme émetteur, mais par expérience j'ai utilisé Yandex.Disk.
Au cours des expériences, il a été constaté que certains types de fournisseurs NAT ne permettent pas une connexion. Surtout avec les opérateurs mobiles où les torrents sont bloqués.

Je prévois de finaliser en termes de:
  • La génération automatique de secret.key chaque fois que vous démarrez, cryptez et copiez sur Yandex.Disk pour transférer vers un hôte distant (pris en compte dans la version mise à jour)
  • Attribuer automatiquement des adresses IP d'interface
  • Cryptage des données avant le téléchargement sur Yandex.Disk
  • Optimisation du code

Qu'il y ait IPv6 dans chaque maison!

Source: https://habr.com/ru/post/fr481034/


All Articles