14 KiB
Монтана — 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.
- Welcome (если нет кошелька) — большая Юнона + 2 кнопки: «Создать кошелёк» / «Восстановить из 24 слов».
- Create — генерация BIP39 24 слов через
crypto.getRandomValues(32 байта)→ SHA-256 checksum → 24 индекса по 11 бит. Показ полным списком, кнопки «Копировать» / «Сохранил, войти». - Recover —
textareaдля ввода 24 слов, проверка через тот же BIP39 алгоритм (валидация checksum обязательна). - Main — Юнона + баланс
XXX Ɉ+≈ XXX₽+ центральная круглая кнопка «Включить» / «Включено» + статус «в сети · чеканка идёт» + таймер uptime + адрес кошелька внизу.
Адрес кошелька: SHA-256("montana:" + 24-слов)[:20] → 40 hex-символов.
Чеканка монет
Каждые 5 секунд WebView пингует POST /api/vpn/heartbeat:
{"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/<sub_url> — 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объявлены<queries>дляcom.v2raytun.android,com.v2ray.ang,app.hiddify.com— чтобы Android 11+ разрешал диспетчеризацию intent в эти приложения.
Зависимости (app/build.gradle.kts):
androidx.core:core-ktx:1.13.1androidx.appcompat:appcompat:1.7.0androidx.webkit:webkit:1.11.0
Никаких VPN-движков, тяжёлых SDK, аналитики — только WebView.
Архитектурное решение: «зачем нужен внешний VPN-клиент»
Встроить xray-core в APK теоретически возможно — есть Go-библиотеки AndroidLibXrayLite и sing-box-mobile. Это потребовало бы:
- Установить Go-toolchain для Android (gomobile, NDK).
- Скомпилировать xray-core в
.aarдля arm64-v8a + armeabi-v7a + x86_64. - Написать
class V2RayVpnService : android.net.VpnServiceс tun2socks (hev-socks5-tunnel.so). - Управление foreground service (
FOREGROUND_SERVICE_SPECIAL_USE+PROPERTY_SPECIAL_USE_FGS_SUBTYPEдля Android 14+). - 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.
Сборка
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. Версия видна:
- На сайте — в инструкции по установке для Android.
- В правом верхнем углу WebView внутри приложения (
v3.0.0). - В Android Settings → Apps → Монтана → version
3.0.0.
Чтобы установить новую версию поверх старой — подпись должна совпадать (Genesis keystore). Иначе Android требует «Удалить старое».
Установка на устройство (dev)
adb mdns services # найти текущий IP:port WiFi-ADB
adb connect <IP>:<port>
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.
Текущее состояние (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 (на Moscow:5008)
- Реальная страница UI: https://montana.quest/vpn/app/
- Сайт landing: https://montana.quest/vpn/
- Public sub: https://montana.quest/vpn/sub
- Тестовое устройство: Pixel 9 Pro XL —
adb-54211FDAS0009S-L1ohem