Приватная mesh-сеть
Как объединить серверы в защищённую сеть, где каждый видит каждого по короткому имени и приватному IP — независимо от того, в каком дата-центре и у какого провайдера они стоят.
Это фундамент для всего остального: кластеры PostgreSQL, Redis, MongoDB, RabbitMQ и MinIO должны общаться по приватным адресам, а не по публичному интернету. Отказоустойчивость без Kubernetes (HA.md) начинается именно здесь.
Содержание
- Что это и зачем
- Модель: full-mesh без хаба
- Что делает mitdev за вас
- Сценарий 1: сеть из двух серверов
- Сценарий 2: третий и последующие узлы
- Сценарий 3: автоподключение по SSH
- Токены: два разных вида
- Привязка служб к приватной сети
- Watchdog: самовосстановление сети
- Несколько сетей на одном сервере
- Смена IP, удаление узла, миграция
- Устройство: файлы состояния
- Устранение неполадок
- Безопасность
Что это и зачем
Три сервера в разных дата-центрах. Приложение на первом, PostgreSQL на втором, Redis на третьем. Без приватной сети у вас три плохих варианта:
- Открыть 5432 в интернет. Пароль — единственная преграда. Любой скан портов найдёт вашу базу за часы.
- SSH-туннели. Работают, пока не упадёт
autossh. Не переживают перезагрузку без плясок. Не масштабируются на пять узлов. - 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 # с явным пользователем
Требования:
- у root на новом сервере есть SSH-доступ к указанному узлу (ключ, не пароль);
- у пользователя на том узле есть право выполнить
mitdev net token(то есть root или sudo без пароля).
Это единственный режим, где 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 с):
- интерфейс
wg0упал → поднять; - у пира нет handshake дольше
STALE_THRESHOLD(150 с) → пнуть его пингом, заставив WireGuard переустановить туннель; - endpoint пира не резолвится или сменил IP (динамический адрес, смена провайдера) → перерезолвить DNS и обновить endpoint.
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)
Разводите их заранее: shop → 10.100.0.0/24, crm → 10.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
Причины по убыванию частоты:
- Порт закрыт у провайдера. UFW разрешил, security group — нет.
Проверьте снаружи:
nc -vzu 203.0.113.20 51820(UDP-проверка ненадёжна, лучше смотреть счётчики:wg show wg0 transfer— еслиreceived: 0 B, пакеты не доходят). - Endpoint устарел. Публичный IP узла сменился.
sudo mitdev net repairна нём — endpoint перезапишется изnet_public_ip. - Peer-токен не добавлен на этой стороне. Классика:
joinсделали,add-peerзабыли.mitdev net statusпокажет узел в реестре, но handshake не появится, потому что другая сторона не знает ваш ключ. - 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 не отвечает на пакеты от неизвестных пиров, поэтому порт не обнаруживается сканером.
Что не защищено.
- Токен сети = доступ к сети. Утёк токен — злоумышленник присоединится.
Ротация:
net removeна всех узлах и пересоздание. Планируйте, кому его даёте. - Один PSK на всю сеть. Компрометация любого узла раскрывает PSK для всех пар. PSK — второй рубеж, а не первый; основная защита — ключевые пары.
- Приватная сеть — не изоляция. Узел в сети видит все приватные IP всех
узлов. Если один сервер скомпрометирован, приватные адреса остальных для
атакующего открыты. Ограничивайте доступ на уровне служб
(
pg allow <конкретный ip>), а не только сетью. net exportсодержит приватный ключ. Режим файла600, но это обычный tar.gz. Не оставляйте его на диске.
Рекомендуемая гигиена:
# 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
Что дальше
- HA.md — отказоустойчивые кластеры поверх этой сети, без Kubernetes
- KUBERNETES.md — когда mesh перестаёт хватать и нужен k3s
- COMMANDS.md — полный справочник
mitdev net - SECURITY.md — модель безопасности mitdev целиком