# Монтана — Android-приложение **Версия:** `3.0.0` (versionCode `30000`) **Дата релиза:** 2026-05-16 **Package ID:** `quest.montana.vpn` **Подпись:** Genesis-keystore (`/Users/kh./Python/Ничто/Montana/Android/keystore/montana.keystore`, fingerprint `305bc99b…3ce4d`, SHA384withRSA 4096-bit, действителен до 2126) **Min SDK:** 24 (Android 7.0) **Target SDK:** 34 (Android 14) **Compile SDK:** 34 --- ## Что это Тонкое Android-приложение Монтана: при запуске пользователь сразу видит **кошелёк** и **центральную кнопку ВПН**. Никаких лишних табов, настроек, серверов вручную. Профиль предустановлен — выбирать ничего не нужно. Пока ВПН включён — на кошельке посекундно тикают монеты `Ɉ` (юна́), всё работает через бэкэнд `montana.quest/api/vpn/`. Под капотом это **WebView-обёртка** над страницей `https://montana.quest/vpn/app/` плюс минимальный JS-bridge `MontanaApp` для запуска VPN через intent в установленный VPN-клиент (V2RayTun / V2rayNG / Hiddify). Сам VPN-движок внутри APK **не встроен** в версии 3.0.0 — это сознательное решение, см. секцию «Архитектурное решение» ниже. --- ## Архитектура ``` ┌─────────────────────────────────────────────┐ │ Pixel 9 Pro XL (Android 15) │ │ ┌────────────────────────────────────────┐ │ │ │ Монтана 3.0.0 (quest.montana.vpn) │ │ │ │ ┌──────────────────────────────────┐ │ │ │ │ │ MainActivity → WebView fullscreen│ │ │ │ │ │ грузит montana.quest/vpn/app/ │ │ │ │ │ │ │ │ │ │ │ │ JS-bridge MontanaApp: │ │ │ │ │ │ version() → "3.0.0" │ │ │ │ │ │ platform() → "android" │ │ │ │ │ │ connectVPN() → Intent │ │ │ │ │ │ openVPNApp() → V2RayTun launch │ │ │ │ │ └──────────────────────────────────┘ │ │ │ └────────────────────────────────────────┘ │ │ ↓ intent │ │ ┌────────────────────────────────────────┐ │ │ │ V2RayTun (com.v2raytun.android) │ │ │ │ принимает sub URL → подключает ВПН │ │ │ └────────────────────────────────────────┘ │ └─────────────────────────────────────────────┘ ↓ трафик через Reality ┌─────────────────────────────────────────────┐ │ Helsinki (91.132.142.42) — front │ │ xray-core balancer → US / Frankfurt │ └─────────────────────────────────────────────┘ ↓ выход в интернет ┌─────────────────────────────────────────────┐ │ Frankfurt (89.19.208.158) / US (86.104.…) │ └─────────────────────────────────────────────┘ ↑ heartbeat с этого IP ┌─────────────────────────────────────────────┐ │ Moscow (176.124.208.93) │ │ montana-vpn-balance.service на :5008 │ │ POST /api/vpn/heartbeat │ │ body: {address} │ │ ip-whitelist античит: только узлы Монтаны│ │ каждая секунда онлайн = 0.001 Ɉ │ │ GET /api/vpn/balance?address=… │ │ GET /api/vpn/check (свой IP + узел) │ │ GET /api/vpn/stats │ └─────────────────────────────────────────────┘ ``` --- ## Экраны (внутри WebView) Все экраны на одной странице `montana.quest/vpn/app/index.html` (single-page, vanilla JS). Состояние кошелька — в `localStorage`. 1. **Welcome** (если нет кошелька) — большая Юнона + 2 кнопки: «Создать кошелёк» / «Восстановить из 24 слов». 2. **Create** — генерация BIP39 24 слов через `crypto.getRandomValues(32 байта)` → SHA-256 checksum → 24 индекса по 11 бит. Показ полным списком, кнопки «Копировать» / «Сохранил, войти». 3. **Recover** — `textarea` для ввода 24 слов, проверка через тот же BIP39 алгоритм (валидация checksum обязательна). 4. **Main** — Юнона + баланс `XXX Ɉ` + `≈ XXX₽` + центральная круглая кнопка «Включить» / «Включено» + статус «в сети · чеканка идёт» + таймер uptime + адрес кошелька внизу. Адрес кошелька: `SHA-256("montana:" + 24-слов)[:20]` → 40 hex-символов. --- ## Чеканка монет Каждые 5 секунд WebView пингует `POST /api/vpn/heartbeat`: ```json {"address":"<40hex>"} ``` Бэкэнд (`/opt/montana-vpn-balance/app.py` на Moscow:5008) проверяет: - IP запроса должен быть в whitelist узлов Монтаны: `91.132.142.42`, `89.19.208.158`, `86.104.72.12`. - Если IP **не** в whitelist → `403 not_via_montana_vpn` → клиент видит «ВПН выключен». - Если в whitelist → начислить `(now - last_hb)` секунд (capped 30 сек), баланс += секунды × `0.001 Ɉ/сек`. Через 4 минуты онлайн → ~+0.24 Ɉ; за час → ~3.6 Ɉ ≈ 43₽. Античит держится на том, что **только реальный трафик через узел Монтаны** даёт IP-источник запроса равный IP узла Монтаны. Прокинуть фейковый heartbeat снаружи невозможно: запрос придёт с другого IP и будет отклонён. --- ## JavaScript bridge `MontanaApp` Доступен внутри WebView как `window.MontanaApp`. Используется фронтендом для интеграции с native-частью: | Метод | Возвращает | Описание | |---|---|---| | `version()` | `String` | `"3.0.0"` — показывается в правом верхнем углу | | `platform()` | `String` | `"android"` | | `connectVPN()` | `Boolean` | Открывает intent `v2raytun://import/` — V2RayTun автоматически добавит подписку Монтаны и подключится | | `openVPNApp()` | `Boolean` | Просто запустить V2RayTun (если уже настроен) | Если bridge есть (`!!window.MontanaApp` в JS) — кнопка ВПН вызывает `connectVPN()`. Если нет (открыта страница в обычном браузере) — fallback через `incy://` (iOS) или `intent://...v2rayng...` (Android Chrome). --- ## Native-часть (Kotlin) - `quest.montana.app.MainActivity` — содержит `WebView`, грузит `BuildConfig.APP_URL = "https://montana.quest/vpn/app/"`. JS включён, DOM storage включён, кеш стандартный. `WebViewClient.shouldOverrideUrlLoading` — пускает только `montana.quest`, остальные ссылки идут в системные intent. - `MontanaBridge` — `@JavascriptInterface` методы, регистрируются как `window.MontanaApp`. - В `AndroidManifest.xml` объявлены `` для `com.v2raytun.android`, `com.v2ray.ang`, `app.hiddify.com` — чтобы Android 11+ разрешал диспетчеризацию intent в эти приложения. Зависимости (`app/build.gradle.kts`): - `androidx.core:core-ktx:1.13.1` - `androidx.appcompat:appcompat:1.7.0` - `androidx.webkit:webkit:1.11.0` Никаких VPN-движков, тяжёлых SDK, аналитики — только WebView. --- ## Архитектурное решение: «зачем нужен внешний VPN-клиент» Встроить xray-core в APK теоретически возможно — есть Go-библиотеки `AndroidLibXrayLite` и `sing-box-mobile`. Это потребовало бы: 1. Установить Go-toolchain для Android (gomobile, NDK). 2. Скомпилировать xray-core в `.aar` для arm64-v8a + armeabi-v7a + x86_64. 3. Написать `class V2RayVpnService : android.net.VpnService` с tun2socks (`hev-socks5-tunnel.so`). 4. Управление foreground service (`FOREGROUND_SERVICE_SPECIAL_USE` + `PROPERTY_SPECIAL_USE_FGS_SUBTYPE` для Android 14+). 5. Embedded universal VLESS-профиль (зашит в `assets/`). Это **2–3 дня работы** + полный цикл тестирования на устройстве, плюс APK раздувается до ~30 МБ. В версии 3.0.0 выбран другой путь: **JS-bridge → intent в V2RayTun**. Минусы: - Пользователь должен установить V2RayTun (бесплатный, из Google Play / RuStore). - Два приложения вместо одного. Плюсы: - APK маленький (~3 МБ против ~30). - VPN-стек проверен и стабилен (V2RayTun — флагман). - Обновлять можно независимо. - Reality / VLESS-Vision / любые новые транспорты работают сразу. **Roadmap:** в 4.0.0 рассмотреть встроенный VPN-движок на базе `sing-box-mobile` (он официально поддерживает Reality и компактнее xray). Это потребует пересмотра minSdk и foreground service permissions. --- ## Сборка ```bash cd /Users/kh./Python/Ничто/Montana/Android/MontanaApp export JAVA_HOME=/opt/homebrew/Cellar/openjdk@17/17.0.19/libexec/openjdk.jdk/Contents/Home export PATH=$JAVA_HOME/bin:$PATH export ANDROID_HOME=/opt/homebrew/share/android-commandlinetools # Debug: ./gradlew assembleDebug # → app/build/outputs/apk/debug/app-debug.apk # Release (подписан Genesis): ./gradlew assembleRelease # → app/build/outputs/apk/release/app-release.apk ``` **Maven-зеркало:** в `settings.gradle.kts` указаны Aliyun mirror'ы (`maven.aliyun.com/repository/google` + `/public`) перед official Google. Это обход TLS-handshake проблем при сборке через VPN. --- ## Деплой Подписанный release-APK ложится на montana.quest по пути `/var/www/montana_quest/vpn/montana.apk`. Версия видна: 1. На сайте — в инструкции по установке для Android. 2. В правом верхнем углу WebView внутри приложения (`v3.0.0`). 3. В Android Settings → Apps → Монтана → version `3.0.0`. Чтобы установить новую версию поверх старой — подпись должна совпадать (Genesis keystore). Иначе Android требует «Удалить старое». --- ## Установка на устройство (dev) ```bash adb mdns services # найти текущий IP:port WiFi-ADB adb connect : adb install -r app/build/outputs/apk/debug/app-debug.apk adb shell monkey -p quest.montana.vpn 1 # запустить adb logcat | grep -i 'MontanaApp\|WebView' # отладка ``` Тестовое устройство и pairing-ключи: [reference_pixel9_adb.md](../../../..../.claude/projects/-Users-kh--Python------/memory/reference_pixel9_adb.md). --- ## Текущее состояние (v3.0.0) | Компонент | Статус | |---|---| | WebView + JS-bridge | ✅ написан | | Страница `montana.quest/vpn/app/` | ✅ задеплоена | | Бэкэнд `/api/vpn/` (balance/heartbeat/check/stats) | ✅ работает | | Античит IP-whitelist | ✅ проверен через curl с frankfurt | | BIP39 кошелёк (создание + восстановление) | ✅ работает | | Чеканка `0.001 Ɉ/сек` пока VPN включён | ✅ проверена end-to-end | | Минималистичный UI (юнона + баланс + 1 кнопка) | ✅ | | Шрифт Inter для `Ɉ` и `₽` (фикс «квадрата» в Chrome) | ✅ через Google Fonts | | Сборка release-APK | ⏳ в работе | | Подпись Genesis | ⏳ | | Загрузка на montana.quest | ⏳ | --- ## Связанные ссылки - Backend balance API: [/opt/montana-vpn-balance/app.py](http://montana.quest/api/vpn/stats) (на Moscow:5008) - Реальная страница UI: - Сайт landing: - Public sub: - Тестовое устройство: Pixel 9 Pro XL — `adb-54211FDAS0009S-L1ohem`