Continua sul CommunityHelpWiki (sullo stesso dominio di come sono riuscito a organizzare un tunnel diretto VPNtra due computer che si trovano dietro i NAT dei provider. Nell'articolo precedente ho descritto il processo di connessione tramite una terza parte - un intermediario (noleggiato VPS che fungeva da qualcosa come un server STUN e un trasmettitore di dati dei nodi per la connessione). In questo articolo parlerò di come me la sono cavata senza VPS, ma gli intermediari sono rimasti e sono stati il server STUN e Yandex.Disk...
Introduzione
Leggendo i commenti al post precedente, ho capito che il principale svantaggio dell'implementazione era l'uso di un intermediario - una terza parte (VPS) che indicava i parametri attuali del nodo, dove e come collegarsi. Tenendo conto delle raccomandazioni di utilizzare un vero STUN (ce ne sono moltissimi) per determinare i parametri correnti di connessione. La prima cosa che ho deciso di fare è stata controllare, utilizzando TCPDump, il contenuto dei pacchetti durante il lavoro del server STUN con i client e ho ottenuto un contenuto del tutto illeggibile. Cercando su Google il protocollo, ho trovato un articolo che descrive il protocollo. Ho capito che non potevo implementare autonomamente la richiesta al server STUN e ho messo da parte l'idea in un "cassetto lontano".
Teoria
Recentemente ho dovuto installare un server STUN su Debian da un pacchetto
# apt install stun-servere nelle dipendenze ho visto il pacchetto stun-client, ma non ci ho dato molto peso. Ma poi ho ricordato il pacchetto stun-client e ho deciso di capire come funziona, cercando su Google e Yandex, ho ottenuto:
# apt install stun-client
# stun stun.ekiga.net -p 21234 -v
In risposta ho ricevuto:
STUN client version 0.97
Porta 21234 aperta con fd 3
Porta 21235 aperta con fd 4
Codifica messaggio stun:
Codifica ChangeRequest: 0Pronto a inviare msg di lunghezza 28 a 216.93.246.18:3478
Codifica messaggio stun:
Codifica ChangeRequest: 4Pronto a inviare msg di lunghezza 28 a 216.93.246.18:3478
Codifica messaggio stun:
Codifica ChangeRequest: 2Pronto a inviare msg di lunghezza 28 a 216.93.246.18:3478
Messaggio stun ricevuto: 92 byte
MappedAddress = :2885
SourceAddress = 216.93.246.18:3478
ChangedAddress = 216.93.246.17:3479
Attributo sconosciuto: 32800
ServerName = Vovida.org 0.98-CPC
Messaggio ricevuto di tipo 257 id=1
Codifica messaggio stun:
Codifica ChangeRequest: 0Pronto a inviare msg di lunghezza 28 a 216.93.246.17:3478
Codifica messaggio stun:
Codifica ChangeRequest: 4Pronto a inviare msg di lunghezza 28 a 216.93.246.18:3478
Codifica messaggio stun:
Codifica ChangeRequest: 2Pronto a inviare msg di lunghezza 28 a 216.93.246.18:3478
Codifica messaggio stun:
Codifica ChangeRequest: 0Pronto a inviare msg di lunghezza 28 a :2885
Messaggio stun ricevuto: 28 byte
ChangeRequest = 0
Messaggio ricevuto di tipo 1 id=11
Codifica messaggio stun:
Codifica ChangeRequest: 0Pronto a inviare msg di lunghezza 28 a 216.93.246.17:3478
Codifica messaggio stun:
Codifica ChangeRequest: 4Pronto a inviare msg di lunghezza 28 a 216.93.246.18:3478
Codifica messaggio stun:
Codifica ChangeRequest: 2Pronto a inviare msg di lunghezza 28 a 216.93.246.18:3478
Messaggio stun ricevuto: 92 byte
MappedAddress = :2885
SourceAddress = 216.93.246.17:3479
ChangedAddress = 216.93.246.18:3478
Attributo sconosciuto: 32800
ServerName = Vovida.org 0.98-CPC
Messaggio ricevuto di tipo 257 id=10
Codifica messaggio stun:
Codifica ChangeRequest: 4Pronto a inviare msg di lunghezza 28 a 216.93.246.18:3478
Codifica messaggio stun:
Codifica ChangeRequest: 2Pronto a inviare msg di lunghezza 28 a 216.93.246.18:3478
Codifica messaggio stun:
Codifica ChangeRequest: 4Pronto a inviare msg di lunghezza 28 a 216.93.246.18:3478
Codifica messaggio stun:
Codifica ChangeRequest: 2Pronto a inviare msg di lunghezza 28 a 216.93.246.18:3478
Codifica messaggio stun:
Codifica ChangeRequest: 4Pronto a inviare msg di lunghezza 28 a 216.93.246.18:3478
Codifica messaggio stun:
Codifica ChangeRequest: 2Pronto a inviare msg di lunghezza 28 a 216.93.246.18:3478
Codifica messaggio stun:
Codifica ChangeRequest: 4Pronto a inviare msg di lunghezza 28 a 216.93.246.18:3478
Codifica messaggio stun:
Codifica ChangeRequest: 2Pronto a inviare msg di lunghezza 28 a 216.93.246.18:3478
Codifica messaggio stun:
Codifica ChangeRequest: 4Pronto a inviare msg di lunghezza 28 a 216.93.246.18:3478
Codifica messaggio stun:
Codifica ChangeRequest: 2Pronto a inviare msg di lunghezza 28 a 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
Il valore restituito è 0x000006
Stringa con valore
MappedAddress = :2885
è proprio ciò che ci vuole! Mostrava lo stato attuale per la connessione sulla porta UDP locale 21234. Ma questo è solo metà del lavoro, è emersa la domanda su come trasferire questi dati al nodo remoto e organizzare una connessione VPN. L'uso di un protocollo di posta, o forse Telegram?! Ci sono molte opzioni e ho deciso di usare Yandex.Disk, dato che mi era apparso. articolo sul funzionamento di Curl tramite WebDav con Yandex.Disk. Riflettendo sull'implementazione, sono giunto a questo schema:
- Segnalare la disponibilità dei nodi per la connessione tramite la presenza di un determinato file con un timestamp su Yandex.Disk;
- Se i nodi sono pronti, ottenere i parametri attuali dal server STUN;
- Caricare i parametri attuali su Yandex.Disk;
- Controllare la presenza e leggere i parametri del nodo remoto dal file su Yandex.Disk;
- Stabilire una connessione con il nodo remoto tramite OpenVPN.
Pratica
Dopo averci pensato un po', considerando l'esperienza del precedente articolo, ho scritto rapidamente uno script. Ci servirà:
# apt install openvpn stun-client curl Lo script stesso:
Versione iniziale
# 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
donePer far funzionare lo script è necessario:
- Copiare negli appunti e incollare in un editor, ad esempio:
# nano vpn8.sh - indicare il login e la password di Yandex.Disk.
- nel campo «—ifconfig 10.45.54.(1 o 2) 255.255.255.252» indicare l'indirizzo IP interno dell'interfaccia
- creare secret.key con il comando:
# openvpn --genkey --secret secret.key - rendere lo script eseguibile:
# chmod +x vpn8.sh - avviare lo script:
# ./vpn8.sh nZbVGBuX5dtturDdove nZbVGBuX5dtturD è l'ID della connessione generato qui
Sul nodo remoto eseguire tutto allo stesso modo a parte la generazione di secret.key e ID della connessione, che devono essere identici.
Versione aggiornata (per un corretto funzionamento il tempo deve essere sincronizzato):
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
Per far funzionare lo script è necessario:
- Copiare negli appunti e incollare in un editor, ad esempio:
# nano vpn10.sh - indicare il login (2° riga) e la password di Yandex.Disk (3° riga).
- indicare l'indirizzo IP interno del tunnel (4° riga).
- rendere lo script eseguibile:
# chmod +x vpn10.sh - avviare lo script:
# ./vpn10.sh nZbVGBuX5dtturDdove nZbVGBuX5dtturD è l'ID della connessione generato qui
Sul nodo remoto eseguire tutto allo stesso modo, indicare il corrispondente interno indirizzo IP del tunnel e l'ID della connessione.
Per l'avvio automatico dello script all'accensione utilizzo il comando «nohup /<percorso dello script>/vpn10.sh nZbVGBuX5dtturD > /var/log/vpn10.log 2>/dev/null &» contenuto nel file /etc/rc.local
Conclusione
Lo script funziona, testato su Ubuntu (18.04, 19.10, 20.04) e Debian 9. Come trasmettitore si può utilizzare qualsiasi altro servizio, ma per esperienza ho usato Yandex.Disk.
Durante gli esperimenti è stato scoperto che alcuni tipi di NAT dei provider non consentono di stabilire una connessione. Soprattutto da parte degli operatori mobili, dove i torrent sono bloccati.
Ho intenzione di migliorare nei seguenti aspetti:
- Generazione automatica di secret.key ogni volta che si avvia, crittografia e copia su Yandex.Disk per la trasmissione al nodo remoto (tenuto in considerazione nella versione aggiornata)
- Assegnazione automatica degli indirizzi IP delle interfacce
- Crittografia dei dati prima del caricamento su Yandex.Disk
- Ottimizzazione del codice
Che ci sia IPv6 in ogni casa!
Aggiornato! Ultimi file e pacchetto DEB qui — yandex.disk
Fonte: habr.com
