Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Статья о том, как мне удалось организовать прямой (точка-точка) VPN-туннель между двумя компьютерами, каждый из которых находился за NAT'ом провайдеров, при помощи VPS и простых скриптов, используя стандартные утилиты Linux, без каких-либо настроек сетевого оборудования.
Существует множество программ (TeamViewer, Hamachi) для удаленного управления компьютером, передачи данных и т.п. Работают они на удивление хорошо и в любых условиях. Но как правило это платные сервисы. Как устроены эти программы неизвестно, да и особого желания их использовать они у меня не вызывают. Я как пользователь ОС Linux (в основном Ubuntu) использовал для передачи данных и управлением своими ПК связку из OpenVPN, SSH, VNC, RDP и прочие сервисы в зависимости от потребности. Для того чтобы организовать сеть между двух компьютеров я использовал VPN-сервер, что бы эти компьютеры подключались к OpenVPN серверу и весь трафик шел через сервер. Это не совсем хорошо, ведь трафик проходит двойной путь, в следствии этого терялась скорость.
Я давно задумался о решении по организации канала передачи данных непосредственно от узла к узлу, которые находятся за NATом провайдеров, без использования серверов-посредников. Перебрав некоторое количество статей о различных технологиях типа GRE-туннелей, IPsec, UDP Hole Punching, OpenVPN и прочего, я понял, что узлы должны пробивать соединение на встречу друг другу, то есть посылать пакеты на IP и порт удаленного узла. Поставил несколько опытов по организации GRE-туннеля через NAT, передачу сообщения при помощи NetCat навстречу друг другу, иногда это работало, иногда нет, всё зависело от типа используемого провайдером NAT. Не так давно попалась на глаза интересная статья, в которой было описание организации работы OpenVPN соединения между двумя компьютерами (далее узлами). Я прочитал, проверил и заработало, но при условии, что локальный порт узла и внешний порт будут совпадать, то есть мой провайдер будет использовать Cone NAT. Мне же стало интересно организовать туннель между двумя компьютерами при условии, что оба будут находятся за любым типом (Cone или Symmetric) NAT, то есть локальный порт может не совпадать с внешним портом. Задача уперлась в невозможность определения текущего порта внешнего интерфейса без внешней помощи. Если внешний IP-адрес хоть как-то можно узнать (например: curl ifconfig.me), а вот с определением текущего внешнего порта возникла трудность.
Для решения этой задачи пришлось использовать VPS (S-синий овал), на нем я поднял скрипт который выполнял роль «Соединителя», что-то типа STUN-сервера: при помощи утилиты TCPDump получал UDP-пакет (далее везде используется UDP протокол) на определенный интерфейс и порт, парсил содержимое пакета, определял IP-адрес/порт источника и отвечал утилитой NetCat возвращая текущие параметры (IP-адрес и порт) соединения, а так же параметры (IP-адрес и порт) удаленного узла к которому нужно подключиться, если эти данные были доступны, иначе ждал пока они не появятся. Все это дело сопоставлялось с идентификатором (ID) соединения, так как несколько соединений могли мешать друг другу, при использовании ID все решалось. Так же была проблема того, что узел получал свои же старые данные и пытался по ним подключиться и это решилось с помощью хеша Hostname.
На узлах A и B я использовал скрипт и сгенерированный заранее ключ для авторизации VPN-соединения, работало это так: запускался скрипт, случайно выбирался локальный порт в диапазоне от 20000 до 65000 и с этого порта отправлялся пакет на VPS, который содержал в себе ID соединения и хеш hostname, с помощью утилиты NetCat и тут же запускался TCPDump ожидая ответа. В ответ приходил пакет который содержал в себе текущие данные (IP-адрес/порт) этого узла, а при наличие данных удаленного узла, они тоже приходили и начинался обмен приветствиями между узлами. Если же данных удаленного узла не было, то опрос повторялся с периодичностью 30-45 секунд, для поддержания сессии. В момент когда все необходимые данные (IP-адрес и порт текущего узла и удаленного) были на узлах, начинался обмен пакетами, пакет содержал в себе число m=0 и сгенерированное число от 0 до 254 (это число спользовалось для генерации внутреннего IP-адреса VPN-соединения). Удаленный узел получив пакет с m=0 отправлял в ответ m=1 и так далее до 10. При получении пакета m=10 инициировалась посылка пачки пакетов с периодичность в 1 секунду с m=13 и запуск OpenVPN, удаленный узел получив m=13 тоже запускал OpenVPN используя локальный порт, IP-адрес и порт удаленного узла, а также сгенерированный внутренний IP вида 10.X.X.{1,2}/30.
Внимание: скрипты написаны и проверены на ОС Ubuntu 18.04 и Debian 9
Скрипт на VPS:
# cat connector2.sh
Запускается автоматически строкой в /etc/rc.local
Скрипт на узлах: # cat vpn5.sh
Запускается автоматически строкой в /etc/rc.local
Если все настроено правильно, то будет работать как часы: включил и через пару минут у тебя есть связь с удаленным узлом. В скриптах используются бесконечные циклы и временные задержки, то есть скрипт работает пока включен узел и при потере связи будет пытаться восстановить её. Скрипты не идеальные но, сам факт того что эта технология работает дает мне массу новых идей, например:
Приемущества:
Недостатки:
В планах дальнейшее развитие скрипта до какой-нибудь системы, минимизировать время ожидания ответов, прикрутить шифрование передаваемых данных, адаптировать его к ОС Windows и Android, научить работать через Proxy и тому подобное.
Думаю на фоне дефицита белых IPv4 адресов моё решение будет актуально.
Введение
Существует множество программ (TeamViewer, Hamachi) для удаленного управления компьютером, передачи данных и т.п. Работают они на удивление хорошо и в любых условиях. Но как правило это платные сервисы. Как устроены эти программы неизвестно, да и особого желания их использовать они у меня не вызывают. Я как пользователь ОС Linux (в основном Ubuntu) использовал для передачи данных и управлением своими ПК связку из OpenVPN, SSH, VNC, RDP и прочие сервисы в зависимости от потребности. Для того чтобы организовать сеть между двух компьютеров я использовал VPN-сервер, что бы эти компьютеры подключались к OpenVPN серверу и весь трафик шел через сервер. Это не совсем хорошо, ведь трафик проходит двойной путь, в следствии этого терялась скорость.
Теория
Я давно задумался о решении по организации канала передачи данных непосредственно от узла к узлу, которые находятся за NATом провайдеров, без использования серверов-посредников. Перебрав некоторое количество статей о различных технологиях типа GRE-туннелей, IPsec, UDP Hole Punching, OpenVPN и прочего, я понял, что узлы должны пробивать соединение на встречу друг другу, то есть посылать пакеты на IP и порт удаленного узла. Поставил несколько опытов по организации GRE-туннеля через NAT, передачу сообщения при помощи NetCat навстречу друг другу, иногда это работало, иногда нет, всё зависело от типа используемого провайдером NAT. Не так давно попалась на глаза интересная статья, в которой было описание организации работы OpenVPN соединения между двумя компьютерами (далее узлами). Я прочитал, проверил и заработало, но при условии, что локальный порт узла и внешний порт будут совпадать, то есть мой провайдер будет использовать Cone NAT. Мне же стало интересно организовать туннель между двумя компьютерами при условии, что оба будут находятся за любым типом (Cone или Symmetric) NAT, то есть локальный порт может не совпадать с внешним портом. Задача уперлась в невозможность определения текущего порта внешнего интерфейса без внешней помощи. Если внешний IP-адрес хоть как-то можно узнать (например: curl ifconfig.me), а вот с определением текущего внешнего порта возникла трудность.
Практика
Для решения этой задачи пришлось использовать VPS (S-синий овал), на нем я поднял скрипт который выполнял роль «Соединителя», что-то типа STUN-сервера: при помощи утилиты TCPDump получал UDP-пакет (далее везде используется UDP протокол) на определенный интерфейс и порт, парсил содержимое пакета, определял IP-адрес/порт источника и отвечал утилитой NetCat возвращая текущие параметры (IP-адрес и порт) соединения, а так же параметры (IP-адрес и порт) удаленного узла к которому нужно подключиться, если эти данные были доступны, иначе ждал пока они не появятся. Все это дело сопоставлялось с идентификатором (ID) соединения, так как несколько соединений могли мешать друг другу, при использовании ID все решалось. Так же была проблема того, что узел получал свои же старые данные и пытался по ним подключиться и это решилось с помощью хеша Hostname.
На узлах A и B я использовал скрипт и сгенерированный заранее ключ для авторизации VPN-соединения, работало это так: запускался скрипт, случайно выбирался локальный порт в диапазоне от 20000 до 65000 и с этого порта отправлялся пакет на VPS, который содержал в себе ID соединения и хеш hostname, с помощью утилиты NetCat и тут же запускался TCPDump ожидая ответа. В ответ приходил пакет который содержал в себе текущие данные (IP-адрес/порт) этого узла, а при наличие данных удаленного узла, они тоже приходили и начинался обмен приветствиями между узлами. Если же данных удаленного узла не было, то опрос повторялся с периодичностью 30-45 секунд, для поддержания сессии. В момент когда все необходимые данные (IP-адрес и порт текущего узла и удаленного) были на узлах, начинался обмен пакетами, пакет содержал в себе число m=0 и сгенерированное число от 0 до 254 (это число спользовалось для генерации внутреннего IP-адреса VPN-соединения). Удаленный узел получив пакет с m=0 отправлял в ответ m=1 и так далее до 10. При получении пакета m=10 инициировалась посылка пачки пакетов с периодичность в 1 секунду с m=13 и запуск OpenVPN, удаленный узел получив m=13 тоже запускал OpenVPN используя локальный порт, IP-адрес и порт удаленного узла, а также сгенерированный внутренний IP вида 10.X.X.{1,2}/30.
Внимание: скрипты написаны и проверены на ОС Ubuntu 18.04 и Debian 9
Скрипт на VPS:
# cat connector2.sh
#!/bin/bash
if [[ $1 == '' ]]; then echo -e "Укажите номер порта который нужно слушать"; exit; fi
iface=`ip route get 8.8.8.8 | head -n 1 | sed 's|.*dev ||' | awk '{print $1}'`
a=0
until (( $a == 500)); do
packet=`tcpdump -i $iface udp port $1 -vvn -c1 -A`
if [[ "$packet" == *"Ident"* ]]; then
pack=`echo "$packet" | grep -e "udp sum ok" -e "Ident"`
myip=`echo $pack | sed 's/\./ /g' | awk '{print $1"."$2"."$3"."$4}'`
myport=`echo $pack | sed 's/\./ /g' | awk '{print $5}'`
id=`echo $pack | sed 's|.*Ident:||' | awk '{print $1}'`
myname=`echo $pack | sed 's|.*Ident:||' | awk '{print $2}'`
echo "$myip:$myport $myname" > /tmp/vpn2-$id-$myname
echo "MyData $myip:$myport $(cat /tmp/vpn2-$id-* | grep -v $myname | awk {'print $1'})" | nc $myip $myport -u -p $1 -w 1
cat /tmp/vpn2-$id-*
topoint=`cat /tmp/vpn2-$id-* | grep -v "$myname" | sed 's/:/ /g'`
if [[ $topoint != '' ]]; then
ip=`echo $topoint | awk '{print $1}'`
port=`echo $topoint | awk '{print $2}'`
echo "MyData $ip:$port $(cat /tmp/vpn2-$id-* | grep $myname | awk {'print $1'})" | nc $ip $port -u -p $1 -w 1
fi
fi
done
Запускается автоматически строкой в /etc/rc.local
nohup /путь-до-скрипта/connector2.sh 13013 > /var/log/connector2.log &
, где 13013 — это порт который нужно слушать, /var/log/connector2.log — лог.Скрипт на узлах: # cat vpn5.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 ps grep awk kill md5sum shuf nc curl sleep openvpn tcpdump"
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 /<путь к файлу>/vpn5.sh > /var/log/vpn5.log 2>/dev/hull & ${END}"; exit; fi
ABSOLUTE_FILENAME=`readlink -f "$0"` # полный путь до скрипта
DIR=`dirname "$ABSOLUTE_FILENAME"` # каталог в котором лежит скрипт
#################################################################
if [ ! -f "$DIR/secret.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
localport=`shuf -i 20000-65000 -n 1` # Выбор локального порта
until [[ $count > 100 ]]; do
#########################################################################################
name=`uname -n | md5sum | awk '{print $1}'` # Имя узла
ipsrv="45.141.103.45" # IP-адрес сервера получения данных о второй стороне
portsrv="13013" # Порт сервера
iftosrv=`ip route get $ipsrv | head -n 1 | sed 's|.*dev ||' | awk '{print $1}'` # Определение интерфейса
netcattosrv="nc -u $ipsrv $portsrv -p $localport -w 1" # Команда посылки пакета NC
tcpdumptosrv="tcpdump -i $iftosrv udp and port $portsrv and src $ipsrv -n -c1 -A" # Команда приема пакета
id=`echo $1| md5sum | awk '{print $1}'` # Преобразеум имя узла в md5 что бы не светить реальное имя
##################### Получение данных о себе и удаленного узла ###################################
echo -e "$(date) ${GREEN}Фаза 1 - Получение данных с сервера ${END}"
echo -e "$(date) ${GREEN}Использую интерфейс $iftosrv и локальный порт $localport ${END}"
until [[ -n "$ip" && -n "$port" ]]; do
if [[ -z "$data" ]]; then
sleep 1 && echo "Ident: $id $name" | $netcattosrv > /dev/null &
sleep 4 && pid=`ps xa | grep "$tcpdumptosrv" | grep -v grep | awk '{print $1}'` && if [[ -n $pid ]]; then kill $pid; echo "$(date) Нет ответа от сервера, пробую еще раз..."; fi &
data=`$tcpdumptosrv`
sleep 2
fi
if [[ -n "$data" ]]; then
#echo "Ответ: $data"
data=`echo "$data" | grep "MyData" | sed 's|.*MyData ||' | sed 's/:/ /g'`
myip=`echo "$data" | awk '{print $1}'`
myport=`echo "$data" | awk '{print $2}'`
ip=`echo "$data" | awk '{print $3}'`
port=`echo "$data" | awk '{print $4}'`
data=''
if [[ -z $tst ]]; then echo -e "$(date) ${GREEN}Мой внешний IP: $myip и порт: $myport ${END}"; fi
if [[ -n $localport && -n $myport && -z $tst ]]; then
if [[ $localport == $myport ]]; then
echo -e "$(date) ${GREEN}Мой локальный порт $localport и внешний порт $myport одиноковые, используется CONE NAT ${END}"; tst=1
else
echo -e "$(date) ${RED}Мой локальный порт $localport и внешний порт $myport разные, используется SYMMETRIC NAT ${END}"; tst=1
fi
fi
fi
if [[ -n "$ip" && -n "$port" ]]; then
echo -e "$(date) ${GREEN}Получил данные об удаленном узле: IP: $ip Порт: $port ${END}";
else
sleep=`shuf -i 30-45 -n 1`;
echo -e "$(date) ${RED}Нет данных об удаленном узле, ожидаю данных $sleep секунд и оповещаю еще раз... ${END}";
sleep $sleep && pid=`ps xa | grep "$tcpdumptosrv" | grep -v grep | awk '{print $1}'` && if [[ -n $pid ]]; then kill $pid; fi &
data=`$tcpdumptosrv`
fi
done
###################### Организация соединения с удаленным узлом
iftopeer=`ip route get "$ip" | head -n 1 | sed 's|.*dev ||' | awk '{print $1}'`
echo -e "$(date) ${GREEN}Фаза 2 - Установка соединения со второй стороной ${END}"
echo -e "$(date) ${GREEN}Попытка установки соединения: $myip:$myport -> $ip:$port ${END}"
m=0
t=0
connect=0
until (( $connect >= 10 )); do
if [ ! -f "$DIR/.mes" ]; then
mes=`shuf -i 0-254 -n 1`
echo $mes > $DIR/.mes
else
mes=`cat $DIR/.mes`
fi
netcattopeer="nc -u $ip $port -p $localport -w 1"
tcpdumptopeer="tcpdump -i $iftopeer udp and port $localport and src "$ip" -n -c1 -A"
until (( $m >= 10 )); do
for w in {1..3}; do sleep 1 && echo "445Hi: $m $mes" | $netcattopeer > /dev/null; done &
sleep 4 && pid=`ps xa | grep "$tcpdumptopeer" | grep -v grep | awk '{print $1}'` && sleep 10 && if [[ -n $pid ]]; then kill $pid && echo -e "$(date) ${RED} Нет ответа от пира, пробую еще раз... ${END}"; fi &
peer=`$tcpdumptopeer`
if [[ -n $peer ]]; then
newport=`echo "$peer" | grep " IP " | sed "s|.* $ip.||" | awk '{print $1}'`
if (( $newport != $port )); then
echo -e "$(date) ${WARN}Порт изменился $port -> $newport ${END}";
port=$newport;
netcattopeer="nc -u $ip $port -p $localport -w 1"
fi
mm=`echo "$peer" | grep "445Hi: " | sed 's|.*445Hi: ||' | awk '{print $1}'`
echo -e "$(date) ${GREEN} Получил: $mm - передаю: $mm+1 ${END}"
if (( $m <= $mm )); then
m=$(( $mm + 1 ));
fi
text=`echo "$peer" | grep "445Hi: " | sed 's|.*445Hi: ||' | awk '{print $2}'`
else
(( t++ ))
echo -e "$(date) ${RED} Данных нет $t раз ${END}"
if (( $t > 5 ));then echo -e "$(date) ${RED} Превышен интервал ожидания, переподключаюсь ${END}"; m=10; connect=1012; ip=''; port=''; fi
fi
done
if (( $connect < 1012 )); then
echo -e "$(date) ${GREEN}Значение: $m ${END}"
if [[ $port > $myport ]]; then ipaddress="10.$text.$mes.1"; else ipaddress="10.$mes.$text.2"; fi
m=12
for w in {1..9}; do echo "445Hi: $m $mes" | $netcattopeer > /dev/null; done
echo -e "$(date) ${GREEN} Запускаюсь и использую $ipaddress...${END}"
$(which lsmod) | grep tun || $(which modprobe) tun
openvpn --remote $ip --rport $port --lport $localport \
--proto udp --dev tap --float --auth-nocache --verb 3 --mute 20 \
--ifconfig "$ipaddress" 255.255.255.252 \
--secret "$DIR/secret.key" \
--auth SHA256 --cipher AES-256-CBC \
--comp-lzo --ncp-disable --ping 10 --ping-exit 50
echo -e "$(date) ${RED} Соединение прервано... ${END}"
m=0
t=0
(( connect++ ));
fi
done
done
Запускается автоматически строкой в /etc/rc.local
sleep 15 && nohup /путь-до-скрипта/vpn5.sh <ID-соединения> >/var/log/vpn5.log 2>/dev/hull &
, где ID-соединения — это любая уникальная фраза для вашего соединения, /var/log/vpn5.log — лог соединения, ipsrv=«45.141.103.45» — IP-адрес узла где запущен connector2.sh (первый скрипт), sleep 15 — нужно чтобы за это время поднялись сетевые интерфейсы.Если все настроено правильно, то будет работать как часы: включил и через пару минут у тебя есть связь с удаленным узлом. В скриптах используются бесконечные циклы и временные задержки, то есть скрипт работает пока включен узел и при потере связи будет пытаться восстановить её. Скрипты не идеальные но, сам факт того что эта технология работает дает мне массу новых идей, например:
- использовать узел для хранения особо важных данных, и иметь к ним доступ, допустим, в случае утери ноутбука все данные будут на узле
- использовать на ноутбуке удаленный рабочий стол компьютера
- организовать связи с компьютером друга и поиграть в сетевую игру
- иметь доступ к домашней камере видеонаблюдения с рабочего компьютера или просмотреть веб-камеру ноутбука
Приемущества:
- простота в настройке и использовании
- трафик идет напрямую
- трафик шифруется и защищен от перехвата
- нет необходимости пробрасывать порты
- скрипт запускается автоматически
Недостатки:
- главный недостаток это использования VPS как источника данных для соединения, но я думаю над этим
- некоторые провайдеры блокируют трафик между другими провайдерами (замечено у сотовых операторов)
- при использовании с обоих сторон одного провадера может не сработать, зависит от типа NAT
- при использовании провадером «жесткого» NAT может не сработать
В планах дальнейшее развитие скрипта до какой-нибудь системы, минимизировать время ожидания ответов, прикрутить шифрование передаваемых данных, адаптировать его к ОС Windows и Android, научить работать через Proxy и тому подобное.
Думаю на фоне дефицита белых IPv4 адресов моё решение будет актуально.