Верно, и богу шифрования сегодня скажем то же.
Здесь будет о нешифрованном 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) это такая специальная штука
Для некоторых языков программирования есть биндинги для работы с 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
Результаты
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
канал на 1 оптимистичный Gbps
Во всех случаях ipipou довольно близок по показателям к базовому каналу, и это прекрасно!
Нешифрованный туннель openvpn повёл себя довольно странно в обоих случаях.
Если кто соберётся потестить, будет интересно услышать отзывы.
Да пребудет с нами IPv6 и NetPrickle!