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

Что мы говорим богу 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 туннель следующими командами, без всяких скриптов.

на сервере:
# Подгрузить модуль ядра 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 клиента, назначенный интерфейсу 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 и иже с ним тебе в помощь (например в этом комментарии 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) — это такая специальная штука для дилетантов, не умеющих разрабатывать модули ядра, которая средствами 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:
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@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/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

Результаты


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

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

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

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

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

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

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

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

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

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

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

Средняя зарплата в IT

110 450 ₽/мес.
Средняя зарплата по всем IT-специализациям на основании 7 043 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 12

    +1

    "Нешифрованный туннель openvpn повёл себя довольно странно в обоих случаях" — медленный оказался?
    Попробуйте добавить --tun-mtu 60000 --mssfix 0


    Ну и для более полного тюнинга:
    https://community.openvpn.net/openvpn/wiki/Gigabit_Networks_Linux

      0
      В первом случае (20 Mbps) он раз-за разом показывал чуть меньшее время отклика даже по сравнению с основным каналом, хотя ожидается чуть большее.
      В во втором (Gbps) казалось бы с гораздо более качественным соединением упёрся в 12Mbps (для TCP) при тех же параметрах. Выставлять большие --tun-mtu имеет смысл, чтоб за 200-300 Mbps разгонять openvpn, поэтому ожидаемо, что на обычных параметрах больше 200-300 не вышло, но 12 уж слишком мало. Может перепроверю в другое время. Возможно по какой-то причине именно трафик openvpn направлялся по другому сильно медленному маршруту где-то посередине. Я проверял несколько раз, в то же время другие туннели вели себя адекватно. Хотя особо с точностью измерений не заморачивался, просто общую картину хотелось получить.
      +1
      Почему бы не взять IPsec с NAT-T с cipher и auth == NULL? И не нужно костылей, а если тяготит IKE, то можно через setkey из racoon статикой все забить, а то и вообще через ip xfrm.
        0

        Идеальный совет! Айписек — палочка-выручалочка

          0

          Я бы не называл ipipou костылём, это скорее надстройка над ipip-over-fou.


          Для решения одной задачи можно использовать множество способов, IPsec+NAT-T, тоже может подойти, как и OpenVPN, и другие.
          Я бы был не прочь подробнее узнать о преимуществах IPsec+NAT-T (и остального набора непонятных мне буков), пока только вот эту статью нашёл с поверхностным описанием.


          ipipou (ipip-over-fou) более нишевое решение — только для Linux, с упором на производительность и меньший оверхед, и по моим ощущениям, опередит в этом плане IPsec+NAT-T (ибо основной трафик в ядре, в накладных расходах только заголовок внутреннего IP, и лишь частично бесполезный внешний UDP). У кого есть желание может провести эксперимент. У меня личная трансцендентная неприязнь к IPsec, я с ним плохо знаком, поэтому это буду не я.

            +1
            OpenVPN торомоз.
            IPsec полностью реализован в ядре, потому трафик при cipher и auth == NULL будет только получать заголовок туннельного режима и UDP от NAT-T. То есть все как у вас, только стандартное и работает со всем оборудованием и ПО, что умеет IPsec в туннельном режиме, а не только со свежим Linux.
            Насчет неприязни — это за счет сложности устройства этого стека протоколов, зато после понимаешь что он позволяет делать очень гибкие вещи. Ну да и ладно, я просто рассказал, что уже давно есть 1-в-1 альтернатива, которая стандартна и работает.
              0

              Про 1-в-1 только из этого комментария понятно стало. В любом случае спасибо за информацию, даже если IPsec окажется лучшим решением стоило создать альтернативу, и написать статью, чтобы об этом узнать.


              OpenVPN хоть и тормоз зато универсален, можно настроить и потюнить под очень разные задачи, впрочем как и IPsec, наверно.

                0

                Я тут почитал, и как понял, если взять IPsec с NAT-T с cipher и auth=null, то это аналог ipip-over-fou безо всякой аутентификации. С ipipou же можно настроить аутентификацию соединения на том же порту (правило nftables для передачи в nfqueue работает как своего рода демультиплексор для протокола аутентификации и fou).

                +1
                И еще в плюс IPsec — он умеет в IKE Mediation, а это значит что защищенное пробитие NAT для двух хостов в серых сетях уже встроено в протокол.
              0
              Есть еще вот такой проект: samy.pl/chownat Позволяет коннектиться 2-м клиентам из-за ната, но одному из них надо знать внешний адрес другого.
                0

                Тогда уж pwnat, как более совершенная версия, от того же автора, основана на том же методе.
                Для прямого соединения между хостами за NAT ещё часто пользуются STUN, например, вот в этой статье автор тоже использовал IPIP-over-FOU вместе со STUN для туннелирования сквозь NAT.


                Но это не всегда работает, например с самым непролазным Carrier-grade NAT, хотя при должном упорстве, если время позволяет, и его бывает можно пробить, если долго долбить перебирая порты. Я как-то пробовал и для простых случаев использовать pwnat, но не получилось.


                Может дойдут руки добавить методы для обхода NAT в ipipou, пока же нужен хост с публичным IP.

                0
                Хочу предложить тыжпрограммистам изначально закладывать возможность подключения из JavaScript, и поэтому чтоб в основе был WebRTC. Но при этом чтоб адреса были ipv6.

                Предлагаемая схема: нарезаем в приватном пространстве ipv6 кусок, резервируем часть оставшихся битов под верхний домен. Берём publicsuffixlist, нумеруем tld и прочее его содержимое, а дальше предлагаем желающим получить место в этом пространстве регать домены вида base32.tld и поднимать там сервер WebRTC и/или WebSocket. Для обычных программ делаем движок, который через драйвер tuntap умеет установить подключение.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое