# 01. Архитектура — Montana Android v6.5.0 ## Высокоуровневая схема ``` ┌─────────────────────────────────────────────────────────────────────┐ │ Android-устройство │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ quest.montana.vpn (APK 32 MB) │ │ │ │ ┌──────────────────────┐ ┌────────────────────────────┐ │ │ │ │ │ MainActivity │◀▶│ WebView │ │ │ │ │ │ (Kotlin) │ │ ↳ app.html + JS │ │ │ │ │ │ - JS bridge │ │ ↳ BIP39 derivation │ │ │ │ │ │ - VPN permission │ │ ↳ localStorage (seed) │ │ │ │ │ └──────────────────────┘ └────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ │ │ MontanaVpnService (foreground service) │ │ │ │ │ │ - tun0 интерфейс (172.19.0.1/30, default route) │ │ │ │ │ │ - xray-core (vless+reality outbound) │ │ │ │ │ │ - hev-socks5-tunnel (TUN ↔ SOCKS5) │ │ │ │ │ │ - HeartbeatThread (раз в 5 сек, через xray SOCKS5) │ │ │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ default route → tun0 │ heartbeat → SOCKS5 → Reality ▼ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ Helsinki (91.132.142.42:443) — публичный вход VPN │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ haproxy (TCP L4) │ │ │ │ ↳ stick-table type ip size 200k expire 24h │ │ │ │ ↳ balance leastconn │ │ │ │ ↳ распределяет на один из 3 backend │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ :40443 (fi) :40444 (fra) :40445 (us) │ │ xray-pinned-fi xray-pinned-fra xray-pinned-us │ │ ↓ freedom ↓ cascade ↓ cascade │ │ internet via via Reality via Reality │ │ 91.132.142.42 →89.19.208.158 →86.104.72.12 │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ user traffic exits │ heartbeat exits ▼ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ Moscow (176.124.208.93) — координатор балансов (НЕ exit, НЕ VPN) │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ nginx (443/8443) │ │ │ │ ↳ /api/vpn/heartbeat → :5008 │ │ │ │ ↳ /vpn/sub → :5008 │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ montana-vpn-balance.service (Flask + gunicorn) │ │ │ │ ↳ /var/lib/montana-vpn-balance/balances.json │ │ │ │ ↳ atomic LOCK_EX через balances.json.lock │ │ │ │ ↳ purge inactive >30 days via /api/vpn/admin/purge │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ montana-node.service (TimeChain validator) │ │ │ │ ↳ ПОКА не интегрирован с балансами VPN (см. 07) │ │ │ └──────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ ``` --- ## Компоненты Android-приложения ### MainActivity.kt Точка входа. Создаёт `WebView`, загружает `app.html` через `loadDataWithBaseURL("https://montana.local/", ...)` (синтетический base URL, наружу запросы не идут). Связь с native: - `MontanaApp.bip39()` — выдаёт wordlist BIP39 EN (с SHA-256 integrity check, см. `03-Криптография.md` §2) - `MontanaApp.startVPN(address)` — запрашивает разрешение VPN, стартует `MontanaVpnService` - `MontanaApp.stopVPN()` — останавливает сервис - `MontanaApp.isVpnRunning()` / `getBalance()` / `getSeconds()` / `getNode()` / `getStatus()` — read-only state WebView-debug включён только в DEBUG-сборках через `BuildConfig.DEBUG`. ### MontanaVpnService.kt (foreground service) Реализует Android `VpnService`. При старте: 1. **Запрашивает default network** через `ConnectivityManager.requestNetwork()` — для `setUnderlyingNetworks(...)` при смене WiFi/cellular. 2. **Строит `VpnService.Builder`:** - MTU 1500 - Адрес `172.19.0.1/30` - DNS `1.1.1.1`, `8.8.8.8` - Default route `0.0.0.0/0` - **`addDisallowedApplication(packageName)`** — критично: само приложение исключается из своего же VPN (иначе TUN-петля, см. `07` F-петля) 3. **Поднимает libv2ray (xray-core)** с конфигом vless+reality к `cdn.montana.quest:443`. 4. **Запускает `HevSocks5TunnelService`** для TUN ↔ SOCKS5 (127.0.0.1:10808). 5. **HeartbeatThread** раз в 5 сек: SOCKS5 → SSL (ALPN=`http/1.1`) → POST `/api/vpn/heartbeat` на montana.quest. Heartbeat идёт **через** Reality cascade — backend видит exit-IP как источник. ### app.html (WebView UI + кошелёк) Single-page приложение. Три экрана: `s-main`, `s-auth`, `s-recover`, `s-account`. Логика кошелька — целиком JS: - `generateMnemonic()` — 256 бит энтропии через `crypto.getRandomValues`, BIP39 24 слова с checksum - `mnemonicToEntropy(words)` — обратная операция + checksum validation - `mnemonicToSeed(mnemonic, "")` — PBKDF2-HMAC-SHA512, 2048 итераций, 512 бит вывод - `deriveAddr(words)` — `SHA-256("montana-v1:" || seed)[0..20]` Подробности — `03-Криптография.md` §3. --- ## Компоненты серверной части ### Helsinki — публичный вход VPN **haproxy** на `0.0.0.0:443` (TCP mode): - `balance leastconn` — новый клиент попадает на наименее загруженный backend - `stick-table type ip size 200k expire 24h, stick on src` — закрепление source-IP за backend на 24 часа Три локальных xray-инстанса: - `xray-pinned-fi` на `127.0.0.1:40443` — outbound `freedom`, exit с Helsinki - `xray-pinned-fra` на `127.0.0.1:40444` — outbound vless+reality cascade к `89.19.208.158:443` (Frankfurt) - `xray-pinned-us` на `127.0.0.1:40445` — outbound vless+reality cascade к `86.104.72.12:443` (NewYork) Все три inbound используют **identical Reality params** (privateKey, sni, shortId) → клиент не различает на стороне приложения. ### Moscow — координатор балансов **Roles:** API REST для heartbeat и subscription. **Не выпускает VPN-трафик**, **не является exit-нодой**. `montana-vpn-balance.service` (Flask + gunicorn 2 workers × 4 threads): - `POST /api/vpn/heartbeat` — приём heartbeat, начисление 0.001 Ɉ/сек - `GET /api/vpn/balance` — read-only баланс - `GET /vpn/sub` — универсальный VLESS URL (base64) - `POST /api/vpn/admin/purge` — localhost-only cleanup inactive State: `/var/lib/montana-vpn-balance/balances.json` (JSON-файл). ### Frankfurt, NewYork — exit-узлы каскада Только принимают cascade traffic из Helsinki через Reality:443, выполняют freedom outbound в открытый интернет. **Не общаются** с Android-клиентом напрямую. --- ## Потоки данных ### Поток 1: Стандартный VPN-трафик ``` Browser/любое приложение Android │ ▼ Default route (Android system) │ ▼ (не если приложение исключено через addDisallowedApplication) tun0 (172.19.0.1) │ ▼ hev-socks5-tunnel │ ▼ TCP/UDP → SOCKS5 127.0.0.1:10808 (xray inbound на устройстве) │ ▼ vless+reality cdn.montana.quest:443 (= 91.132.142.42) │ ▼ Helsinki haproxy (stick on src) │ ▼ xray-pinned-{fi|fra|us} │ ▼ (для fra/us — vless+reality cascade) Exit IP открытый интернет ``` ### Поток 2: Heartbeat кошелька ``` HeartbeatThread (Kotlin) │ ▼ socksConnect через 127.0.0.1:10808 │ ▼ Тот же канал что Поток 1 Helsinki → backend exit → internet │ ▼ POST montana.quest:443/api/vpn/heartbeat nginx (Moscow:443) │ ▼ proxy_pass gunicorn (127.0.0.1:5008) │ ▼ atomic LOCK_EX read-modify-write balances.json │ ▼ HTTP/1.1 200 application/json {"balance": ..., "node": "newyork", "node_ip": "86.104.72.12", "via_montana": true} │ ▼ org.json.JSONObject (Kotlin) MontanaVpnService.lastBalance / lastStatusText / connectedNode │ ▼ getBalance() / getStatus() bridge WebView UI ``` ### Поток 3: Создание кошелька ``` User: tap "Войти в Монтану" → "Создать ключ" │ ▼ JS createKey() crypto.getRandomValues(Uint8Array(32)) ← 256 бит энтропии │ ▼ entropyToMnemonic(entropy) │ ↳ SHA-256(entropy) → checksum bits (первые 8 бит) │ ↳ 264 бита / 11 = 24 слова │ ↳ Каждое 11-битное число → index в BIP39 EN wordlist │ (wordlist получен через MontanaApp.bip39() с SHA-256 verify) │ ▼ 24 слова deriveAddr(words) │ ↳ mnemonicToEntropy(words) — validate checksum │ ↳ PBKDF2-HMAC-SHA512(words, salt="mnemonic", iter=2048) → 64B seed │ ↳ SHA-256("montana-v1:" || seed) → 32B hash │ ↳ first 20 bytes → hex → 40-символьный address │ ▼ (seed_words, address) localStorage["m.seed"] = words.join(" ") localStorage["m.addr"] = address ← внимание: оба в plain text! ``` --- ## Границы доверия | Граница | Что доверяем | Что не доверяем | |---------|--------------|------------------| | Android OS | Изоляция приложений (data dir 0700), Permission model, KeyStore (не используется!) | Root-доступ, malicious apps с MANAGE_EXTERNAL_STORAGE, forensic image | | WebView Chromium | SubtleCrypto детерминизм (SHA-256, PBKDF2, NFKD) | Гарантии константного времени, защиту от подмены кода через debug | | haproxy stick-table | Sticky-pin по source-IP до 24h | Защиту от смены клиентом IP (mobile network → IP меняется) | | Reality TLS | Active probing resistance, fingerprint disguise | DPI который умеет анализировать SNI Echo, machine learning по timing | | Moscow backend | Атомарный учёт balances.json | Подпись heartbeat (её сейчас нет — см. `07` F-2) | | Cascade exit | Изоляция трафика разных пользователей | Логи трафика на exit-нодах (нет policy сейчас) | --- ## Что приложение делает 1. Создаёт / восстанавливает BIP39-кошелёк (24 слова английский wordlist). 2. Запускает VPN через системный `VpnService` API. 3. Поднимает локальный xray (libv2ray), пересылает весь TCP/UDP трафик устройства через Reality на Helsinki. 4. Шлёт heartbeat backend для учёта времени онлайн → начисление 0.001 Ɉ/сек. 5. Показывает баланс, статус, узел подключения, таймер сессии. ## Что приложение НЕ делает 1. Не интегрировано с консенсусом TimeChain (балансы у Moscow в JSON-файле, не у валидаторов). 2. Не подписывает heartbeat криптографически (любой может слать heartbeat от чужого имени с правильного exit-IP). 3. Не шифрует seed в localStorage (plain text 24 слова). 4. Не использует Android KeyStore. 5. Не требует backup confirmation при создании кошелька. 6. Не имеет proper logout flow с предупреждением о необратимой потере. Все эти ограничения — в `07-Известные-ограничения.md` с severity и closure paths.