# Архитектура VPN-сети Montana Версия: **2026-05-18** ## Поток подключения клиента ``` ┌──────────────┐ 1. DNS resolve cdn.montana.quest │ iPhone Happ │ ─────────────────────────────────────┐ └──────┬───────┘ │ │ ▼ │ ┌──────────────────────────┐ │ │ Cloudflare DNS multi-A │ │ │ TTL=120, не proxied │ │ │ → FI 91.132.142.42 │ │ │ → FRA 89.19.208.158 │ │ │ → US 86.104.72.12 │ │ └────────┬─────────────────┘ │ │ │ 2. TLS handshake (Reality) ───────────┘ │ SNI=www.googletagmanager.com │ pbk=EkTs2aGKnFNgFZ0f7wgft2sJp3VjwFQqIrwkZKM4gD8 ▼ ┌────────────────────────────────────────────────────┐ │ xray :443 на одном из узлов (любом — один keypair) │ │ vless + xtls-rprx-vision + reality │ │ user=xray:xray, Restart=always, StartLimitBurst │ │ outbound: freedom (direct internet) │ └────────────────────────────────────────────────────┘ ``` ## Поток управления (health watchdog) ``` ┌────────────────────────────────────────────────┐ │ montana-orchestrator.service на Moscow │ │ /opt/montana-orchestrator/server.py :5020 │ │ │ │ Flask: │ │ POST /register {ip, alias, …, secret} │ ← admin │ POST /deregister {ip, secret} │ ← узел при shutdown │ GET /pool — CF records + state │ ← аудитор │ GET /health — кол-во live │ ← мониторинг │ │ │ Background thread (каждые 30 сек): │ │ for ip in registry∪pool: │ │ ok = tls_probe(ip:443, SNI) │ │ if ok && success≥2 && not in_pool: │ │ cf_api POST /dns_records │ │ if !ok && fails≥2 && in_pool: │ │ cf_api DELETE /dns_records/ │ └────────────────────────────────────────────────┘ │ ▼ Cloudflare API ``` ## Компоненты | Узел | Хост | Роль | Сервисы | |---|---|---|---| | Helsinki | 91.132.142.42 | VPN-backend | xray | | Frankfurt | 89.19.208.158 | VPN-backend | xray | | New York | 86.104.72.12 | VPN-backend | xray | | Moscow | 176.124.208.93 | orchestrator + landing + p2p (НЕ VPN) | nginx, montana-orchestrator, montana-node | ## Криптографические параметры | Параметр | Значение | Где | |---|---|---| | Reality protocol | `vless + xtls-rprx-vision` | inbound config | | Server SNI / dest | `www.googletagmanager.com:443` | realitySettings.dest | | Universal UUID | `e6d355e2-2d79-4c96-a373-3b0e6b6f4b0d` | clients[0].id | | Reality publicKey | `EkTs2aGKnFNgFZ0f7wgft2sJp3VjwFQqIrwkZKM4gD8` | URL `pbk=` | | Reality shortId | `302805bc0c25e504` | URL `sid=` | | Reality privateKey | (хранится только на узлах) | `/usr/local/etc/xray/config.json` (600) | ## Сетевые потоки | От | До | Порт | Назначение | |---|---|---|---| | Любой клиент | VPN-узел | 443/tcp | Reality VPN | | Moscow orchestrator | VPN-узел | 443/tcp | watchdog TLS-probe | | Moscow orchestrator | api.cloudflare.com | 443/tcp | DNS API | | Узел | Любой destination | 443/tcp etc. | freedom outbound | | Любой p2p-узел | Любой узел | 8444/tcp | montana-node Bootstrap+VDF | | Admin | Moscow `montana.quest/vpn/node/register` | 443/tcp | POST с secret | ## Решения и обоснования - **Universal keypair на всех backends** — Reality требует, чтобы клиент знал `publicKey` сервера. Multi-A pool с разными keypair невозможен без подписки с несколькими `vless://`. Принято: один keypair с trade-off shared-secret (см. THREAT-MODEL.md T-2). - **DNS multi-A вместо подписки с несколькими URL** — даёт «один ключ распределяет» без логики на клиенте; iOS native resolver сам ротирует ANSWER. - **TTL 120s** — баланс между скоростью переключения при отказе и нагрузкой на DNS. - **Watchdog interval 30s, threshold 2** — реакция за 60–120 секунд при минимальной нагрузке на узлы (≈ 1 probe в минуту с одной точки). - **orchestrator на Moscow, не на VPN-узле** — изоляция управления от data-plane; падение orchestrator не роняет существующие подключения. - **Moscow исключена из VPN pool** — на :443 nginx (efir.org), :2053 — отдельный xray не для публики.