Для понимания сути вопроса - немного истории:
Жила-была 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 debian apt update apt upgrade apt install iproute2 cd /opt ./qeli server -c server.conf & ...
Заранее положив в /opt бинарник и конфиги.
Важный нюанс - нужно установить пакет iproute2, в котором утилита ip - иначе ничего не будет работать.
Серверный конфиг, например, такой:
[auth] require_client_key_proof = true bind_static_to_session = true brute_force.max_attempts = 5 brute_force.window_secs = 60 brute_force.lockout_secs = 300 [logging] level = info file = /var/log/qeli/server.log [web] enabled = false [profile:udp-obfs] enabled = true bind.address = 0.0.0.0 bind.port = MY_PORT bind.transport = udp tun.mtu = 1460 tun.queues = 0 tun.name = qserver # 240 netmask, nets 0_, 16_, 32, 48, 64, 80, 96, 112, 128, 141, 160, 176, 224, 240 tun.netmask = 255.255.255.240 pool.cidr = 192.168.168.32/28 tun.address = 192.168.168.33 pool.exclude = 192.168.168.33 routing.nat.enabled = false routing.forward_private = true routing.client_to_client = true dns.enabled = false obf.mode = obfs obf.obfs_key = MY_KEY obf.padding.enabled = false obf.heartbeat.enabled = true obf.heartbeat.interval_ms = 5000 obf.heartbeat.jitter_ms = 2000 perf.connection.max_clients = 64 perf.connection.handshake_timeout_secs = 10 perf.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 )) = 112 echo $(( 2#10000000 )) = 128 echo $(( 2#10010000 )) = 141
При старте нового сервера формируется новый серверный key, что-то типа такого: 88936461a63b49214745065699c7eac148a4260fcf4695f1dc1f7d710b71371b
Его можно получить командой:
./qeli show-identity
Также, нужно завести юзера:
./qeli add-client username
Можно указать сразу пароль, а можно получить некий случайный JKj6788sdfJJK2333
Юзеры с паролями сохраняются в /etc/qeli/users.conf.
В принципе, каталог /etc/qeli можно тоже копировать между серверами, подключая при запуске контейнера, это уменьшит количество операций по настройке, но почему-то решил делать для разных серверов разные
Теперь настраиваем конфиг клиента для этого сервера:
[qeli] server = SERVER_IP:MY_PORT proto = udp user = username pass = JKj6788sdfJJK2333 key = 88936461a63b49214745065699c7eac148a4260fcf4695f1dc1f7d710b71371b mode = obfs obfs_key = MY_KEY dev = qelialfa mtu = 1460 mtu_probe = false gateway = false route_local = true kill_switch = false allow_ipv6_leak = true dns = off autostart = false #post_up = /etc/qeli/up.sh #post_down = /etc/qeli/down.sh [logging] level = info file = /var/log/qeli/client.log
Сохраняем его в файл типа client-alfa.conf, и идем на следующий сервер.
Устанавливаем, настраиваем подсеть, порты, адреса, юзера и ключи, сохраняем файл client-bravo.conf.
И так далее для всех N серверов. Почти везде - копипаста.
А потом пишем скрипты запуска всего этого зоопарка, типа такого:
cd /opt setsid ./qeli server -c server.conf setsid ./qeli client -c client-alfa.conf setsid ./qeli client -c client-bravo.conf setsid ./qeli client -c client-charly.conf ...
Убираем только запуск сам-на-себя.
И запускаем:
docker start qeli docker 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/frr docker run -ti -v /etc/frr:/etc/frr --name frr --privileged --network host debian apt update apt upgrade apt install frr exit
Включаем нужных демонов, остальное не трогаем:
vim /etc/frr/daemons ospfd=y ospf6d=y bfdd=y
Запускаем, и делаем первичную настройку:
docker start frr docker exec frr /usr/lib/frr/frrinit.sh start docker exec frr vtysh configute terminal interface lo ip address 192.168.200.1/32 ipv ospf area 0 ipv6 ospf6 area 0 exit router ospf ospf router-id 1.4.0.1 maximum-path 1 exit router ospf6 ospf6 router-id 1.6.0.1 maximum-path 1 exit exit write memory exit
Теперь у нас 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 255 ip 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/bash PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin; export PATH user='username' mtu=1440 ifaces="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" fi done # clients for 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 fi done
И просто запускаем скрипт после установки соединений. Можно несколько раз, можно хоть каждую минуту - лишнее создаваться не будет.
Только не забыть добавить на 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 iface ip -6 route add default via fd00:bebe::1
а потом добавить на шлюзе правила в ospf:
docker exec frr vtysh configure terminal router ospf6 redistribute static exit ipv6 route fd00:bebe::/64 iface exit exit
Во-вторых, можно локально 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 iface ip -6 add default via fe80::32a6:12ff:fe0e:951a dev iface
и потом добавить правила:
docker exec frr vtysh configure terminal router ospf6 redistribute static exit ipv6 route fd00:cafe::beer/128 iface exit exit
И так и этак будет работать: с машины в сеть, и из сети - на эту машину.
Зачем? Затем, что теперь эта машина может взаимодействовать с узлами нашей сети так, как будто все они находятся в одной локалке.
Например, на локальной машине может находиться бекенд вебсервера, "морда" которого находится на узле Alfa.
Для этого на Alfa устанавливаем, например, nginx и настраиваем upstream:
vim /etc/nginx/sites-available/alfa.com upstream 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-адресов.
