12 KiB
12 KiB
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.
Ключевые элементы:
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.
Структура:
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.jsonsocksConnect()— ручной SOCKS5 handshake без библиотек (auditable)
app.html (single-page)
Не приложу полностью (400 KB включая base64 images). Ключевые JS-функции:
// 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 checkmnemonicToEntropy()— единственное место validate BIP39 checksummnemonicToSeed()— WebCrypto PBKDF2 (не custom)deriveAddr()— детерминирован, проверка checksum включена
AndroidManifest.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 (synthetichttps://montana.local/)
Не используются:
- ACCESS_FINE_LOCATION — не нужно
- READ_EXTERNAL_STORAGE — не пишем во внешнее хранилище
- READ_PHONE_STATE — не получаем IMEI / device IDs
app/build.gradle.kts
Минимальный, см. 08-Воспроизводимая-сборка.md §2. Версионирование вручную:
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-onlyPOST /api/vpn/heartbeat— основной endpoint начисленияGET /api/vpn/check— диагностика IP/via_montanaGET /api/vpn/stats— агрегатыGET /vpn/sub— universal VLESS link (base64)POST /api/vpn/admin/purge— localhost-only cleanup
Конфиги Helsinki
/etc/haproxy/haproxy.cfg— see04-Сетевой-слой.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
MontanaVpnService.startVpn()— пропускaddDisallowedApplication= TUN петля.socksConnect()— ручной SOCKS5 protocol, проверить byte-exact handshake.sendHeartbeat()— ALPN pin критичен для HTTP/1.1 parser.deriveAddr()в app.html —domain = "montana-v1:"префикс должен оставаться immutable, иначе все адреса другие.bip39()в MainActivity — fail-closed при integrity mismatch.- Helsinki haproxy
stick on src— критично для sticky pin. with_state_lock()в backend — атомарность read-modify-write.