
Верно, и богу шифрования сегодня скажем то же.
Здесь будет о нешифрованном 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 туннель следующими командами, без всяких скриптов.
на сервере:
# Подгрузить модуль ядра FOU
modprobe 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 fou
ip 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 eth0
ip address add 172.28.0.1 peer 172.28.0.0 dev ipipou1
ip link set ipipou1 up
где
ipipou*— имя локального туннельного сетевого интерфейса203.0.113.1— публичный IP сервера198.51.100.2— публичный IP клиента192.168.0.2— IP клиента, назначенный интерфейсу eth010001— локальный порт клиента для FOU20001— публичный порт клиента для FOU10000— публичный порт сервера для FOUencap-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 и иже с ним тебе в помощь (например в этом комментарии INSTE предлагает IPsec с NAT-T с cipher и auth == NULL).Как это работает?
Клиент (что обычно за 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. Это связано с тем, что в Интернете бoльшая часть хостов сносно работает только с тремя наиболее популярными протоколами: 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:server
number 0
fou-dev eth0
fou-local-port 10000
tunl-ip 172.28.0.0
auth-remote-pubkey-b64 eQYNhD/Xwl6Zaq+z3QXDzNI77x8CEKqY1n5kt9bKeEI=
auth-secret topsecret
auth-lifetime 3600
reply-on-auth-ok
verb 3
systemctl start ipipou@serverклиент
/etc/ipipou/client.conf:client
number 0
fou-local @eth0
fou-remote SERVER_IP:10000
tunl-ip 172.28.0.1
# pubkey of auth-key-b64: eQYNhD/Xwl6Zaq+z3QXDzNI77x8CEKqY1n5kt9bKeEI=
auth-key-b64 RuBZkT23na2Q4QH1xfmZCfRgSgPt5s362UPAFbecTso=
auth-secret topsecret
keepalive 27
verb 3
systemctl start ipipou@clientopenvpn (без шифрования, с аутентификацией)
сервер
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/18
ListenPort=51820
PrivateKey=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/18
PrivateKey=uCluH7q2Hip5lLRSsVHc38nGKUGpZIUwGO/7k+6Ye3I=
MTU=1440
[Peer]
PublicKey=DjJRmGvhl6DWuSf1fldxNRBvqa701c0Sc7OpRr4gPXk=
AllowedIPs=172.31.192.1/32
Endpoint=SERVER_IP:51820
systemctl start wg-quick@clientРезультаты
proto bandwidth[Mbps] CPU_idle_client[%] CPU_idle_server[%]
# 20 Mbps канал с микрокомпьютера (4 core) до VPS (1 core) через Атлантику
# pure
UDP 20.4 99.80 93.34
TCP 19.2 99.67 96.68
ICMP latency min/avg/max/mdev = 198.838/198.997/199.360/0.372 ms
# ipipou
UDP 19.8 98.45 99.47
TCP 18.8 99.56 96.75
ICMP latency min/avg/max/mdev = 199.562/208.919/220.222/7.905 ms
# openvpn0 (auth only, no encryption)
UDP 19.3 99.89 72.90
TCP 16.1 95.95 88.46
ICMP latency min/avg/max/mdev = 191.631/193.538/198.724/2.520 ms
# openvpn (full encryption, auth, etc)
UDP 19.6 99.75 72.35
TCP 17.0 94.47 87.99
ICMP latency min/avg/max/mdev = 202.168/202.377/202.900/0.451 ms
# wireguard
UDP 19.3 91.60 94.78
TCP 17.2 96.76 92.87
ICMP latency min/avg/max/mdev = 217.925/223.601/230.696/3.266 ms
## около-1Gbps канал между VPS Европы и США (1 core)
# pure
UDP 729 73.40 39.93
TCP 363 96.95 90.40
ICMP latency min/avg/max/mdev = 106.867/106.994/107.126/0.066 ms
# ipipou
UDP 714 63.10 23.53
TCP 431 95.65 64.56
ICMP latency min/avg/max/mdev = 107.444/107.523/107.648/0.058 ms
# openvpn0 (auth only, no encryption)
UDP 193 17.51 1.62
TCP 12 95.45 92.80
ICMP latency min/avg/max/mdev = 107.191/107.334/107.559/0.116 ms
# wireguard
UDP 629 22.26 2.62
TCP 198 77.40 55.98
ICMP latency min/avg/max/mdev = 107.616/107.788/108.038/0.128 ms
канал на 20 Mbps


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


Во всех случаях ipipou довольно близок по показателям к базовому каналу, и это прекрасно!
Нешифрованный туннель openvpn повёл себя довольно странно в обоих случаях.
Если кто соберётся потестить, будет интересно услышать отзывы.
Да пребудет с нами IPv6 и NetPrickle!
Prividen
"Нешифрованный туннель openvpn повёл себя довольно странно в обоих случаях" — медленный оказался?
Попробуйте добавить --tun-mtu 60000 --mssfix 0
Ну и для более полного тюнинга:
https://community.openvpn.net/openvpn/wiki/Gigabit_Networks_Linux
vmspike Автор
В первом случае (20 Mbps) он раз-за разом показывал чуть меньшее время отклика даже по сравнению с основным каналом, хотя ожидается чуть большее.
В во втором (Gbps) казалось бы с гораздо более качественным соединением упёрся в 12Mbps (для TCP) при тех же параметрах. Выставлять большие --tun-mtu имеет смысл, чтоб за 200-300 Mbps разгонять openvpn, поэтому ожидаемо, что на обычных параметрах больше 200-300 не вышло, но 12 уж слишком мало. Может перепроверю в другое время. Возможно по какой-то причине именно трафик openvpn направлялся по другому сильно медленному маршруту где-то посередине. Я проверял несколько раз, в то же время другие туннели вели себя адекватно. Хотя особо с точностью измерений не заморачивался, просто общую картину хотелось получить.