Continuación artículo sobre cómo logré organizar un túnel directo VPN-entre dos computadoras que se encuentran detrás de NATs de los proveedores. En el artículo anterior se describió el proceso de establecer una conexión mediante un tercero: un intermediario (un VPS alquilado VPS que cumplía el rol de algo así como un servidor STUN y transmisor de datos de nodos para la conexión). En este artículo, explicaré cómo me las arreglé sin VPS, pero los intermediarios permanecieron y fueron el servidor STUN y Yandex.Disk...
Introducción
Tras leer los comentarios de la publicación anterior, entendí que la principal desventaja de la implementación fue el uso de un intermediario: un tercero (VPS) que indicaba los parámetros actuales del nodo, a dónde y cómo conectar. Teniendo en cuenta las recomendaciones de utilizar un verdadero STUN (de los cuales hay muchos) para determinar los parámetros actuales de conexión. Primero decidí ver con TCPDump el contenido de los paquetes mientras trabajaba el servidor STUN con los clientes y obtuve un contenido completamente ilegible. Buscando en Google el protocolo, encontré un artículo con la descripción del protocolo. Entendí que no podía implementar la solicitud al servidor STUN por mi cuenta y guardé la idea en un "cajón lejano".
Teoría
Recientemente, tuve que instalar un servidor STUN en Debian desde el paquete
# apt install stun-servery en las dependencias vi el paquete stun-client, pero no le presté mucha atención. Sin embargo, más tarde recordé el paquete stun-client y decidí investigar cómo funciona, buscando en Google y Yandex, obtuve:
# apt install stun-client
# stun stun.ekiga.net -p 21234 -v
Recibí en respuesta:
Versión del cliente STUN 0.97
Puerto 21234 abierto con fd 3
Puerto 21235 abierto con fd 4
Codificando mensaje STUN:
Codificando ChangeRequest: 0A punto de enviar el msg de longitud 28 a 216.93.246.18:3478
Codificando mensaje STUN:
Codificando ChangeRequest: 4A punto de enviar el msg de longitud 28 a 216.93.246.18:3478
Codificando mensaje STUN:
Codificando ChangeRequest: 2A punto de enviar el msg de longitud 28 a 216.93.246.18:3478
Mensaje STUN recibido: 92 bytes
MappedAddress =:2885
SourceAddress = 216.93.246.18:3478
ChangedAddress = 216.93.246.17:3479
Atributo desconocido: 32800
ServerName = Vovida.org 0.98-CPC
Mensaje recibido de tipo 257 id=1
Codificando mensaje STUN:
Codificando ChangeRequest: 0A punto de enviar el msg de longitud 28 a 216.93.246.17:3478
Codificando mensaje STUN:
Codificando ChangeRequest: 4A punto de enviar el msg de longitud 28 a 216.93.246.18:3478
Codificando mensaje STUN:
Codificando ChangeRequest: 2A punto de enviar el msg de longitud 28 a 216.93.246.18:3478
Codificando mensaje STUN:
Codificando ChangeRequest: 0A punto de enviar el msg de longitud 28 a
:2885
Mensaje STUN recibido: 28 bytes
ChangeRequest = 0
Mensaje recibido de tipo 1 id=11
Codificando mensaje STUN:
Codificando ChangeRequest: 0A punto de enviar el msg de longitud 28 a 216.93.246.17:3478
Codificando mensaje STUN:
Codificando ChangeRequest: 4A punto de enviar el msg de longitud 28 a 216.93.246.18:3478
Codificando mensaje STUN:
Codificando ChangeRequest: 2A punto de enviar el msg de longitud 28 a 216.93.246.18:3478
Mensaje STUN recibido: 92 bytes
MappedAddress =:2885
SourceAddress = 216.93.246.17:3479
ChangedAddress = 216.93.246.18:3478
Atributo desconocido: 32800
ServerName = Vovida.org 0.98-CPC
Mensaje recibido de tipo 257 id=10
Codificando mensaje STUN:
Codificando ChangeRequest: 4A punto de enviar el msg de longitud 28 a 216.93.246.18:3478
Codificando mensaje STUN:
Codificando ChangeRequest: 2A punto de enviar el msg de longitud 28 a 216.93.246.18:3478
Codificando mensaje STUN:
Codificando ChangeRequest: 4A punto de enviar el msg de longitud 28 a 216.93.246.18:3478
Codificando mensaje STUN:
Codificando ChangeRequest: 2A punto de enviar el msg de longitud 28 a 216.93.246.18:3478
Codificando mensaje STUN:
Codificando ChangeRequest: 4A punto de enviar el msg de longitud 28 a 216.93.246.18:3478
Codificando mensaje STUN:
Codificando ChangeRequest: 2A punto de enviar el msg de longitud 28 a 216.93.246.18:3478
Codificando mensaje STUN:
Codificando ChangeRequest: 4A punto de enviar el msg de longitud 28 a 216.93.246.18:3478
Codificando mensaje STUN:
Codificando ChangeRequest: 2A punto de enviar el msg de longitud 28 a 216.93.246.18:3478
Codificando mensaje STUN:
Codificando ChangeRequest: 4A punto de enviar el msg de longitud 28 a 216.93.246.18:3478
Codificando mensaje STUN:
Codificando ChangeRequest: 2A punto de enviar el msg de longitud 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
Primario: Mapeo Independiente, Filtro Dependiente del Puerto, puerto aleatorio, se hará hairpin
El valor de retorno es 0x000006
La cadena con el valor
MappedAddress =
:2885
es justo lo que necesitaba. Mostraba el estado actual para la conexión en el puerto UDP local 21234. Pero eso es solo la mitad del asunto, surgió la cuestión de cómo transmitir estos datos a un nodo remoto y organizar una conexión VPN. ¿Utilizar el protocolo postal, o tal vez Telegram? Hay muchas opciones y decidí usar Yandex.Disk, ya que me había aparecido. Artículo sobre el funcionamiento de Curl a través de WebDav con Yandex.Disk. Tras pensar en la implementación, llegué a este esquema:
- Notificar la disposición de los nodos para establecer conexión mediante la presencia de un archivo con una marca de tiempo en Yandex.Disk;
- Si los nodos están listos, obtener los parámetros actuales del servidor STUN;
- Cargar los parámetros actuales en Yandex.Disk;
- Verificar la existencia y leer los parámetros del nodo remoto desde un archivo en Yandex.Disk;
- Establecer la conexión con el nodo remoto a través de OpenVPN.
Práctica
Después de reflexionar un poco, y teniendo en cuenta la experiencia del artículo anterior, escribí un script de forma rápida. Necesitaremos:
# apt install openvpn stun-client curl El script en sí:
Opción inicial
# 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
donePara que el script funcione, es necesario:
- Copiar al portapapeles y pegar en un editor, por ejemplo:
# nano vpn8.sh - indicar el nombre de usuario y la contraseña de Yandex.Disk.
- en el campo «—ifconfig 10.45.54.(1 o 2) 255.255.255.252» indicar la dirección IP interna de la interfaz
- crear secret.key con el comando:
# openvpn --genkey --secret secret.key - hacer que el script sea ejecutable:
# chmod +x vpn8.sh - ejecutar el script:
# ./vpn8.sh nZbVGBuX5dtturDdonde nZbVGBuX5dtturD — ID de la conexión generada aquí
En el nodo remoto, hacer lo mismo, excepto la generación de secret.key y el ID de la conexión, que deben ser idénticos.
Versión actualizada (para un funcionamiento correcto, el tiempo debe estar sincronizado):
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
Para que el script funcione, es necesario:
- Copiar al portapapeles y pegar en un editor, por ejemplo:
# nano vpn10.sh - indicar el nombre de usuario (línea 2) y la contraseña de Yandex.Disk (línea 3).
- indicar la dirección IP interna del túnel (línea 4).
- hacer que el script sea ejecutable:
# chmod +x vpn10.sh - ejecutar el script:
# ./vpn10.sh nZbVGBuX5dtturDdonde nZbVGBuX5dtturD — ID de la conexión generada aquí
En el nodo remoto, hacer lo mismo, indicando la dirección interna correspondiente Dirección IP del túnel y el ID de la conexión.
Para el autoarranque del script al encender, uso el comando «nohup //vpn10.sh nZbVGBuX5dtturD > /var/log/vpn10.log 2>/dev/null &» contenido en el archivo /etc/rc.local
Conclusión
El script funciona, probado en Ubuntu (18.04, 19.10, 20.04) y Debian 9. Como transmisor, se puede usar cualquier otro servicio, pero para la experiencia usé Yandex.Disk.
Durante los experimentos, se descubrió que algunos tipos de NAT de los proveedores no permiten establecer una conexión. Esto ocurre principalmente con los operadores de telefonía móvil, donde se bloquean los torrents.
Planeo mejorar en los siguientes aspectos:
- Generación automática de secret.key cada vez que se inicia, cifrado y carga en Yandex.Disk para su transmisión al nodo remoto (tenido en cuenta en la versión actualizada)
- Asignación automática de direcciones IP a las interfaces
- Cifrado de datos antes de cargar en Yandex.Disk
- Optimización del código
¡Que haya IPv6 en cada hogar!
¡Actualizado! Últimos archivos y paquete DEB aquí — yandex.disk
Fuente: habr.com
