montana/Android/Внешний-аудит/01-Архитектура.md
2026-05-18 22:11:45 +03:00

265 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.