01 — Онбординг и жизненный цикл клиента
Как клиент попадает в систему, кто хранит приватный ключ, чем отличаются управляемые и личные устройства, и в каком состоянии клиент живёт в базе.
Одноразовая ссылка — что в ней на самом деле
Главное правило: в URL лежит не конфиг, а opaque-токен. Конфиг резолвит control plane по этому токену. Если положить конфиг прямо в ссылку — он утечёт в историю браузера, логи прокси, мессенджеры и т.д.
Что нужно заложить в механику токена:
- TTL — короткое время жизни (минуты–часы). Протух — показываем понятную страницу, не 500-ку.
- Строгий single-use — пометка «использован» ставится атомарно (compare-and-set в БД), иначе при двойном клике или гонке выдашь конфиг дважды.
- Идемпотентность доставки — реальный сценарий: клиент дёрнул ссылку, MDM-пуш ушёл, но сеть моргнула до подтверждения. Повторный клик в пределах TTL должен до-доставить тот же конфиг, а не выпустить новый ключ. После TTL — только re-issue.
- Привязка к платформе — токен знает, для какого канала доставки он выпущен (см. ниже), чтобы не гадать по User-Agent.
- Re-issue (перевыпуск) — на случай потери устройства: новый ключ + удаление старого пира со всех серверов, где он мог осесть. Это требует, чтобы в БД хранился список инсталляций пира (см. машину состояний), иначе останется «сиротский» пир.
Кто генерирует приватный ключ
Это и есть суть модели безопасности, а не деталь.
- Серверная генерация (control plane создаёт пару и кладёт приватник в конфиг): неизбежна для MDM-доставки на Apple и для подписки sing-box — там приватник по определению рождается на твоей стороне. Значит приватник какое-то время живёт в канале доставки → защищаем канал (TLS, пер-девайс токен, single-use, ротация) и нигде не логируем ключ.
- Клиентская генерация (устройство само делает пару, наверх отдаёт только pubkey): самый чистый вариант — в транзите нет секрета вообще, доставляемый конфиг это публичная информация. Но требует агента/кооперирующего приложения на устройстве. Реально доступно на Linux/Windows (свой агент) и на Android (если выкатываешь своё приложение). На штатных WG-app и в sing-box клиентской генерации с отдачей одного pubkey нет.
Вывод на пальцах: где можешь поставить свой агент — генерируй ключ на клиенте и проблему защиты конфига растворяешь. Где не можешь (iOS, sing-box) — приватник едет, защищай транспорт.
Определяй канал доставки, а не «тип устройства»
User-Agent ненадёжен, и важен не тип, а как именно доставить:
| Платформа | Канал доставки конфига |
|---|---|
| Apple (iOS/macOS) | .mobileconfig (VPN payload) через MDM, либо подписка sing-box |
| Android | managed config (Android Enterprise) через MDM, либо подписка sing-box |
| Windows | .conf + скрипт (/installtunnelservice) через MDM/агент, либо подписка |
| Linux | .conf + wg syncconf через свой агент |
Один «готовый конфиг» на выходе — это четыре разных механизма. Канал лучше фиксировать в токене, а не вычислять на лету.
Managed vs unmanaged — развилка под одним URL
MDM может доставить тихо только на заэнролленное устройство. Отсюда два потока под одной ссылкой:
- Устройство уже в MDM → ссылка просто триггерит (ре-)пуш через MDM API. Тихо.
- Устройство не в MDM (BYOD) → тихого пути нет; ссылка ведёт на энроллмент или на ручную установку профиля/импорт конфига. Не тихо.
Поэтому одноразовая ссылка и MDM-пуш не заменяют друг друга — они закрывают две разные популяции устройств. Если весь парк managed, ссылка по сути схлопывается в триггер энроллмента.
Модель адресации B (выбрана)
Клиент получает разный tunnel-IP в каждом регионе (пул на регион). Каждый региональный сервер
владеет своим агрегатом — BGP чистый, /32 никуда не мигрируют.
Следствие для миграции: при смене региона у клиента меняется не только endpoint, а минимум:
Address— новый IP из пула нового региона;Endpoint— новый сервер;Peer PublicKey— публичный ключ сервера в каждом регионе свой (это часто забывают);- возможно
AllowedIPs— если маршрутизируемые ресурсы регион-зависимы.
Это полноценный перенос пира, см. файл 03. (Альтернатива — модель A, фиксированный глобальный IP
клиента, мигрирующий по BGP /32 — отвергнута из-за риска двойного анонса/split-brain маршрута.)
Машина состояний клиента
Минимальный жизненный цикл, который надо вести в БД:
issued ──(клик по ссылке, доставка)──▶ delivered ──(handshake виден)──▶ active
│
┌──────────────(миграция)─────────┘
▼
migrating ──(handshake на новом сервере)──▶ active
active/любое ──(re-issue / потеря / отзыв)──▶ revoked
Что хранить на клиента (помимо состояния):
client_id,telegram_id(или иной principal);current_region,assigned_ip(пул региона);wg_pubkeyклиента (приватник на сервере не храним дольше доставки);peer_installations— список серверов, где сейчас стоит пир этого клиента. Без него миграция и отзыв рано или поздно оставят сиротские пиры.
Грабли
- Не клади конфиг в URL — только токен.
- Single-use должен быть атомарным, иначе гонка выдаст ключ дважды.
- На миграции меняется и серверный pubkey, а не только endpoint.
- Веди
peer_installations, иначе отзыв неполный.