ProHoster > Blog > administração > Túnel VPN direto entre computadores através de NATs de provedor (sem VPS, usando servidor STUN e Yandex.disk)
Túnel VPN direto entre computadores através de NATs de provedor (sem VPS, usando servidor STUN e Yandex.disk)
Extensão artigos sobre como consegui organizar um túnel VPN direto entre dois computadores localizados atrás de provedores NAT. O artigo anterior descreveu o processo de organização de uma conexão com a ajuda de um terceiro - um intermediário (um VPS alugado atuando como algo como um servidor STUN e um nó transmissor de dados para a conexão). Neste artigo vou contar como consegui sem VPS, mas os intermediários permaneceram e eram o servidor STUN e Yandex.Disk...
Introdução
Depois de ler os comentários do post anterior, percebi que a principal desvantagem da implementação era a utilização de um intermediário - um terceiro (VPS) que indicava os parâmetros atuais do nó, onde e como conectar. Considerando as recomendações para usar este STUN (dos quais há muitos) para determinar os parâmetros de conexão atuais. Em primeiro lugar, decidi usar o TCPDump para examinar o conteúdo dos pacotes quando o servidor STUN estava trabalhando com clientes e recebia conteúdo completamente ilegível. Pesquisando no Google o protocolo que encontrei artigo descrevendo o protocolo. Percebi que não conseguiria implementar sozinho uma solicitação ao servidor STUN e coloquei a ideia em uma “caixa distante”.
Теория
Recentemente tive que instalar o servidor STUN no Debian a partir do pacote
# apt install stun-server
e nas dependências vi o pacote stun-client, mas de alguma forma não prestei atenção nele. Mas depois me lembrei do pacote stun-client e decidi descobrir como ele funciona, depois de pesquisar no Google e pesquisar no Yandex consegui:
Cliente STUN versão 0.97
Porta aberta 21234 com fd 3
Porta aberta 21235 com fd 4
Codificando mensagem de choque:
Codificação ChangeRequest: 0
Prestes a enviar mensagem de len 28 para 216.93.246.18:3478
Codificando mensagem de choque:
Codificação ChangeRequest: 4
Prestes a enviar mensagem de len 28 para 216.93.246.18:3478
Codificando mensagem de choque:
Codificação ChangeRequest: 2
Prestes a enviar mensagem de len 28 para 216.93.246.18:3478
Mensagem de choque recebida: 92 bytes
EndereçoMapeado = <Meu IP>:2885
Endereço de origem = 216.93.246.18:3478
Endereço alterado = 216.93.246.17:3479
Atributo desconhecido: 32800
Nome do servidor = Vovida.org 0.98-CPC
Mensagem recebida do tipo 257 id=1
Codificando mensagem de choque:
Codificação ChangeRequest: 0
Prestes a enviar mensagem de len 28 para 216.93.246.17:3478
Codificando mensagem de choque:
Codificação ChangeRequest: 4
Prestes a enviar mensagem de len 28 para 216.93.246.18:3478
Codificando mensagem de choque:
Codificação ChangeRequest: 2
Prestes a enviar mensagem de len 28 para 216.93.246.18:3478
Codificando mensagem de choque:
Codificação ChangeRequest: 0
Prestes a enviar mensagem de len 28 para <Meu IP>:2885
Mensagem de choque recebida: 28 bytes
Solicitação de alteração = 0
Mensagem recebida do tipo 1 id=11
Codificando mensagem de choque:
Codificação ChangeRequest: 0
Prestes a enviar mensagem de len 28 para 216.93.246.17:3478
Codificando mensagem de choque:
Codificação ChangeRequest: 4
Prestes a enviar mensagem de len 28 para 216.93.246.18:3478
Codificando mensagem de choque:
Codificação ChangeRequest: 2
Prestes a enviar mensagem de len 28 para 216.93.246.18:3478
Mensagem de choque recebida: 92 bytes
EndereçoMapeado = <Meu IP>:2885
Endereço de origem = 216.93.246.17:3479
Endereço alterado = 216.93.246.18:3478
Atributo desconhecido: 32800
Nome do servidor = Vovida.org 0.98-CPC
Mensagem recebida do tipo 257 id=10
Codificando mensagem de choque:
Codificação ChangeRequest: 4
Prestes a enviar mensagem de len 28 para 216.93.246.18:3478
Codificando mensagem de choque:
Codificação ChangeRequest: 2
Prestes a enviar mensagem de len 28 para 216.93.246.18:3478
Codificando mensagem de choque:
Codificação ChangeRequest: 4
Prestes a enviar mensagem de len 28 para 216.93.246.18:3478
Codificando mensagem de choque:
Codificação ChangeRequest: 2
Prestes a enviar mensagem de len 28 para 216.93.246.18:3478
Codificando mensagem de choque:
Codificação ChangeRequest: 4
Prestes a enviar mensagem de len 28 para 216.93.246.18:3478
Codificando mensagem de choque:
Codificação ChangeRequest: 2
Prestes a enviar mensagem de len 28 para 216.93.246.18:3478
Codificando mensagem de choque:
Codificação ChangeRequest: 4
Prestes a enviar mensagem de len 28 para 216.93.246.18:3478
Codificando mensagem de choque:
Codificação ChangeRequest: 2
Prestes a enviar mensagem de len 28 para 216.93.246.18:3478
Codificando mensagem de choque:
Codificação ChangeRequest: 4
Prestes a enviar mensagem de len 28 para 216.93.246.18:3478
Codificando mensagem de choque:
Codificação ChangeRequest: 2
Prestes a enviar mensagem de len 28 para 216.93.246.18:3478
teste I = 1
teste II = 0
teste III = 0
teste I(2) = 1
é nat = 1
IP mapeado igual = 1
grampo de cabelo = 1
porta preservadora = 0
Primário: mapeamento independente, filtro dependente de porta, porta aleatória, gancho
O valor de retorno é 0x000006
String com valor
EndereçoMapeado = <Meu IP>:2885
exatamente o que você precisa! Ele exibiu o status atual da conexão na porta UDP local 21234. Mas isso é apenas metade da batalha: surgiu a questão de como transferir esses dados para o host remoto e organizar uma conexão VPN. Usando o protocolo de correio, ou talvez Telegram?! Existem muitas opções e decidi usar Yandex.disk, pois me deparei artigo sobre como trabalhar com Curl via WebDav com Yandex.disk. Depois de pensar na implementação, criei o seguinte esquema:
Sinalizar que os nós estão prontos para estabelecer uma conexão pela presença de um arquivo específico com carimbo de data e hora no Yandex.disk;
Se os nós estiverem prontos, receba os parâmetros atuais do servidor STUN;
Carregue as configurações atuais para Yandex.disk;
Verifique a presença e leia os parâmetros de um nó remoto de um arquivo em Yandex.disk;
Estabelecendo uma conexão com um host remoto usando OpenVPN.
Prática
Depois de pensar um pouco, levando em consideração a experiência do último artigo, rapidamente escrevi um roteiro. Nós vamos precisar:
# apt install openvpn stun-client curl
O roteiro em si:
versão original
# 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
Para que o script funcione você precisa de:
Copie para a área de transferência e cole no editor, por exemplo:
# nano vpn8.sh
especifique o nome de usuário e a senha para Yandex.disk.
no campo "—ifconfig 10.45.54.(1 ou 2) 255.255.255.252" especifique o endereço IP interno da interface
criar chave secreta por comando:
# openvpn --genkey --secret secret.key
torne o script executável:
# chmod +x vpn8.sh
execute o script:
# ./vpn8.sh nZbVGBuX5dtturD
onde nZbVGBuX5dtturD é o ID de conexão gerado aqui
No nó remoto, faça tudo igual, exceto gerar secret.key e ID de conexão, eles devem ser idênticos.
Versão atualizada (o horário deve estar sincronizado para o correto funcionamento):
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 o script funcione você precisa de:
Copie para a área de transferência e cole no editor, por exemplo:
# nano vpn10.sh
indique o login (2ª linha) e senha para Yandex.disk (3ª linha).
especifique o endereço IP interno do túnel (4ª linha).
torne o script executável:
# chmod +x vpn10.sh
execute o script:
# ./vpn10.sh nZbVGBuX5dtturD
onde nZbVGBuX5dtturD é o ID de conexão gerado aqui
No nó remoto, faça o mesmo, especifique o endereço IP interno correspondente do túnel e o ID de conexão.
Para executar automaticamente o script quando ativado, eu uso o comando “nohup /<caminho para o script>/vpn10.sh nZbVGBuX5dtturD > /var/log/vpn10.log 2>/dev/null &” contido no arquivo /etc/ rc.local
Conclusão
O script funciona, testado no Ubuntu (18.04, 19.10, 20.04) e Debian 9. Você pode usar qualquer outro serviço como transmissor, mas para experiência usei Yandex.disk.
Durante os experimentos, descobriu-se que alguns tipos de provedores NAT não permitem o estabelecimento de conexão. Principalmente de operadoras móveis onde os torrents são bloqueados.
Pretendo melhorar em termos de:
Geração automática de secret.key toda vez que você inicia, criptografa e copia para Yandex.disk para transferência para um nó remoto (levando em consideração na versão atualizada)
Atribuição automática de endereços IP de interfaces
Criptografando dados antes de enviar para Yandex.disk
Otimização de código
Que haja IPv6 em todas as casas!
Atualizada! Arquivos mais recentes e pacote DEB aqui - yandex.disk