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

17 KiB
Raw Blame History

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.