montana/Android/Внешний-аудит/09-Инвентаризация-кода.md
2026-05-18 22:11:45 +03:00

296 lines
12 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.

# 09. Инвентаризация кода — Montana Android v6.5.0
## Структура проекта
```
Montana/Android/MontanaApp/
├── app/
│ ├── build.gradle.kts (50 строк) — версии, deps, signing
│ ├── src/main/
│ │ ├── AndroidManifest.xml (50 строк) — permissions, service decl
│ │ ├── java/quest/montana/app/
│ │ │ ├── MainActivity.kt (90 строк) — WebView host
│ │ │ └── MontanaVpnService.kt (450 строк) — VPN service + heartbeat
│ │ ├── assets/
│ │ │ ├── app.html (~400 KB, основная масса = base64 images)
│ │ │ ├── bip39-en.txt (13 KB)
│ │ │ ├── geoip.dat, geosite.dat (xray data)
│ │ │ ├── symbol.jpg, juno.jpg, juno-back.jpg
│ │ ├── jniLibs/ (native xray + hev-socks5-tunnel)
│ │ └── res/
│ │ ├── values/strings.xml
│ │ ├── values/themes.xml
│ │ └── mipmap-*/ic_launcher.*
└── settings.gradle.kts
```
## Файл-за-файлом разбор
### MainActivity.kt
**Назначение:** Activity-точка входа. Создаёт WebView, грузит app.html, прокидывает JS bridge `MontanaBridge`.
**Ключевые элементы:**
```kotlin
class MainActivity : AppCompatActivity() {
companion object { @Volatile var instance: MainActivity? = null }
lateinit var web: WebView
private val PREP_VPN = 991
override fun onCreate(savedInstanceState: Bundle?) {
// создаёт WebView, JavaScriptEnabled, добавляет MontanaBridge,
// загружает app.html через loadDataWithBaseURL("https://montana.local/", ...)
// WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
}
override fun onDestroy() {
// tearDown WebView корректно: stopLoading, removeJavascriptInterface,
// detach view, web.destroy(), clear instance
}
fun requestVpnPermissionAndStart(address: String) { ... }
fun startVpnService(address: String) { ... }
fun stopVpnService() { ... }
}
class MontanaBridge(activity: MainActivity) {
@JavascriptInterface fun version(): String
@JavascriptInterface fun bip39(): String // с SHA-256 integrity check
@JavascriptInterface fun platform(): String = "android"
@JavascriptInterface fun isNative(): Boolean = true
@JavascriptInterface fun isVpnRunning(): Boolean
@JavascriptInterface fun getBalance(): Double
@JavascriptInterface fun getSeconds(): Double
@JavascriptInterface fun getStatus(): String
@JavascriptInterface fun getNode(): String
@JavascriptInterface fun isOnline(): Boolean
@JavascriptInterface fun startVPN(address: String): Boolean
@JavascriptInterface fun stopVPN(): Boolean
}
```
**Security-relevant методы:**
- `bip39()` — integrity check SHA-256 (см. `03-Криптография.md` §2)
- `startVPN(address)` — единственная точка инициации VPN, требует Android VPN permission
### MontanaVpnService.kt
**Назначение:** `VpnService` который поднимает TUN-интерфейс, запускает xray + hev-socks5-tunnel, шлёт heartbeats.
**Структура:**
```kotlin
class MontanaVpnService : VpnService() {
companion object {
// constants: TAG, NOTI_ID, CHAN_ID, ACTION_START/STOP, EXTRA_ADDRESS
// SOCKS_HOST = "127.0.0.1", SOCKS_PORT = 10808
// BACKEND_HOST = "montana.quest", BACKEND_PORT = 443
// @Volatile state: isRunning, lastBalance, lastSeconds, lastStatusText, connectedNode, viaMontana
}
private var tun: ParcelFileDescriptor? = null
private var core: CoreController? = null // xray-core
private var hbThread: Thread? = null
private var address: String = ""
private var netCallbackRegistered = false
private val connectivity by lazy { getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager }
private val defaultNetworkRequest by lazy { NetworkRequest.Builder()... }
private val defaultNetworkCallback = object : NetworkCallback() { ... }
override fun onCreate() { ensureNotiChannel(); Libv2ray.initCoreEnv(...); }
override fun onStartCommand(...) { startVpn() либо stopVpn() }
override fun onDestroy() { stopVpn() }
override fun onRevoke() { stopVpn() }
private fun startVpn() {
// 1. requestNetwork для setUnderlyingNetworks
// 2. VpnService.Builder с addDisallowedApplication(self)
// 3. builder.establish() → tunFd
// 4. libv2ray startLoop с xray config
// 5. hev-socks5-tunnel start
// 6. HeartbeatThread spawn
}
private fun stopVpn() { ... корректный teardown в reverse order ... }
private fun socksConnect(host: String, port: Int, timeoutMs: Int = 10000): Socket {
// SOCKS5 handshake к локальному xray:10808
// 1. TCP connect
// 2. Send 0x05 0x01 0x00 (no auth)
// 3. Receive 0x05 0x00
// 4. Send 0x05 0x01 0x00 0x03 <hostlen> <host> <portHi> <portLo>
// 5. Parse response, return connected Socket
}
private fun sendHeartbeat() {
// socksConnect montana.quest:443
// SSL wrap с ALPN pin http/1.1
// POST /api/vpn/heartbeat
// org.json.JSONObject parse response
// Update Companion state
}
private fun buildXrayConfig(): String { ... возвращает JSON ... }
private fun ensureNotiChannel() { ... }
private fun buildNotification(text: String): Notification { ... }
private fun startForegroundNoti(text: String) { ... }
private fun updateNoti(text: String) { mgr.notify(NOTI_ID, ...) }
}
```
**Security-relevant функции:**
- `startVpn()``addDisallowedApplication(packageName)` критично для разрыва TUN-петли
- `sendHeartbeat()` — через SOCKS5 в локальный xray, ALPN pin = http/1.1, JSON parse через org.json
- `socksConnect()` — ручной SOCKS5 handshake без библиотек (auditable)
### app.html (single-page)
Не приложу полностью (400 KB включая base64 images). Ключевые JS-функции:
```javascript
// Constants
const L_S='m.seed', L_A='m.addr';
const RATE = 12.04; // Ɉ → ₽ visual rate
const isNative = !!window.MontanaApp;
// State
let st = {addr: null, online: false, bal: 0, sec: 0};
let BIP39_WORDS = null;
// Screen navigation
function show(n) { ... }
// BIP39
async function loadBip39() { ... через MontanaApp.bip39() + check size 2048 }
async function entropyToMnemonic(entropy) { ... }
async function mnemonicToEntropy(words) { ... checksum validate }
async function mnemonicToSeed(mnemonic, passphrase) { ... PBKDF2-HMAC-SHA512 }
async function deriveAddr(words) { ... SHA-256("montana-v1:" || seed)[0:20] }
async function generateMnemonic() { ... 256-bit entropy }
// Wallet operations
async function createKey() { ... }
async function recover() { ... }
function showSeed() { ... render 24 numbered words }
function hideSeed() { ... }
function logoutAcc() { ... localStorage.removeItem ... }
// UI updates
function render() { ... bn, bf elements }
function setUI(on, msg) { ... .power class, lbl, coin .on }
function applyAuth() { ... login-btn label }
function onLoginTap() { show('auth') либо show('account') }
// Polling
function pollUI() { ... }
function pollSync() { ... }
function tick() {
// counter +0.001/sec + uptime hh:mm:ss
}
function startPoll() / startTick() { setInterval }
// Boot
(async() => {
let addr = localStorage.getItem(L_A);
if (addr) st.addr = addr;
applyAuth(); show('main');
startPoll(); startTick();
})();
// Auto-VPN если ещё не запущен
if (isNative) setTimeout(() => { if (!MontanaApp.isVpnRunning()) toggle() }, 700);
```
**Security-relevant:**
- `loadBip39()` через native bridge с integrity check
- `mnemonicToEntropy()` — единственное место validate BIP39 checksum
- `mnemonicToSeed()` — WebCrypto PBKDF2 (не custom)
- `deriveAddr()` — детерминирован, проверка checksum включена
### AndroidManifest.xml
```xml
<manifest ...>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<application ... usesCleartextTraffic="true">
<activity android:name=".MainActivity" exported="true" ... />
<service android:name=".MontanaVpnService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:foregroundServiceType="specialUse">
<intent-filter><action android:name="android.net.VpnService"/></intent-filter>
<property name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" value="vpn"/>
</service>
</application>
</manifest>
```
**Permissions justification:**
- INTERNET — heartbeat + xray outbound
- ACCESS/CHANGE_NETWORK_STATE — `setUnderlyingNetworks` для cellular/wifi switching
- FOREGROUND_SERVICE_SPECIAL_USE — Android 14+ требует для VPN
- POST_NOTIFICATIONS — Android 13+ требует для foreground noti
- WAKE_LOCK — keep VPN active при screen off
- RECEIVE_BOOT_COMPLETED — `usesCleartextTraffic` — нужно для WebView local content (synthetic `https://montana.local/`)
**Не используются:**
- ACCESS_FINE_LOCATION — не нужно
- READ_EXTERNAL_STORAGE — не пишем во внешнее хранилище
- READ_PHONE_STATE — не получаем IMEI / device IDs
### app/build.gradle.kts
Минимальный, см. `08-Воспроизводимая-сборка.md` §2. Версионирование вручную:
```kotlin
defaultConfig {
applicationId = "quest.montana.vpn"
minSdk = 24
targetSdk = 34
versionCode = 60500
versionName = "6.5.0"
buildConfigField("String", "APP_URL", "\"https://montana.quest/vpn/app/\"")
ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64") }
}
```
## Серверная часть (отдельно от Android)
### /opt/montana-vpn-balance/app.py (Moscow)
~280 строк Flask. Endpoints:
- `GET /api/vpn/balance?address=X` — баланс read-only
- `POST /api/vpn/heartbeat` — основной endpoint начисления
- `GET /api/vpn/check` — диагностика IP/via_montana
- `GET /api/vpn/stats` — агрегаты
- `GET /vpn/sub` — universal VLESS link (base64)
- `POST /api/vpn/admin/purge` — localhost-only cleanup
### Конфиги Helsinki
- `/etc/haproxy/haproxy.cfg` — see `04-Сетевой-слой.md` §3
- `/etc/xray-pinned/{fi,fra,us}.json` — 3 backend xray-pinned
- `/etc/systemd/system/xray-pinned@.service` — systemd template
- `/etc/systemd/system/haproxy.service` — systemd unit
### Конфиги Frankfurt, NewYork
- `/usr/local/etc/xray/config.json` — single inbound, cascade-only receiver
- `/etc/systemd/system/xray.service` — systemd unit
## Точки риска при code review
1. **`MontanaVpnService.startVpn()`** — пропуск `addDisallowedApplication` = TUN петля.
2. **`socksConnect()`** — ручной SOCKS5 protocol, проверить byte-exact handshake.
3. **`sendHeartbeat()`** — ALPN pin критичен для HTTP/1.1 parser.
4. **`deriveAddr()` в app.html** — `domain = "montana-v1:"` префикс должен оставаться immutable, иначе все адреса другие.
5. **`bip39()` в MainActivity** — fail-closed при integrity mismatch.
6. **Helsinki haproxy `stick on src`** — критично для sticky pin.
7. **`with_state_lock()` в backend** — атомарность read-modify-write.