Для понимания сути вопроса - немного истории:
Жила-была 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-адресов.