230 lines
14 KiB
Markdown
230 lines
14 KiB
Markdown
# Монтана — 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/<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/`).
|
||
|
||
Это **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 <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](../../../..../.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: <https://montana.quest/vpn/app/>
|
||
- Сайт landing: <https://montana.quest/vpn/>
|
||
- Public sub: <https://montana.quest/vpn/sub>
|
||
- Тестовое устройство: Pixel 9 Pro XL — `adb-54211FDAS0009S-L1ohem`
|