Строим сеть на Qeli

от автора

Для понимания сути вопроса — немного истории:
Жила-была CRM, частная, корпоративная — хостилась на удаленном сервере у надежного хостера, сотрудники из разных филиалов заходили в нее и работали свою работу.
Никаких тысяч серверов, никакого кровавого энтерпрайза — всё тихо, мирно, почти по-домашнему.
И работало это долго, пока враги не начали ломать интернет.

У части пользователей пропадал доступ к сайту, работать они не могли — сначала помогал VPN, но потом враги начали бороться и с VPN.
Попробовали разнести точки входа по серверам по локациям, сохранив единую базу — враги начали блокировать межсерверные соединения.
А тут еще у одного знакомого вдруг сервер отключили (другие враги, не местные) — значит, надо забирать базу локально.
У другого знакомого локальный сервер поломался — значит, нужно делать удаленную резервную площадку.

Ну ок, системы разнесли, резервы понастроили — а как к ним обращаться, если напрямую нет соединения? Ну прокинули очередной VPN, повесили на какой-то IP адрес канала — а завтра этот канал не работает и этот адрес теперь недоступен, надо менять. Утро начинается не с кофе, утро начинается с проверки «что сегодня и как заблокировали».

не дай упасть

не дай упасть

Решение, конечно, нашлось, и я о нем писал: каждому серверу был выделен уникальный IP-адрес, не связаный ни с чем, ни с каналами, ни с провайдерами, все сервисы — на этих адресах, а маршрутизация между ними — средствами OSPF через временные по сути каналы связи. Отвалится один — не страшно, мы пойдем другим путем.
OpenVPN отвалился первым, WireGuard вторым, IPsec за пределами локальной сети так и не заработал, но этим, к счастью, варианты не ограничиваются.
Называть мы их не будем, когда приходится играть в карты с шулерами — лучше держать в рукаве десяток запасных тузов.

И вот, появился Qeli. Плюс в том, что в нем сразу предусмотрены несколько возможных протоколов, он один — как нож со сменными лезвиями.
Решено было добавить его в систему каналов. Во-первых, потестировать, а во-вторых — если не получится, ну, не получится, бывает.

По топологии — это “звезда”: к одному серверу может подключаться сразу несколько клиентов, и обмениваться трафиком. То есть, строго говоря, можно назначить один из узлов сервером — остальные подключатся к нему и будут через него работать.
Очевидный минус — между двумя “лучами” задержка суммируется — до сервера и от сервера.
Кроме того, получаем единую точку отказа: если что-то пойдет не так — связь потеряется у всех.

Но ведь можно каждый узел сделать сервером, и каждый же — клиентом для всех остальных.
Конечно, лютый оверинжиниринг, но и условия у нас, скажем так, далеки от лабораторных.

Основных проблем виделось две: много ручной работы по настройке, и разные версии ОС на серверах, а бинарник qeli привязан к конкретной версии GLIBC, для которой он был собран.

Начнем, конечно, со второй:
если создать docker контейнер с ОС нужной версии — программу можно запустить в контейнере!

docker run -ti --name qeli --privileged --network host -v /opt:/opt debianapt updateapt upgradeapt install iproute2cd /opt./qeli server -c server.conf &...

Заранее положив в /opt бинарник и конфиги.
Важный нюанс — нужно установить пакет iproute2, в котором утилита ip — иначе ничего не будет работать.

Серверный конфиг, например, такой:

[auth]require_client_key_proof = truebind_static_to_session = truebrute_force.max_attempts = 5brute_force.window_secs = 60brute_force.lockout_secs = 300[logging]level = infofile = /var/log/qeli/server.log[web]enabled = false[profile:udp-obfs]enabled = truebind.address = 0.0.0.0bind.port = MY_PORTbind.transport = udptun.mtu = 1460tun.queues = 0tun.name = qserver# 240 netmask, nets 0_, 16_, 32, 48, 64, 80, 96, 112, 128, 141, 160, 176, 224, 240tun.netmask = 255.255.255.240pool.cidr = 192.168.168.32/28tun.address = 192.168.168.33pool.exclude = 192.168.168.33routing.nat.enabled = falserouting.forward_private = truerouting.client_to_client = truedns.enabled = falseobf.mode = obfsobf.obfs_key = MY_KEYobf.padding.enabled = falseobf.heartbeat.enabled = trueobf.heartbeat.interval_ms = 5000obf.heartbeat.jitter_ms = 2000perf.connection.max_clients = 64perf.connection.handshake_timeout_secs = 10perf.connection.idle_timeout_secs = 0

Заранее рассчитаны несколько подсетей для нескольких серверов:
Например, подсеть 192.168.168.128/28, адрес сервера будет 192.168.168.129 (128 + 1), адреса клиентов соответственно от 130 до 139, 140 бродкаст и 141 — следующая сеть.

Методика расчета простая: записываем биты последнего октета, из расчета «маска подсети — 24», в данном случае меняем 4 первых бита:

echo $(( 2#01110000 )) = 112echo $(( 2#10000000 )) = 128echo $(( 2#10010000 )) = 141

При старте нового сервера формируется новый серверный key, что-то типа такого: 88936461a63b49214745065699c7eac148a4260fcf4695f1dc1f7d710b71371b
Его можно получить командой:

./qeli show-identity

Также, нужно завести юзера:

./qeli add-client username

Можно указать сразу пароль, а можно получить некий случайный JKj6788sdfJJK2333
Юзеры с паролями сохраняются в /etc/qeli/users.conf.
В принципе, каталог /etc/qeli можно тоже копировать между серверами, подключая при запуске контейнера, это уменьшит количество операций по настройке, но почему-то решил делать для разных серверов разные

Теперь настраиваем конфиг клиента для этого сервера:

[qeli]server = SERVER_IP:MY_PORTproto = udpuser = usernamepass = JKj6788sdfJJK2333key = 88936461a63b49214745065699c7eac148a4260fcf4695f1dc1f7d710b71371bmode = obfsobfs_key = MY_KEYdev = qelialfamtu = 1460mtu_probe = falsegateway = falseroute_local = truekill_switch = falseallow_ipv6_leak = truedns = offautostart = false#post_up = /etc/qeli/up.sh#post_down = /etc/qeli/down.sh[logging]level = infofile = /var/log/qeli/client.log

Сохраняем его в файл типа client-alfa.conf, и идем на следующий сервер.
Устанавливаем, настраиваем подсеть, порты, адреса, юзера и ключи, сохраняем файл client-bravo.conf.
И так далее для всех N серверов. Почти везде — копипаста.

А потом пишем скрипты запуска всего этого зоопарка, типа такого:

cd /optsetsid ./qeli server -c server.confsetsid ./qeli client -c client-alfa.confsetsid ./qeli client -c client-bravo.confsetsid ./qeli client -c client-charly.conf...

Убираем только запуск сам-на-себя.

И запускаем:

docker start qelidocker exec qeli /opt/startup

Эти команды можно записать, например, в /etc/rc.local или какой другой стартап-скрипт.
Всё, первая часть выполнена: сервера связаны каждый с каждым, появилось по N-1 сетевых интерфейсов с соответствующими именами, пинги ходят туда-сюда.

Теперь нужно весь этот бардак упорядочить.
Имена интерфейсов на каждом узле известны, однотипны — qelialfa, qelibravo, и т.д.
Нужно только обьявить на интерфейсе lo уникальные IP-адреса и настроить связность по OSPF.

OSPF — это система обмена маршрутами к узлам сети между роутерами.
Роутер в данном случае — сам компьютер, узел сети, на котором запущены демоны маршрутизации — OSPF и BFD.
Демону OSPF указывается список интерфейсов и адресов, с которым он работает, и если с той стороны интерфейса слушает другой демон OSPF — они обмениваются сообщениями.
BFD контролирует активность интерфейсов.

Как-то так:
ospfd: — у меня есть узел 123.45.67.89, надо кому?
iface1: — принято, записал, 123.45.67.89 это к тебе, а у меня есть сеть 112.11.11.0/24
ospfd: — принято, эта сеть — к тебе
bfdd: — там похоже iface2 сдох, потыкай его палочкой
ospfd: — и правда, сдох. Через него работало 33.23.22.33, кто-нибудь знает как туда еще попасть? Никто? Вычеркиваем…

Для настройки OSPF можно использовать пакет frr в котором уже есть нужные демоны, и запускается он также в докере:

mkdir /etc/frrdocker run -ti -v /etc/frr:/etc/frr --name frr --privileged --network host debianapt updateapt upgradeapt install frrexit

Включаем нужных демонов, остальное не трогаем:

vim /etc/frr/daemonsospfd=yospf6d=ybfdd=y

Запускаем, и делаем первичную настройку:

docker start frrdocker exec frr /usr/lib/frr/frrinit.sh startdocker exec frr vtyshconfigute terminalinterface loip address 192.168.200.1/32ipv ospf area 0ipv6 ospf6 area 0exitrouter ospfospf router-id 1.4.0.1maximum-path 1exitrouter ospf6ospf6 router-id 1.6.0.1maximum-path 1exitexitwrite memoryexit

Теперь у нас OSPF подхватит и раздаст адреса на интерфейсе lo, но пока не указано кому.

И вот тут возникла неожиданная проблема:
Для работы OSPF нужно, чтобы линки между узлами пропускали мультикаст-пакеты, 224.0.0.1, причем в обе стороны. А qely версии 0.7.10 не пропускает.

Частично вопрос можно решить, обьявив в OSPF интерфейс как non-broadcast — но всё равно от сервера к клиенту пакеты с IP, отличающимся от подсети сервера, не проходят, а это значит что связи не будет.
Узлы будут знать, куда отправлять пакет — но его не пропустит qeli.

Но тут удачно вспомнилось существование протокола IPv6. Qeli его тоже не пропускает, но зато поверх линков можно создать ipv6-туннели, и делается это довольно просто:

ip tunnel add IF_NAME mode sit local LOCAL_QELI_IP remote REMOTE_QELI_IP ttl 255ip link set dev IF_NAME up mtu 1440

Сделать это нужно на обоих сторонах линка, поменяв местами local и remote.
Параметр MTU должен быть на 20 байт меньше, чем MTU линка, а его задали как 1460.
У полученного интерфейса автоматически появляются link-local ipv6 адреса, по которым прекрасно работает OSPF, точнее OSPF6, для 6 версии протокола.

И теперь, если на узлах добавить эти интерфейсы в OSPF6 — установится связь.
Только уникальный адрес на интерфейсе lo должен быть из ipv6 — например, fd00:cafe::1, fd00:cafe::2 и т.п.
Соответственно, между ними заработает сеть ipv6, в которой каждый узел будет иметь свой заданный независимый адрес, что и требовалось.

Справедливости ради — можно создать туннели mode ipip, точно такие же — но для ipv4, и они тоже работают хорошо.
Но у них не будет link-local адресов, их нужно назначать вручную, и ничего не перепутать — и вот это довольно сложно автоматизировать для N серверов.
Что ж, это лишний повод попытаться перейти на ipv6, но пока просто автоматизируем это всё, потому что создавать ручками кучу линков — ну это такое себе.

За основу берем два факта:
Во-первых, у qeli есть функция получения списка активных коннектов.

./qeli list-client

получаем список текущих подключений с выданнными адресами, а свой адрес мы знаем, он 1 в той же подсети. Это позволяет создать половинки туннелей с серверной стороны.

Во-вторых, нам известен список возможных клиентских интерфейсов — мы их задавали в конфигах.
Можно получить локальный выданный адрес, а адрес сервера мы опять знаем, он 1 в той же подсети. Это позволит создать клиентские половинки туннелей.

Получаем созданные интерфейсы, и добавляем их в OSPF через командный инерфейс vtysh.
Остается совместить одно с другим

#!/bin/bashPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin; export PATHuser='username'mtu=1440ifaces="qelialfa qelibravo qelicharly qelidelta ..."docker exec qeli /ext/qeli list-clients | while IFS= read -r line ; do  ip=$( echo $line | grep "$user" | awk '{ print $2}' )  if [ "x$ip" != "x" ]; then    last_num=$( echo $ip | sed -E 's/^.*\.([0-9]+)/\1/')    net_num=$(( ($last_num  & 2#11110000) + 1  ))    local_ip="192.168.168.$net_num"    if_ip="qsit${last_num}"    ip link show dev $if_ip    if [ $? -ne 0 ]; then      echo "create"      ip tunnel add $if_ip mode sit local $local_ip remote $ip ttl 255    fi    ip link set dev $if_ip up mtu $mtu    docker exec frr vtysh \      -c "configure terminal" \      -c "interface $if_ip" \      -c "ipv6 ospf6 area 1" \      -c "ipv6 ospf6 priority 10" \      -c "ipv6 ospf6 bfd" \      -c "exit" \      -c "exit"  fidone# clientsfor i in $ifaces ; do  ip link set dev $i up  if [ $? -eq 0 ] ; then    ip=$( ip addr show dev $i | grep 'inet ' | awk '{ print $2 }' | sed 's/\/.*//' )    if [ "x$ip" != "x" ]; then      last_num=$( echo $ip | sed -E 's/^.*\.([0-9]+)/\1/')      net_num=$(( ($last_num  & 2#11110000) + 1  ))      server_ip="192.168.168.$net_num"      # add to frr      if_ip="qsit${last_num}"      ip link show dev $if_ip      if [ $? -ne 0 ]; then        echo "create"        ip tunnel add $if_ip mode sit remote $server_ip local $ip ttl 255      fi      ip link set dev $if_ip up mtu $mtu      docker exec frr vtysh \        -c "configure terminal" \        -c "interface $if_ip" \        -c "ipv6 ospf6 area 1" \        -c "ipv6 ospf6 priority 10" \        -c "ipv6 ospf6 bfd" \        -c "exit" \        -c "exit"    fi  fidone

И просто запускаем скрипт после установки соединений. Можно несколько раз, можно хоть каждую минуту — лишнее создаваться не будет.
Только не забыть добавить на lo интерфейс ipv6 адреса, типа fd00:cafe::1/128

Вторая часть, построение сети — готова: с любого узла проходят пинги на соседний fd00:cafe::x
Причем они проходят по шифрованному каналу, по наименьшему пути, и вообще оно работает «как-то само», особенно если начать отрубать то одно, то другое соединения — просто сеть перестраивается на лету.

Третья задача — дать доступ к этой сети нескольким машинам в локалке, сидящей где-то сбоку за одним из узлов.
Можно, конечно, в лоб: еще одно qeli-соединение, еще один туннель, который включится в общую сеть.
А можно по другому:

Во-первых, можно поднять в локальной сети свою ipv6-сеть, просто добавив адреса на шлюзе

ip addr add fd00:bebe::1/64 dev iface

и на целевой машине

ip addr add fd00:bebe::2/64 dev ifaceip -6 route add default via fd00:bebe::1

а потом добавить на шлюзе правила в ospf:

docker exec frr vtyshconfigure terminalrouter ospf6redistribute staticexitipv6 route fd00:bebe::/64 ifaceexitexit

Во-вторых, можно локально ipv6 не поднимать, добавить адрес прямо из серверной сети на целевую машину и настроить статический роутинг через link-local адрес интерфейса на шлюзе:

ip -6 addr show dev iface...inet6 fe80::32a6:12ff:fe0e:951a/64 scope link...

на целевой машине:

ip addr add fd00:cafe::beer/128 dev ifaceip -6 add default via fe80::32a6:12ff:fe0e:951a dev iface

и потом добавить правила:

docker exec frr vtyshconfigure terminalrouter ospf6redistribute staticexitipv6 route fd00:cafe::beer/128 ifaceexitexit

И так и этак будет работать: с машины в сеть, и из сети — на эту машину.

Зачем? Затем, что теперь эта машина может взаимодействовать с узлами нашей сети так, как будто все они находятся в одной локалке.

Например, на локальной машине может находиться бекенд вебсервера, «морда» которого находится на узле Alfa.
Для этого на Alfa устанавливаем, например, nginx и настраиваем upstream:

vim /etc/nginx/sites-available/alfa.comupstream beer {  server [fd00:cafe::beer]:3000;}server {  listen 80;  server_name alfa.com;  location / {   return 301 https://$host$request_uri;  }}server {   listen       443 ssl;   http2 on;   server_name alfa.com;   keepalive_timeout   70;   ssl_certificate     /root/.acme.sh/alfa.com_ecc/fullchain.cer;   ssl_certificate_key /root/.acme.sh/alfa.com_ecc/alfa.com.key;   ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;   ssl_ciphers         HIGH:!aNULL:!MD5;   location / {    proxy_pass http://beer;    proxy_http_version 1.1;    proxy_set_header Upgrade $http_upgrade;    proxy_set_header Connection "";    proxy_set_header Host $host;    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;    proxy_set_header X-Forwarded-HTTPS 0;   }}

Теперь при заходе на сервер alfa.com реальное соединение идет с бекендом на сервере Beer, который стоит где-то у вас совсем в другом месте, вовсе не на Alfa.
При этом сервер bravo.com тоже может быть реально на Beer. А может быть не на Beer, а на Beef, который стоит в совсем другой локации — но всё это для вас «локальная сеть».
Мало ли для чего может пригодиться локальная сеть большого радиуса действия…

А самое интересное тут, что ipv6 оказывается действительно удобен. И это даже в режиме «локальных» fd00-адресов.

ссылка на оригинал статьи https://habr.com/ru/articles/1059102/