Русский
Русский
English
Статистика
Реклама

Ipip

Простой UDP hole punching на примере IPIP-туннеля

12.07.2020 22:04:29 | Автор: admin
Доброе время суток!
В этой статье хочу рассказать как я реализовал (еще один) скрипт на Bash для соединения двух компьютеров, находящимися за NAT, с использованием технологии UDP hole punching на примере ОС Ubuntu/Debian.

Организация соединения состоит из нескольких шагов:
1. Запуск узла и ожидание готовности удаленного узла;
2. Определение внешнего IP-адреса и UDP-порта;
3. Передача внешнего IP-адреса и UDP-порта удаленному узлу;
4. Получение внешнего IP-адреса и UDP-порта от удаленного узла;
5. Организация IPIP-туннуля;
6. Мониторинг соединения;
7. При обрыве соединения удалять IPIP туннель.

Я долго думал и еще думаю, что можно использовать для обмена данными между узлами, самое простое и быстрое для меня на данный момент, это работа через Яндекс.диск.
  • Во первых, это простота в использовании нужно 3 действия: создать, прочитать, удалить. С помощью curl это:
    Создать:
    curl -s -X MKCOL --user "$usename:$password" https://webdav.yandex.ru/$folder
    

    Прочитать:
    curl -s --user "$usename:$password" -X PROPFIND -H "Depth: 1" https://webdav.yandex.ru/$folder
    

    Удалить:
    curl -s -X DELETE --user "$usename:$password" https://webdav.yandex.ru/$folder
    
  • Во вторых, это простота в установке:
    apt install curl
    


Для определения внешнего IP-адреса и UDP-порта используется stun-client командой:
stun stun.sipnet.ru -v -p $1 2>&1 | grep "MappedAddress"

Установка командой:
apt install stun-client


Для организации туннеля используются штатные средства ОС из пакета iproute2. Существует множество туннелей которые можно поднять штатными средствами (L2TPv3, GRE и т.п.), но я выбрал IPIP потому что он создаёт минимальную дополнительную нагрузку на систему. Я пробовал L2TPv3 поверх UDP и разочаровался, скорость упала в 10 раз, но это могут быть различные ограничения связанные с провайдерами или еще чего-то. Так как IPIP-туннель работает на уровне IP, то используется FOU-туннель для того, что бы работать на уровне UDP-портов. Для организации IPIP-туннеля нужно:

загрузить модуль FOU:
modprobe fou

слушать локальный порт:
ip fou add port $localport ipproto 4

создать туннель:
ip link add name fou$name type ipip remote $remoteip local $localip encap fou  encap-sport $localport encap-dport $remoteport

поднять интерфейс туннеля:
ip link set up dev fou$name

назначить внутренний локальный и внутренний удаленный IP-адрес тунеля:
ip addr add $intIP peer $peerip dev fou$name


Удалить туннель:
ip link del dev fou$name

ip fou del port $localport


Мониторинг состояния туннеля происходит с помощью периодического пинга внутреннего IP-адреса туннеля удаленного узла, командой:
ping -c 1 $peerip -s 0

Периодический пинг нужен в первую очередь для поддержания канала, иначе при простое туннеля на роутерах могут очиститься таблицы NAT и тогда соединение разорвется.
Если пинг пропадает, то IPIP-туннель удаляется и ждет готовности от удаленного узла.

Собственно сам скрипт:
#!/bin/bashusername="username@yandex.ru"password="password"folder="vpnid"intip="10.0.0.1"localport=`shuf -i 10000-65000 -n 1`cid=`shuf -i 10000-99999 -n 1`tid=`shuf -i 10-99 -n 1`function yaread {        curl -s --user "$1:$2" -X PROPFIND -H "Depth: 1" https://webdav.yandex.ru/$3 | sed 's/></>\n</g' | grep "displayname" | sed 's/<d:displayname>//g' | sed 's/<\/d:displayname>//g' | grep -v $3 | grep -v $4 | sort -r}function yacreate {        curl -s -X MKCOL --user "$1:$2" https://webdav.yandex.ru/$3}function yadelete {        curl -s -X DELETE --user "$1:$2" https://webdav.yandex.ru/$3}function myipport {        stun stun.sipnet.ru -v -p $1 2>&1 | grep "MappedAddress" | sort | uniq | awk '{print $3}' | head -n1}function tunnel-up {modprobe fouip fou add port $4 ipproto 4ip link add name fou$7 type ipip remote $1 local $3 encap fou encap-sport $4 encap-dport $2ip link set up dev fou$7ip addr add $6 peer $5 dev fou$7}function tunnel-check {sleep 10        pings=0        until [[ $pings == 4 ]]; do                if ping -c 1 $1 -s 0 &>/dev/null;                        then    echo -n .; n=0                        else    echo -n !; ((pings++))                fisleep 15        done}function tunnel-down {ip link del dev fou$1ip fou del port $2}trap 'echo -e "\nDisconnecting..." && yadelete $username $password $folder; tunnel-down $tunnelid $localport; echo "IPIP tunnel disconnected!"; exit 1' 1 2 3 8 9 14 15until [[ -n $end ]]; do    yacreate $username $password $folder    until [[ -n $ip ]]; do        mydate=`date +%s`        timeout="60"        list=`yaread $username $password $folder $cid | head -n1`        yacreate $username $password $folder/$mydate:$cid        for l in $list; do                if [ `echo $l | sed 's/:/ /g' | awk {'print $1'}` -ge $(($mydate-65)) ]; then#echo $list                        myipport=`myipport $localport`                        yacreate $username $password $folder/$mydate:$cid:$myipport:$intip:$tid                        timeout=$(( $timeout + `echo $l | sed 's/:/ /g' | awk {'print $1'}` - $mydate + 3 ))                        ip=`echo $l | sed 's/:/ /g' | awk '{print $3}'`                        port=`echo $l | sed 's/:/ /g' | awk '{print $4}'`                        peerip=`echo $l | sed 's/:/ /g' | awk '{print $5}'`peerid=`echo $l | sed 's/:/ /g' | awk '{print $6}'`if [[ -n $peerid ]]; then tunnelid=$(($peerid*$tid)); fi                fi        done        if ( [[ -z "$ip" ]] && [ "$timeout" -gt 0 ] ) ; then                echo -n "!"                sleep $timeout        fi    done    localip=`ip route get $ip | head -n1 | sed 's|.*src ||' | cut -d' ' -f1`    tunnel-up $ip $port $localip $localport $peerip $intip $tunnelid    tunnel-check $peerip    tunnel-down $tunnelid $localport    yadelete $username $password $folder    unset ip port myipportdoneexit 0

Переменные username, password и folder должны быть одинаковые на обоих сторонах, а вот intip разные, например: 10.0.0.1 и 10.0.0.2. Время на узлах должно быть синхронизированно. Запускать скрипт можно так:
nohup script.sh &

Обращаю внимание на то, что IPIP-теннель небезопасен с точки зрения того, что трафик не шифруется, но это легко решается при помощи IPsec по этой статье, она мне показалось простой и понятной.
Использую данный скрипт для подключения к рабочим ПК уже несколько недель и проблем не замечал. Удобно в плане того, что настроил и забыл.
Возможно у Вас будут замечания и предложения, буду рад выслушать)
Спасибо за внимание!
Подробнее..

Ipipou больше чем просто нешифрованный туннель

07.10.2020 18:18:13 | Автор: admin
Что мы говорим богу IPv6?

IPv6? Not today

Верно, и богу шифрования сегодня скажем то же.

Здесь будет о нешифрованном IPv4 туннеле, но не о тёплом ламповом, а о модерновом светодиодном. А ещё тут мелькают сырые сокеты, и идёт работа с пакетами в пространстве пользователя.

Есть N протоколов туннелирования на любой вкус и цвет:

  • стильный, модный, молодёжный WireGuard
  • мультифункциональные, как швейцарские ножи, OpenVPN и SSH
  • старый и не злой GRE
  • максимально простой, шустрый, совсем не шифрованный IPIP
  • активно развивающийся GENEVE
  • множество других.

Но яжпрограммист, поэтому увеличу N лишь на толику, а разработку настоящих протоколов оставлю Ъ-девелоперам.

В одном ещё не родившемся проекте, которым сейчас занимаюсь, надо достучаться до хостов за NAT-ом извне. Используя для этого протоколы со взрослой криптографией, меня никак не покидало ощущение, что это как из пушки по воробьям. Т.к. туннель используется по большей части только для проковыривания дырки в NAT-e, внутренний трафик обычно тоже зашифрован, все же топят за HTTPS.

Исследуя различные протоколы туннелирования внимание моего внутреннего перфекциониста раз за разом привлекал IPIP из-за его минимальных накладных расходов. Но у него есть полтора существенных недостатка для моих задач:

  • он требует публичные IP на обеих сторонах,
  • и никакой тебе аутентификации.

Поэтому перфекционист загонялся обратно в тёмный угол черепной коробки, или где он там сидит.

И вот как-то раз читая статьи по нативно поддерживаемым туннелям в Linux наткнулся на FOU (Foo-over-UDP), т.е. что-попало, завёрнутое в UDP. Пока из чего-попало поддерживаются только IPIP и GUE (Generic UDP Encapsulation).

Вот она серебряная пуля! Мне и простого IPIP за глаза. думал я.

На деле пуля оказалась не до конца серебряной. Инкапсуляция в UDP решает первую проблему к клиентам за NAT-ом можно подключаться снаружи используя заранее установленное соединение, но тут половинка следующего недостатка IPIP расцветает в новом свете за видимыми публичными IP и портом клиента может скрываться кто угодно из приватной сети (в чистом IPIP этой проблемы нет).

Для решения этой полуторной проблемы и родилась утилита ipipou. В ней реализован самопальный механизм аутентификации удалённого хоста, при этом не нарушая работы ядрёного FOU, который будет шустро и эффективно обрабатывать пакеты в пространстве ядра.

Не нужон твой скрипт!


Ок, если тебе известны публичные порт и IP клиента (например за ним все свои, куда попало не ходют, NAT пытается мапить порты 1-в-1), можешь создать IPIP-over-FOU туннель следующими командами, без всяких скриптов.

на сервере:
# Подгрузить модуль ядра FOUmodprobe fou# Создать IPIP туннель с инкапсуляцией в FOU.# Модуль ipip подгрузится автоматически.ip link add name ipipou0 type ipip \    remote 198.51.100.2 local 203.0.113.1 \    encap fou encap-sport 10000 encap-dport 20001 \    mode ipip dev eth0# Добавить порт на котором будет слушать FOU для этого туннеляip fou add port 10000 ipproto 4 local 203.0.113.1 dev eth0# Назначить IP адрес туннелюip address add 172.28.0.0 peer 172.28.0.1 dev ipipou0# Поднять туннельip link set ipipou0 up

на клиенте:
modprobe fouip link add name ipipou1 type ipip \    remote 203.0.113.1 local 192.168.0.2 \    encap fou encap-sport 10001 encap-dport 10000 encap-csum \    mode ipip dev eth0# Опции local, peer, peer_port, dev могут не поддерживаться старыми ядрами, можно их опустить.# peer и peer_port используются для создания соединения сразу при создании FOU-listener-а.ip fou add port 10001 ipproto 4 local 192.168.0.2 peer 203.0.113.1 peer_port 10000 dev eth0ip address add 172.28.0.1 peer 172.28.0.0 dev ipipou1ip link set ipipou1 up

где
  • ipipou* имя локального туннельного сетевого интерфейса
  • 203.0.113.1 публичный IP сервера
  • 198.51.100.2 публичный IP клиента
  • 192.168.0.2 IP клиента, назначенный интерфейсу eth0
  • 10001 локальный порт клиента для FOU
  • 20001 публичный порт клиента для FOU
  • 10000 публичный порт сервера для FOU
  • encap-csum опция для добавления контрольной суммы UDP в инкапсулированные UDP пакеты; можно заменить на noencap-csum, чтоб не считать, целостность и так контролируется внешним слоем инкапсуляции (пока пакет находится внутри туннеля)
  • eth0 локальный интерфейс к которому будет привязан ipip туннель
  • 172.28.0.1 IP туннельного интерфейса клиента (приватный)
  • 172.28.0.0 IP туннельного интерфейса сервера (приватный)

Пока живо UDP-соединение, туннель будет в работоспособном состоянии, а как порвётся то, как повезёт если IP: порт клиента останутся прежними будет жить, изменятся порвётся.

Вертать всё взад проще всего выгрузив модули ядра: modprobe -r fou ipip

Даже если аутентификация не требуется публичные IP и порт клиента не всегда известны и часто непредсказуемы или изменчивы (в зависимости от типа NAT). Если опустить encap-dport на стороне сервера, туннель не заработает, не настолько он умный, чтоб брать удалённый порт соединения. В этом случае ipipou тоже может помочь, ну или WireGuard и иже с ним тебе в помощь.

Как это работает?


Клиент (что обычно за NAT-ом) поднимает туннель (как в примере выше), и шлёт пакет с аутентификацией на сервер, чтобы тот настроил туннель со своей стороны. В зависимости от настроек это может быть пустой пакет (просто чтобы сервер увидел публичные IP: порт соединения), или с данными по которым сервер сможет идентифицировать клиента. Данные могут быть простой парольной фразой открытым текстом (в голову приходит аналогия с HTTP Basic Auth) или подписанные приватным ключом специально оформленные данные (по аналогии с HTTP Digest Auth только посильнее, см. функцию client_auth в коде).

На сервере (сторона с публичным IP) при запуске ipipou создаёт обработчик очереди nfqueue и настраивает netfilter так, чтоб нужные пакеты направлялись куда следует: пакеты инициализирующие соединение в очередь nfqueue, а [почти] все остальные прямиком в listener FOU.

Кто не в теме, nfqueue (или NetfilterQueue) это такая специальная штука для дилетантов, не умеющих разрабатывать модули ядра, которая средствами netfilter (nftables/iptables) позволяет перенаправлять сетевые пакеты в пространство пользователя и обрабатывать их там примитивными подручными средствами: модифицировать (опционально) и отдавать обратно ядру, или отбрасывать.

Для некоторых языков программирования есть биндинги для работы с nfqueue, для bash не нашлось (хех, не удивительно), пришлось использовать python: ipipou использует NetfilterQueue.

Если производительность не критична, с помощью этой штуки можно относительно быстро и просто стряпать собственную логику работы с пакетами на достаточно низком уровне, например ваять экспериментальные протоколы передачи данных, или троллить локальные и удалённые сервисы нестандартным поведением.

Рука об руку с nfqueue работают сырые сокеты (raw sockets), например когда туннель уже настроен, и FOU слушает на нужном порту, обычным способом отправить пакет с этого же порта не получится занято, зато можно взять и запулить произвольно сгенерированный пакет прямо в сетевой интерфейс используя сырой сокет, хоть над генерацией такого пакета и придётся повозиться чуть больше. Так и создаются в ipipou пакеты с аутентификацией.

Так как ipipou обрабатывает только первые пакеты из соединения (ну и те, которые успели просочиться в очередь до установки соединения) производительность почти не страдает.

Как только ipipou-сервер получает пакет прошедший аутентификацию, туннель создаётся и все последующие пакеты в соединении уже обрабатываются ядром минуя nfqueue. Если соединение протухло, то первый пакет следующего будет направлен в очередь nfqueue, в зависимости от настроек, если это не пакет с аутентификацией, но с последнего запомненного IP и порта клиента, он может быть либо пропущен дальше или отброшен. Если аутентифицированный пакет приходит с новых IP и порта, туннель перенастраивается на их использование.

У обычного IPIP-over-FOU есть ещё одна проблема при работе с NAT нельзя создать два IPIP туннеля инкапсулированных в UDP с одинаковыми IP, ибо модули FOU и IPIP достаточно изолированы друг от друга. Т.е. пара клиентов за одним публичным IP не сможет одновременно подключиться к одному серверу таким способом. В будущем, возможно, её решат на уровне ядра, но это не точно. А пока проблемы NAT-а можно решить NAT-ом если случается так, что пара IP адресов уже занята другим туннелем, ipipou сделает NAT с публичного на альтернативный приватный IP, вуаля! можно создавать туннели пока порты не закончатся.

Т.к. не все пакеты в соединении подписаны, то такая простецкая защита уязвима к MITM, так что если на пути между клиентом и сервером затаился злодей, который может слушать трафик и управлять им, он может перенаправлять пакеты с аутентификацией через другой адрес и создать туннель с недоверенного хоста.

Если у кого есть идеи, как это исправить оставляя основную часть трафика в ядре, не стесняйтесь высказывайтесь.

К слову сказать инкапсуляция в UDP очень хорошо себя зарекомендовала. По сравнению с инкапсуляцией поверх IP она гораздо стабильнее и чаще быстрее несмотря на дополнительные накладные расходы на заголовок UDP. Это связано с тем, что в Интернете бльшая часть хостов сносно работает только с тремя наиболее популярными протоколами: TCP, UDP, ICMP. Ощутимая часть может вообще отбрасывать всё остальное, или обрабатывать медленнее, ибо оптимизирована только под эти три.

Например, поэтому QUICK, на базе которого создан HTTP/3, создавался именно поверх UDP, а не поверх IP.

Ну да хватит слов, пора посмотреть как это работает в реальном мире.

Батл


Для эмуляции реального мира используется iperf3. По степени приближённости к реальности это примерно как эмуляция реального мира в Майнкрафте, но пока сойдёт.

В состязании участвуют:
  • эталонный основной канал
  • герой этой статьи ipipou
  • OpenVPN с аутентификацией, но без шифрования
  • OpenVPN в режиме всё включено
  • WireGuard без PresharedKey, с MTU=1440 (ибо IPv4-only)

Технические данные для гиков
Метрики снимаются такими командами

на клиенте:

UDP
CPULOG=NAME.udp.cpu.log; sar 10 6 >"$CPULOG" & iperf3 -c SERVER_IP -4 -t 60 -f m -i 10 -B LOCAL_IP -P 2 -u -b 12M; tail -1 "$CPULOG"# Где "-b 12M" это пропускная способность основного канала, делённая на число потоков "-P", чтобы лишние пакеты не плодить и не портить производительность.

TCP
CPULOG=NAME.tcp.cpu.log; sar 10 6 >"$CPULOG" & iperf3 -c SERVER_IP -4 -t 60 -f m -i 10 -B LOCAL_IP -P 2; tail -1 "$CPULOG"

ICMP latency
ping -c 10 SERVER_IP | tail -1

на сервере (запускается одновременно с клиентом):

UDP
CPULOG=NAME.udp.cpu.log; sar 10 6 >"$CPULOG" & iperf3 -s -i 10 -f m -1; tail -1 "$CPULOG"

TCP
CPULOG=NAME.tcp.cpu.log; sar 10 6 >"$CPULOG" & iperf3 -s -i 10 -f m -1; tail -1 "$CPULOG"

Конфигурация туннелей


ipipou
сервер
/etc/ipipou/server.conf:
servernumber 0fou-dev eth0fou-local-port 10000tunl-ip 172.28.0.0auth-remote-pubkey-b64 eQYNhD/Xwl6Zaq+z3QXDzNI77x8CEKqY1n5kt9bKeEI=auth-secret topsecretauth-lifetime 3600reply-on-auth-okverb 3

systemctl start ipipou@server

клиент
/etc/ipipou/client.conf:
clientnumber 0fou-local @eth0fou-remote SERVER_IP:10000tunl-ip 172.28.0.1# pubkey of auth-key-b64: eQYNhD/Xwl6Zaq+z3QXDzNI77x8CEKqY1n5kt9bKeEI=auth-key-b64 RuBZkT23na2Q4QH1xfmZCfRgSgPt5s362UPAFbecTso=auth-secret topsecretkeepalive 27verb 3

systemctl start ipipou@client

openvpn (без шифрования, с аутентификацией)
сервер
openvpn --genkey --secret ovpn.key  # Затем надо передать ovpn.key клиентуopenvpn --dev tun1 --local SERVER_IP --port 2000 --ifconfig 172.16.17.1 172.16.17.2 --cipher none --auth SHA1 --ncp-disable --secret ovpn.key

клиент
openvpn --dev tun1 --local LOCAL_IP --remote SERVER_IP --port 2000 --ifconfig 172.16.17.2 172.16.17.1 --cipher none --auth SHA1 --ncp-disable --secret ovpn.key

openvpn (c шифрованием, аутентификацией, через UDP, всё как положено)
Настроено используя openvpn-manage

wireguard
сервер
/etc/wireguard/server.conf:
[Interface]Address=172.31.192.1/18ListenPort=51820PrivateKey=aMAG31yjt85zsVC5hn5jMskuFdF8C/LFSRYnhRGSKUQ=MTU=1440[Peer]PublicKey=LyhhEIjVQPVmr/sJNdSRqTjxibsfDZ15sDuhvAQ3hVM=AllowedIPs=172.31.192.2/32

systemctl start wg-quick@server

клиент
/etc/wireguard/client.conf:
[Interface]Address=172.31.192.2/18PrivateKey=uCluH7q2Hip5lLRSsVHc38nGKUGpZIUwGO/7k+6Ye3I=MTU=1440[Peer]PublicKey=DjJRmGvhl6DWuSf1fldxNRBvqa701c0Sc7OpRr4gPXk=AllowedIPs=172.31.192.1/32Endpoint=SERVER_IP:51820

systemctl start wg-quick@client

Результаты


Сырая страшненькая табличка
Загрузка CPU сервера не очень показательна, т.к. там крутится много других сервисов иногда они жрут ресурсы:

proto bandwidth[Mbps] CPU_idle_client[%] CPU_idle_server[%]# 20 Mbps канал с микрокомпьютера (4 core) до VPS (1 core) через Атлантику# pureUDP 20.4      99.80 93.34TCP 19.2      99.67 96.68ICMP latency min/avg/max/mdev = 198.838/198.997/199.360/0.372 ms# ipipouUDP 19.8      98.45 99.47TCP 18.8      99.56 96.75ICMP latency min/avg/max/mdev = 199.562/208.919/220.222/7.905 ms# openvpn (auth only, no encryption)UDP 19.3      99.89 72.90TCP 16.1      95.95 88.46ICMP latency min/avg/max/mdev = 191.631/193.538/198.724/2.520 ms# openvpn (auth only, no encryption)UDP 19.6      99.75 72.35TCP 17.0      94.47 87.99ICMP latency min/avg/max/mdev = 202.168/202.377/202.900/0.451 ms# wireguardUDP 19.3      91.60 94.78TCP 17.2      96.76 92.87ICMP latency min/avg/max/mdev = 217.925/223.601/230.696/3.266 ms## около-1Gbps канал между VPS Европы и США (1 core)# pureUDP 729      73.40 39.93TCP 363      96.95 90.40ICMP latency min/avg/max/mdev = 106.867/106.994/107.126/0.066 ms# ipipouUDP 714      63.10 23.53TCP 431      95.65 64.56ICMP latency min/avg/max/mdev = 107.444/107.523/107.648/0.058 ms# openvpn (auth only, no encryption)UDP 193      17.51  1.62TCP  12      95.45 92.80ICMP latency min/avg/max/mdev = 107.191/107.334/107.559/0.116 ms# wireguardUDP 629      22.26  2.62TCP 198      77.40 55.98ICMP latency min/avg/max/mdev = 107.616/107.788/108.038/0.128 ms


канал на 20 Mbps

сравнение пропускной способности на 20 Mbps

сравнение задержки на 20 Mbps

канал на 1 оптимистичный Gbps

сравнение пропускной способности на 1 Gbps

сравнение эффективности использования CPU: Mbps/CPU_usage

Во всех случаях ipipou довольно близок по показателям к базовому каналу, и это прекрасно!

Нешифрованный туннель openvpn повёл себя довольно странно в обоих случаях.

Если кто соберётся потестить, будет интересно услышать отзывы.

Да пребудет с нами IPv6 и NetPrickle!
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru