Приватная mesh-сеть

Как объединить серверы в защищённую сеть, где каждый видит каждого по короткому имени и приватному IP — независимо от того, в каком дата-центре и у какого провайдера они стоят.

Это фундамент для всего остального: кластеры PostgreSQL, Redis, MongoDB, RabbitMQ и MinIO должны общаться по приватным адресам, а не по публичному интернету. Отказоустойчивость без Kubernetes (HA.md) начинается именно здесь.


Содержание


Что это и зачем

Три сервера в разных дата-центрах. Приложение на первом, PostgreSQL на втором, Redis на третьем. Без приватной сети у вас три плохих варианта:

  1. Открыть 5432 в интернет. Пароль — единственная преграда. Любой скан портов найдёт вашу базу за часы.
  2. SSH-туннели. Работают, пока не упадёт autossh. Не переживают перезагрузку без плясок. Не масштабируются на пять узлов.
  3. VPN провайдера. Привязывает вас к одному провайдеру и его тарифу.

Приватная сеть mitdev даёт четвёртый: у каждого сервера появляется постоянный приватный адрес (10.100.0.1, 10.100.0.2, …) и DNS-имя (db.internal), трафик между узлами шифруется WireGuard, а публичные IP остаются на месте и продолжают обслуживать внешних пользователей.

                     публичный интернет
                            │
        ┌───────────────────┼───────────────────┐
        │                   │                   │
   ┌────┴────┐         ┌────┴────┐         ┌────┴────┐
   │  app-1  │         │  db-1   │         │ cache-1 │
   │ Hetzner │         │  OVH    │         │  DO     │
   └────┬────┘         └────┬────┘         └────┬────┘
        │                   │                   │
   10.100.0.1          10.100.0.2          10.100.0.3
   app-1.internal      db-1.internal       cache-1.internal
        │                   │                   │
        └───── шифрованные туннели WireGuard ───┘
                    каждый с каждым

Строка подключения приложения превращается из postgresql://app:[email protected]:5432/app (страшно) в postgresql://app:[email protected]:5432/app (нормально) — а сам порт 5432 закрывается для всего мира командой mitdev net bind postgresql private.


Модель: full-mesh без хаба

Классический VPN — звезда: все узлы подключаются к центральному серверу. Упал центр — упала сеть.

mitdev строит полную сетку (full-mesh): каждый узел держит прямой зашифрованный туннель с каждым другим. Центрального сервера нет, поэтому падение любого узла — включая тот, где сеть создавали, — не влияет на связь между остальными.

   звезда (hub-and-spoke)          full-mesh (mitdev)
                                   
       A                                A
       │                              ╱   ╲
   ┌───┴───┐                        ╱       ╲
   │  HUB  │  ← точка отказа      B ───────── C
   └───┬───┘                        ╲       ╱
       │                              ╲   ╱
       B                                D

Цена модели — при добавлении узла его нужно прописать на всех существующих. mitdev сводит это к одной команде на узел (или к нулю команд, если использовать net join --ssh).

Каждый узел хранит полный реестр пиров и из него рендерит /etc/wireguard/wg0.conf. Реестр — единственный источник правды; конфиг всегда можно пересобрать командой mitdev net repair.


Что делает mitdev за вас

Ручная настройка WireGuard на трёх узлах — это генерация шести ключей, ручное распределение публичных ключей, выбор непересекающейся подсети, раздача IP без коллизий, правила UFW, net.ipv4.ip_forward, юнит автозапуска и записи в /etc/hosts. Двенадцать шагов, каждый — шанс на опечатку.

mitdev net берёт на себя:

Задача Как решено
Генерация ключей Ed25519-пара на узел, 0600, при create/join
Выбор подсети Проверка пересечений с Docker, k3s (10.42/10.43), другими сетями mitdev и локальными маршрутами
Раздача IP Первый свободный адрес .1…254; коллизии отклоняются на хабе
Обмен ключами Base64-токен, никакого удалённого выполнения команд
Файрвол ufw allow <порт>/udp с комментарием mitdev: wireguard
Маршрутизация /etc/sysctl.d/99-mitdev-wg.conf — IPv4 + IPv6 forwarding
DNS Блок в /etc/hosts: <имя>.internal и <имя>
Автозапуск systemctl enable wg-quick@wg0
Восстановление Watchdog-служба на каждом узле
Откат Каждое изменение wg0.conf предваряется бэкапом

Предустановочная проверка подсети — не формальность. Если вы выберете 10.42.0.0/24, а на сервере есть k3s, поды перестанут отвечать. mitdev предупредит и предложит другую подсеть:

[warn] Подсеть 10.42.0.0/24 пересекается: Kubernetes (k3s pod/service network)
Всё равно использовать 10.42.0.0/24? [y/N]

Сценарий 1: сеть из двух серверов

Есть app-1 (публичный 203.0.113.10) и db-1 (публичный 203.0.113.20). Цель: app-1 ходит в базу по приватному адресу, порт 5432 закрыт для интернета.

Шаг 1. На первом сервере — создать сеть

# app-1
sudo mitdev net create

mitdev спросит четыре вещи (в скобках — что отвечать, если сомневаетесь):

Вопрос Значение по умолчанию Комментарий
Имя сети my-production Произвольная метка, попадает в токен
Приватная подсеть (CIDR) 10.100.0.0/24 254 узла; проверяется на конфликты
Порт WireGuard (UDP) 51820 Должен быть доступен извне
Включить IPv6 (ULA)? no Скажите yes, только если действительно нужен dual-stack

Итог: узел получает 10.100.0.1, поднимается wg0, открывается 51820/udp, включается watchdog.

Приватная сеть «my-production» создана
Этот узел: app-1 → 10.100.0.1   (подсеть 10.100.0.0/24, порт 51820/udp)
Подключить второй сервер — на нём: mitdev net join <токен>
Показать токен сети: mitdev net token

Важно: порт 51820/udp должен быть открыт не только в UFW (это mitdev сделал), но и в файрволе провайдера — security group у AWS, firewall у Hetzner Cloud, network rules у Yandex Cloud. Это самая частая причина «handshake не проходит».

Шаг 2. Забрать токен сети

# app-1
sudo mitdev net token

Вывод — длинная base64-строка. Она содержит имя сети, подсеть, порт, предразделяемый ключ и реестр всех пиров. Скопируйте её целиком.

Для скриптов есть --raw — печатает только токен, без рамки и подсказок:

TOKEN=$(sudo mitdev net token --raw)

Шаг 3. На втором сервере — присоединиться

# db-1
sudo mitdev net join <вставьте-токен>

db-1 разбирает токен, генерирует свои ключи, выбирает первый свободный адрес (10.100.0.2), рендерит конфиг и поднимает туннель.

Узел присоединён к сети «my-production»
Этот узел: db-1 → 10.100.0.2
ЧТОБЫ СВЯЗЬ СТАЛА ДВУСТОРОННЕЙ — на каждом существующем узле выполните:
  mitdev net add-peer eyJNSVRERVYtV0ctUEVFUi0x...
Затем проверьте: mitdev net status

Шаг 4. Сделать связь двусторонней

db-1 знает про app-1 (адрес был в токене). app-1 про db-1 ещё не знает — его публичный ключ появился только что. Скопируйте peer-токен из вывода и выполните на первом сервере:

# app-1
sudo mitdev net add-peer eyJNSVRERVYtV0ctUEVFUi0x...

Шаг 5. Проверить

# на любом узле
sudo mitdev net status
  Приватная сеть «my-production»
  Подсеть          10.100.0.0/24
  Порт             51820/udp
  Этот узел        app-1 → 10.100.0.1
  Узлов в сети     2
  MTU              1420
  ✔ wg0            активен
  ✔ watchdog       активен (авто-восстановление, endpoint-failover)

  Пиры
  ✔ db-1 (10.100.0.2)   0.412 ms · 18 c назад

Проверка руками:

# app-1
ping -c3 db-1.internal
ssh [email protected]        # имена работают и для SSH

Шаг 6. Закрыть базу от интернета

# db-1
sudo mitdev net bind postgresql private
sudo mitdev pg allow 10.100.0.1     # разрешить app-1 в pg_hba + файрвол

Теперь PostgreSQL слушает только 10.100.0.2 и 127.0.0.1. Строка подключения для приложения:

postgresql://app:[email protected]:5432/app?sslmode=require

Сценарий 2: третий и последующие узлы

Добавляем cache-1. Правило простое: новый узел делает join один раз, каждый существующий узел делает add-peer один раз.

# на любом узле, уже входящем в сеть
sudo mitdev net token --raw
# cache-1
sudo mitdev net join <токен>
# → печатает peer-токен PEER_C
# app-1
sudo mitdev net add-peer <PEER_C>
# db-1
sudo mitdev net add-peer <PEER_C>

Для сети из N узлов добавление N+1-го требует N вызовов add-peer. Для трёх-пяти серверов это нормально. Для большего — используйте join --ssh.

Ловушка: параллельный join

Не запускайте net join на двух новых серверах одновременно с одним и тем же токеном. Оба увидят в реестре только .1 и оба самостоятельно займут .2 — получите конфликт AllowedIPs, при котором маршрутизация ломается молча.

mitdev защищается на стороне хаба: второй add-peer с занятым IP будет отклонён с готовым рецептом.

IP 10.100.0.2 уже занят узлом «db-1». Свободный адрес: 10.100.0.3.
На узле «cache-1» выполните:  mitdev net set-ip 10.100.0.3
— затем возьмите его peer-токен (mitdev net token) и снова mitdev net add-peer здесь.

Правильный порядок — подключать узлы по одному, дожидаясь завершения предыдущего.

Ловушка: одинаковые hostname

Реестр пиров дедуплицируется по имени узла, а имя берётся из hostname -s. Два свежих VPS у одного провайдера часто оба зовутся ubuntu — второй молча затёр бы первого. mitdev это ловит:

Узел с именем «ubuntu» уже в сети, но с ДРУГИМ ключом — вероятно, два сервера
с одинаковым hostname. Задайте уникальное имя: на новом узле смените hostname
(hostnamectl set-hostname <имя>), затем mitdev net remove и повторный join.

Задавайте осмысленные имена до подключения:

sudo hostnamectl set-hostname cache-1

Сценарий 3: автоподключение по SSH

Ручной обмен токенами утомителен на шести узлах. join --ssh делает всё сам: подключается к существующему узлу по SSH, забирает токен, присоединяется и рассылает свой peer-токен по всем узлам сети.

# новый сервер, ключ SSH уже разложен на app-1
sudo mitdev net join --ssh app-1.example.com
sudo mitdev net join --ssh 203.0.113.10 root      # с явным пользователем

Требования:

Это единственный режим, где mitdev выполняет команды на удалённом сервере. Если такая модель доверия вас не устраивает — используйте ручной обмен токенами: он не требует никакого доступа между узлами, кроме UDP-порта.


Токены: два разных вида

Их легко перепутать — они оба base64 и оба «токен».

Токен сети Peer-токен
Команда net token печатается после net join / net set-ip
Внутри имя, подсеть, порт, PSK, весь реестр пиров имя сети + одна строка нового узла
Куда вставлять net join на новом узле net add-peer на существующих узлах
Магический префикс MITDEV-WG-TOKEN-1 MITDEV-WG-PEER-1
Секретность высокая — содержит PSK средняя — публичный ключ и IP

Токен сети даёт возможность подключиться к сети. Обращайтесь с ним как с приватным ключом: не кладите в чат, не коммитьте, передавайте по защищённому каналу. Посмотреть содержимое, не подключаясь:

sudo mitdev net token --raw | base64 -d

Peer-токен безвреден сам по себе: без ответного add-peer на других узлах он ничего не открывает.


Привязка служб к приватной сети

mitdev net bind переключает службу между тремя режимами прослушивания. Операция идемпотентна, обратима, и каждый конфиг бэкапится перед правкой.

sudo mitdev net bind <служба> [private|public|both]
Служба Что меняется Файл
postgresql listen_addresses conf.d/zz-mitdev-listen.conf
redis bind /etc/redis/redis.conf
mongodb net.bindIp /etc/mongod.conf
rabbitmq listeners.tcp.default, management.tcp.ip /etc/rabbitmq/rabbitmq.conf
minio MINIO_OPTS --address /etc/default/minio

Режим по умолчанию — private: служба слушает приватный IP и 127.0.0.1, и больше ничего.

# типичная связка для узла БД
sudo mitdev net bind postgresql private
sudo mitdev net bind redis private

# вернуть публичный доступ (например, для отладки)
sudo mitdev net bind redis public

После выполнения mitdev печатает, что служба фактически слушает — по ss -tln, а не по содержимому конфига:

  Фактически слушает:
    10.100.0.2:5432
    127.0.0.1:5432

Если ожидаемого адреса в списке нет — служба не перечитала конфиг или упала при старте. Смотрите journalctl -u postgresql -n 50.

bind private не заменяет pg allow / правила файрвола. Он ограничивает интерфейс, на котором служба принимает соединения. Разрешение конкретному клиенту — отдельный шаг (mitdev pg allow <ip>).


Watchdog: самовосстановление сети

На каждом узле ставится служба mitdev-wg-watchdog. Лидера нет и не нужно — сетка бесхабовая, поэтому каждый узел просто следит за своими линками.

Что он делает каждые CHECK_INTERVAL (по умолчанию 15 с):

sudo mitdev net watchdog status     # состояние + последние 10 событий из journal
sudo mitdev net watchdog enable
sudo mitdev net watchdog disable

Параметры живут в /etc/default/mitdev-wg-watchdog:

IF=wg0
CHECK_INTERVAL=15
STALE_THRESHOLD=150
STATE_DIR=/var/lib/mitdev/network

Отключать watchdog осмысленно только при отладке. Обратно — mitdev net watchdog enable.


Несколько сетей на одном сервере

Один сервер может входить в несколько независимых mesh-сетей — например, по одной на проект. Флаг --project <slug> работает со всеми подкомандами net.

# сеть проекта «shop»
sudo mitdev net --project shop create
sudo mitdev net --project shop token

# сеть проекта «crm», параллельно
sudo mitdev net --project crm create
sudo mitdev net --project crm status

Что изолируется по проектам:

Ресурс Без --project (легаси) С --project shop
Интерфейс wg0 wg-shop
Состояние /var/lib/mitdev/network/ /var/lib/mitdev/network/shop/
Конфиг /etc/wireguard/wg0.conf /etc/wireguard/wg-shop.conf
Метка в /etc/hosts # mitdev-private-network # mitdev-private-network:shop
Watchdog mitdev-wg-watchdog mitdev-wg-watchdog@wg-shop

Slug нормализуется: приводится к нижнему регистру, всё кроме a-z0-9- заменяется на -. Если wg-<slug> не влезает в лимит Linux на имя интерфейса (15 символов), берётся детерминированное wg-<6 символов>-<4 hex> — коллизии между длинными именами исключены.

Подсети разных проектов не должны пересекаться. mitdev проверяет это при create и предупредит:

[warn] Подсеть 10.100.0.0/24 пересекается: приватная сеть другого проекта (10.100.0.0/24)

Разводите их заранее: shop10.100.0.0/24, crm10.101.0.0/24.

Каждая сеть требует свой UDP-порт: 51820 для shop, 51821 для crm.


Смена IP, удаление узла, миграция

Сменить приватный IP узла

sudo mitdev net set-ip 10.100.0.7

Обновляет meta, собственную строку в реестре и парный IPv6 (если сеть dual-stack). Публичный ключ и endpoint сохраняются. После этого на каждом другом узле нужно выполнить net add-peer с новым peer-токеном — иначе они продолжат слать пакеты на старый адрес.

Убрать узел из сети

На самом узле:

sudo mitdev net remove          # остановит wg0, закроет порт, удалит ключи

На каждом оставшемся:

sudo mitdev net remove-peer cache-1

Порядок важен: сначала уберите пир на остальных, потом удаляйте сеть на узле — иначе оставшиеся узлы будут секунды-минуты долбиться в мёртвый endpoint (не опасно, но шумит в логах).

Перенести узел на новый VPS

# старый сервер
sudo mitdev net export /root/net.tar.gz     # ключи + meta + реестр, режим 600

# перенесите файл по scp, затем на новом сервере
sudo mitdev net import /root/net.tar.gz

Новый сервер получает ту же личность: тот же приватный IP, тот же публичный ключ. Остальным узлам ничего менять не нужно — только endpoint обновится сам через watchdog (или сразу, если публичный IP не менялся).

Архив содержит приватный ключ WireGuard. Удалите его с обоих серверов после миграции.


Устройство: файлы состояния

/var/lib/mitdev/network/
├── meta        NAME, SUBNET, IPV6, PORT, PSK, SELF_NAME, SELF_IP, SELF_IP6   (0600)
└── peers       по строке на узел: name|pubkey|ip|endpoint|ipv6

/etc/wireguard/
├── wg0.conf    рендерится из meta+peers, не редактируйте руками            (0600)
├── wg0.key     приватный ключ                                               (0600)
└── wg0.pub     публичный ключ                                               (0644)

/etc/hosts                       блок с метками # mitdev-private-network
/etc/sysctl.d/99-mitdev-wg.conf  ip_forward
/etc/default/mitdev-wg-watchdog  параметры watchdog

Формат peers — обычный текст, читается глазами:

app-1|xK7f…=|10.100.0.1|203.0.113.10:51820|
db-1|9pQm…=|10.100.0.2|203.0.113.20:51820|

wg0.conf — производный артефакт. Любая правка руками будет затёрта следующим net add-peer / net repair. Источник правды — peers. Если нужно нестандартное AllowedIPs или свой PostUp — это не тот инструмент, ведите конфиг вручную.


Устранение неполадок

mitdev net repair — начните с него

Пересобирает конфиг из реестра, восстанавливает ключи, ip_forward, правило UFW, автозапуск и watchdog. Идемпотентен, безопасен.

sudo mitdev net repair

Пир виден, но «без handshake»

Handshake — это установление туннеля. Нет handshake → трафик не идёт вообще.

sudo wg show wg0 latest-handshakes

Причины по убыванию частоты:

  1. Порт закрыт у провайдера. UFW разрешил, security group — нет. Проверьте снаружи: nc -vzu 203.0.113.20 51820 (UDP-проверка ненадёжна, лучше смотреть счётчики: wg show wg0 transfer — если received: 0 B, пакеты не доходят).
  2. Endpoint устарел. Публичный IP узла сменился. sudo mitdev net repair на нём — endpoint перезапишется из net_public_ip.
  3. Peer-токен не добавлен на этой стороне. Классика: join сделали, add-peer забыли. mitdev net status покажет узел в реестре, но handshake не появится, потому что другая сторона не знает ваш ключ.
  4. NAT без проброса портов с обеих сторон. Если оба узла за NAT, прямой туннель невозможен. PersistentKeepalive = 25 (mitdev ставит его всегда) спасает, только когда хотя бы одна сторона имеет публичный endpoint.

Handshake есть, ping не проходит

sudo wg show wg0 transfer     # sent растёт, received нулевой?
ip route get 10.100.0.2       # уходит ли через wg0?

Скорее всего конфликт подсетей: 10.100.0.0/24 пересекается с Docker-сетью или подсетью провайдера, и ядро роутит мимо туннеля. Проверьте ip route. Лечение — пересоздать сеть на непересекающейся подсети:

sudo mitdev net remove
sudo mitdev net create      # выберите, например, 10.123.0.0/24

Ping идёт, но TCP-соединения рвутся на больших пакетах

Классический MTU. WireGuard добавляет 60–80 байт заголовков; при MTU канала 1500 внутренний MTU должен быть ~1420 (mitdev выставляет это сам). Если провайдер использует PPPoE или дополнительную инкапсуляцию, канальный MTU меньше 1500 и 1420 уже великоват.

Симптом: ping (маленькие пакеты) работает, psql/curl виснет на ответе.

# проверить, при каком размере пакеты начинают теряться
ping -M do -s 1372 -c3 10.100.0.2      # 1372 + 28 = 1400
ping -M do -s 1392 -c3 10.100.0.2      # 1392 + 28 = 1420

# снизить MTU
sudo ip link set wg0 mtu 1380

Если помогло — закрепите: добавьте MTU = 1380 в секцию [Interface]… но помните, что wg0.conf перегенерируется. Правильный путь — привести канальный MTU в порядок либо принять, что при следующем net repair значение вернётся к дефолту (в этом случае заведите issue: конфигурируемый MTU в реестре пока не поддерживается).

.internal имена не резолвятся

grep mitdev /etc/hosts

Блок пуст или отсутствует → sudo mitdev net repair (он вызывает net_sync_hosts). Учтите: /etc/hosts не используется приложениями, которые резолвят имена сами (Go с CGO_ENABLED=0 в старых версиях, некоторые JVM-настройки). Для них указывайте IP напрямую.

Узел не в сети после перезагрузки

systemctl is-enabled wg-quick@wg0     # должно быть enabled
journalctl -u wg-quick@wg0 -n 30

Частая причина — wg-quick стартует раньше, чем поднялась сеть. Watchdog починит это в течение CHECK_INTERVAL. Если watchdog выключен — включите.


Безопасность

Что защищено. Трафик между узлами шифруется (ChaCha20-Poly1305, аутентификация Poly1305) и дополнительно защищён предразделяемым ключом (PSK) — это даёт устойчивость к квантовым атакам на обмен ключами Curve25519. Аутентификация взаимная: узел без ключа в реестре не получит ответа вообще — WireGuard не отвечает на пакеты от неизвестных пиров, поэтому порт не обнаруживается сканером.

Что не защищено.

Рекомендуемая гигиена:

# 1. Уникальные hostname до подключения
sudo hostnamectl set-hostname db-1

# 2. Службы — только на приватный интерфейс
sudo mitdev net bind postgresql private

# 3. Доступ — поимённо, не подсетью
sudo mitdev pg allow 10.100.0.1        # только app-1, не 10.100.0.0/24

# 4. Публичные порты БД — закрыть
sudo mitdev firewall status            # 5432 не должно быть в списке

# 5. Проверять состояние
sudo mitdev net status
sudo mitdev doctor --deep

Что дальше