Архитектура
Из каких частей состоит Mistgate, как панель общается с агентами нод, где хранятся данные и что может и чего не может каждая часть, если попадёт в чужие руки.
На этой странице
Mistgate — это две программы на Go: панель (mistgate) и агент ноды (mistgate-node). На этой странице — их части и границы между ними, протокол связи, HTTP-поверхность панели и хранилище, контракт протокольных плагинов и коротко модель безопасности. Сами понятия описаны в Обзоре.
Части системы#
браузеры, приложения с подпиской, скрипты, ИИ-агенты
|
TCP 443
|
+--------------------------------------v---------------------------------------------+
| панель: mistgate serve |
| |
| публичный слушатель --+-- сайт-ширма (всё, что не совпало ни с чем другим) |
| +-- <секретный путь>/... подписки, страницы пользователей |
| +-- админка: секретный путь или хост (или свой слушатель) |
| | /api Connect API (cookie сессии или API-токен) |
| | /mcp MCP-сервер (API-токен) |
| +-- секретное имя SNI -> точка подключения агентов (mTLS) |
| |
| модули: auth, fleet, access, protocols, subs, dns, health, update, warp, mcp ... |
| store: SQLite и миграции vault: мастер-ключ, зашифрованные секреты |
+--------------------------------------^---------------------------------------------+
| одно соединение на ноду, открывает нода
+--------------------------------------+---------------------------------------------+
| нода: mistgate-node run |
| ядро агента: соединение, исполнитель, надёжная очередь, сверка состояния |
| движки: hysteria2 (ядро внутри процесса), awg (userspace или модуль ядра) |
| хост: nftables, sysctl, journald, туннели; WARP; сертификаты; доктор; |
| самообновление |
+------------------------------------------------------------------------------------+
|
VPN-трафик из приложений и дальше в интернетМодули панели#
Панель — один процесс. Модули связывает между собой cmd/mistgate; друг о друге они знают только через небольшие интерфейсы.
Модуль (internal/panel/...) |
За что отвечает |
|---|---|
httpserver |
Слушатели, маршрутизация между сайтом-ширмой, админкой, публичными адресами и точкой подключения агентов, ограничения запросов, TLS (свои файлы или Let's Encrypt), заголовки безопасности. |
auth |
Админы и роли, passkey (WebAuthn), пароль с кодом из приложения, сессии, повторное подтверждение входа, API-токены, одобрения владельца, капча Turnstile, журнал аудита. |
instance |
Бренд (название, логотип, акцент, язык) и адреса только для чтения в Настройки → Домены. |
fleet |
CA панели, подключение нод, соединение с агентами, проверка связи, приём статистики и событий, ревизии и дельты желаемого состояния, команды нодам, API нод и обзора для админки. |
access |
Профили, серверы на нодах, группы, пользователи, устройства и учётные данные; правило «кто что получает»; содержимое подписок. |
protocols |
Реестр протокольных плагинов (Hysteria2, AmneziaWG) и их настройки, описанные схемой. |
subs, subsettings, dns |
Публичная точка подписок и страница пользователя, настройки подписок и правила по User-Agent, DNS-пресеты. |
health |
Проверки глазами клиента, алерты, отчёты доктора нод и исправления, хранение истории. |
update |
Подписанная сборка в <каталог данных>/dist, поэтапные выкатки агентов нод. |
warp |
Аккаунты Cloudflare WARP у нод. |
mcp |
MCP-сервер поверх того же API админки. |
store, vault, pagepass |
База, шифрование мастер-ключом, пароли страниц пользователей, выводимые из мастер-ключа. |
Агент ноды#
Часть (internal/node/...) |
За что отвечает |
|---|---|
agent |
Подключение к панели, одно соединение с ней с переподключениями, единственный исполнитель, через который проходят все изменения движков и хоста, надёжная очередь сообщений, команды, лог в памяти на последние 5000 записей. |
engine, hysteria2, awg |
По движку на протокол. Hysteria2 запускает официальное ядро внутри процесса; AmneziaWG работает в userspace (amneziawg-go) или на модуле ядра. |
hostctl |
Единственное состояние хоста, которым владеет агент: его таблицы nftables, базовые настройки sysctl и journald, файрвол туннелей, сведения о хосте и метрики. |
warp, egress |
Туннель WARP ноды и выходы (напрямую или через WARP), которыми пользуются движки. |
certs |
Сертификаты Let's Encrypt и самоподписанные для серверов на ноде. |
doctor |
Проверки хоста и четыре безопасных исправления. |
update, awgprep |
Самообновление из подписанных сборок; сборка модуля ядра AmneziaWG по запросу. |
Движки сами не трогают nftables, sysctl, резолвер и сертификаты: всё нужное им даёт агент.
Подключение нод и CA панели#
У панели свой удостоверяющий центр: CA на ECDSA P-256 сроком на 10 лет, создаётся при первом запуске, а его ключ хранится в базе, зашифрованный мастер-ключом. Он подписывает два вида сертификатов:
- Сертификаты нод — клиентские, на 30 дней. Ноду определяет URI в её сертификате и никогда — то, что она пишет в сообщениях.
- Серверный сертификат точки подключения агентов — на 90 дней, хранится только в памяти и выпускается на секретное имя SNI. Публичный CA в этом не участвует, поэтому имя никогда не попадает в журналы Certificate Transparency.
Подключение ноды:
- Добавить ноду создаёт ноду и одноразовый токен (256 бит, по умолчанию действует час, хранится как хеш SHA-256). Команда установки несёт адрес панели, секретное имя SNI, отпечаток CA и токен.
mistgate-node enrollсоздаёт на ноде ключ и запрос сертификата, подключается по TLS 1.3 с секретным именем и принимает сервер, только если его цепочка заканчивается на CA с закреплённым отпечатком.- Панель проверяет токен, выпускает сертификат и возвращает его вместе с CA. Повтор того же вызова в течение 10 минут (с тем же ключом) получает тот же сертификат — на случай, если ответ потерялся. После десяти неудачных попыток за минуту с одного адреса (для IPv6 — с одной сети /64) запросы на время отклоняются.
- Дальше каждый вызов идёт со взаимным TLS. Панель проверяет серийный номер сертификата при каждом рукопожатии (session tickets выключены, так что каждое рукопожатие полное) и каждые 30 секунд на уже открытом соединении: отозванный сертификат или выведенная нода отключаются.
- Когда до конца сертификата остаётся меньше 10 дней, агент продлевает его с новым ключом. Старый сертификат после продления действует ещё 10 минут — на случай, если ответ потерялся.
Агенты подключаются к публичному порту панели: TLS-рукопожатие с секретным именем попадает в точку подключения агентов, любое другое имя — на обычный публичный сайт. serve --agent-listen вместо этого даёт агентам собственный слушатель.
Соединение агента#
Каждая нода держит ровно одно двунаправленное соединение Connect поверх HTTP/2, и открывает его всегда нода. Более новое соединение от той же ноды заменяет старое.
- Приветствие. Агент начинает со своей версии, возможностей, сведений о хосте, ревизии и хеша того, что у него запущено, и номера последнего надёжного сообщения. Панель отвечает своим временем, настройками ноды и тем, что она уже получила.
- Проверка связи. Считается любое сообщение от ноды. Статистика уходит каждые 10 секунд, даже пустая, так что заодно служит пульсом. После 90 секунд тишины панель закрывает соединение (настраивается для каждой ноды); нода без соединения меньше 10 минут — «Хостер моргнул», дольше — «Недоступна». Агент переподключается с задержкой от 1 до 60 секунд.
- Возможности. Новые функции добавляются, не ломая старое. Агент сообщает, что умеет (
doctor/1,update/1,awg/1,warp/1и так далее), и панель отправляет функцию только тем агентам, которые её назвали. Старый агент с новой панелью продолжает работать; админка показывает, для чего агента нужно обновить.
Желаемое состояние и хеш применённого#
Истина — у панели. Для каждой ноды она вычисляет желаемое состояние: все серверы на ноде с полными настройками и учётными данными всех пользователей, которым они положены, плюс настройки WARP этой ноды.
- Полное состояние перечисляет всё; всё, чего в нём нет, удаляется. Дельта переводит одну ревизию в следующую, чтобы добавление пользователя не пересылало всё целиком.
- Обе стороны вычисляют одинаковый хеш состояния (общий код в
internal/statehash). Применив состояние, агент считает хеш того, что на самом деле держат его движки, и возвращает его. Совпало — всё синхронно; не совпало — событиеstate_drift, повторная отправка полного состояния и алерт, если расхождение держится. - Смена учётных данных только подменяет их набор у работающего сервера; смена его настроек перезапускает только этот сервер.
- Нода получает только то, чем проверяет пользователей. Для Hysteria2 это SHA-256 токена каждого пользователя, а не сам токен; для AmneziaWG — публичный ключ и общий ключ каждого устройства, но никогда не его закрытый ключ.
- Последнее применённое состояние агент хранит на диске и запускает при старте, так что нода продолжает работать, пока панель недоступна.
- Учётные данные с датой окончания агент убирает сам, когда дата наступает, даже без панели.
Статистика и события#
Пакеты статистики (трафик по учётным данным, открытые сессии, метрики хоста, состояние каждого сервера) и события — это надёжные сообщения: у каждого есть порядковый номер, и агент хранит сообщение, пока панель не подтвердит его. Панель записывает каждое в одной транзакции вместе с последним номером, поэтому пакет, повторно отправленный после переподключения, не учитывается дважды. Пока панель недоступна, агент копит до 6 часов пакетов (объединяя пакеты одного часа, когда ждут больше 360), а если пришлось выбросить старые, сообщает событием stats_dropped. Трафик считается по часовым корзинам. Пользователь, превысивший квоту, отключается сразу после пакета, в котором это случилось.
Отчёты доктора — снимки, а не надёжные сообщения: важен только самый свежий. На команды (перезапустить сервер, сбросить сессии, выйти из флота, применить исправление, обновиться, откатиться, отдать лог) агент отвечает ровно одним результатом на каждую.
HTTP-поверхность панели#
| Слушатель | Что отдаёт |
|---|---|
Публичный (--listen) |
Сайт-ширму; публичные адреса под секретным путём подписок (подписки, страницы пользователей, их самообслуживание, логотип бренда); админку, если она на секретном пути или секретном хосте; точку подключения агентов для секретного имени SNI. |
Админка (--admin-listen, по желанию) |
Только админку, по обычному HTTP, для режима с отдельным слушателем. |
Агенты (--agent-listen, по желанию) |
Только точку подключения агентов, по TLS. |
ACME HTTP (--acme-http, вместе с --acme-domain) |
HTTP-01 для Let's Encrypt и перенаправление на HTTPS. |
Как решает публичный слушатель, по порядку: TLS-соединение с секретным именем агентов уходит в точку подключения агентов; запрос к секретному хосту админки или под секретным путём админки — в админку; запрос под секретным путём подписок — обработчику подписок; всё остальное получает сайт-ширму. Неканонические пути, неверные префиксы и неизвестные хосты заканчиваются одним из двух ответов ширмы (её страницей или её 404), и любой ответ 404 занимает не меньше 8 мс, так что неизвестный токен подписки (одно чтение базы) не отличить от неизвестного адреса по времени. Даже 404 или 405, возникшие внутри обработчика подписок, заменяются на 404 ширмы.
Админка — это одностраничное приложение, Connect API под <админка>/api/ и MCP-сервер по адресу <админка>/mcp. API закрыт проверкой сессии: cookie сессии (__Host-sid, Secure, HttpOnly, SameSite=Strict) или API-токен в заголовке Authorization: Bearer. Ответы админки несут строгую Content-Security-Policy, X-Frame-Options: DENY, no-store, noindex и HSTS при TLS; меняющие данные запросы с чужих origin отклоняются.
У каждого клиента (адрес IPv4 или сеть IPv6 /64) свой запас запросов на каждой стороне: публичной, в админке и у агентов. Сверх запаса — ответ 429 в стиле сайта-ширмы. У подписок свои ограничения сверх этого: сеть, перебравшая 20 неизвестных токенов за минуту, 15 минут получает только ширму; один токен можно забрать 60 раз в час; одна ссылка, которой за день пользовались больше чем из 8 сетей, порождает событие «ссылкой поделились».
Хранилище#
- SQLite через
modernc.org/sqlite(чистый Go, без cgo), в режиме WAL, с одним соединением на запись и пулом из четырёх соединений только для чтения. Таблицы STRICT; идентификаторы — 128-битные случайные строки с префиксом вида (nod_,usr_, ...); время — в секундах Unix. - Миграции — SQL-файлы goose, встроенные в бинарник (
internal/panel/store/migrations) и применяемые при каждом открытии базы. Применённую миграцию никогда не правят; изменения приходят новыми файлами. - Секреты запечатаны XChaCha20-Poly1305 под мастер-ключом, а идентификатор строки служит дополнительными данными, так что запечатанное значение нельзя перенести в другую строку. Пароли хранятся как хеши argon2id; API-токены, токены подключения и токены подписок ищутся по их SHA-256. Пароли страниц пользователей выводятся из мастер-ключа и ссылки и нигде не хранятся.
- Файлы — см. устройство каталога данных в Конфигурации.
Контракт протокольного плагина#
Протокол — это две половины с общим словарём (internal/plugin): описание сервера на ноде, учётные данные пользователя, приращения трафика, сессии, состояние.
Половина панели (internal/panel/protocols) описывает протокол и собирает конфиги:
| Метод | Зачем |
|---|---|
ID, DisplayName |
hysteria2 / «Hysteria2», awg / «AmneziaWG». |
SettingsSchema, DefaultSettings, Validate, Summary |
JSON Schema, по которой админка рисует форму профиля, значения по умолчанию со сгенерированными секретами, проверка по полям, строка-сводка на карточке профиля. |
| сборка сервера на ноде | Превращает настройки профиля, ноду и её переопределения в описание, которое запускает нода. |
IssueCredential |
Создаёт учётные данные устройства: секрет, который предъявляет клиент, и проверочные данные, которые уходят на ноды. |
Clients, Render |
Какие приложения умеют протокол и в каких форматах, и его часть подписки в нужном формате. |
Doctor |
Проверки протокола для доктора флота. |
Необязательные интерфейсы покрывают то, что нужно не всем протоколам: собственный ключ у каждого сервера (им AmneziaWG создаёт пару ключей сервера), по учётным данным на устройство и профиль (PerDevice), минимальные версии клиентов (ClientRequirements) и исправление настроек перед проверкой (SettingsNormalizer). Точные сигнатуры — в internal/panel/protocols/protocol.go.
Половина ноды (internal/node/engine) его запускает. Движок применяет описание сервера с учётными данными (повторное применение ничего не ломает; учётные данные меняются без перезапуска), убирает сервер, собирает приращения трафика и сессии, сбрасывает сессии, сообщает, что держит (для хеша состояния), и своё состояние. От агента он получает сертификаты, выходы (напрямую или WARP), DNS ноды и обработчик сайта-ширмы для запросов без авторизации.
Оба реестра встроены в бинарники (internal/panel/protocols/builtin и cmd/mistgate-node/wire.go); загрузки плагинов во время работы нет. Внешние протокольные плагины, первым из которых станет VLESS REALITY, — Планируется.
Модель безопасности коротко#
Подробнее, со входом, ролями, повторным подтверждением, сессиями и аудитом, — на странице Безопасность.
Украденный API-токен
- Может вызывать только процедуры из разрешённого для токенов списка и только в пределах своего профиля: профиль только для чтения — чтение; оператор ещё и управляет пользователями (создать, изменить, включить и выключить, продлить, сбросить трафик, удалить устройство) и запускает проверки; админ ещё и читает журнал аудита.
- Никогда не получает ссылку подписки, ключ устройства или пароль страницы: ни одна доступная токенам процедура их не возвращает.
- Не проходит повторное подтверждение входа, не создаёт и не отзывает токены, не меняет настройки входа, не управляет нодами и профилями. Применить исправление доктора или запустить, приостановить и отменить выкатку можно только через MCP и только после того, как владелец одобрит план в админке.
- Истекает (по умолчанию через 90 дней, максимум через 365) и ограничен по частоте запросов. Его изменения попадают в журнал аудита под именем токена, чтение — не чаще раза в минуту, с адресом, откуда пришёл запрос. Отозвать его можно в разделе Интеграции.
Украденная ссылка подписки
- Даёт то же, что есть у человека: конфиги его серверов и его страницу, а с ней и самообслуживание ключей AmneziaWG, если оно включено. Пароль страницы, если включён, защищает страницу и самообслуживание, но не саму подписку.
- Не даёт ничего о других пользователях, об админке и о нодах, кроме их публичных адресов.
- Каждое приложение, забравшее ссылку, появляется среди устройств человека, а использование из многих сетей порождает событие «ссылкой поделились». Выдайте Новую ссылку (старая сразу перестанет работать) и удалите незнакомые устройства: их учётные данные уйдут со всех нод.
Взломанная нода
- Хранит то, что нужно этой ноде: секреты профилей её серверов (пароли обфускации, самоподписанный ключ, ключи серверов AmneziaWG), свой ключ WARP и проверочные данные пользователей, которым она положена. Токены Hysteria2 тех клиентов, которые к ней подключаются, она видит.
- Может говорить с панелью только от своего имени и только через точку подключения агентов: сообщать трафик, события и состояние своих серверов (пакет, где в среднем больше 10 Гбит/с, отбрасывается). Она не может вызвать API админки, прочитать секреты других нод или увидеть пользователей, которые ею не пользуются.
- Вывести из флота сразу отзывает её сертификат; после этого смените секреты профилей, которые на ней работали.
Взломанная панель или её каталог данных
- Это ключи от всего: мастер-ключ расшифровывает все хранимые секреты, а панель решает, что запускать на каждой ноде.
- И всё же она не заставит ноду запустить код, который вы не подписали: агенты принимают обновление, только если подпись проходит проверку публичным ключом релиза, встроенным в них, а закрытый ключ релиза хранится офлайн, а не на панели. Команды нодам — фиксированный список; исправления доктора — четыре безопасные операции; диапазоны прыжков по портам ниже 1024 или поверх порта sshd ноды отвергают.