montana/Android/MontanaApp/SPEC.md
2026-05-18 18:05:32 +03:00

14 KiB
Raw Blame History

Монтана — 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. Recovertextarea для ввода 24 слов, проверка через тот же BIP39 алгоритм (валидация checksum обязательна).
  4. 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.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/).

Это 23 дня работы + полный цикл тестирования на устройстве, плюс 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. Версия видна:

  1. На сайте — в инструкции по установке для Android.
  2. В правом верхнем углу WebView внутри приложения (v3.0.0).
  3. В 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

Связанные ссылки