montana/Android/MontanaApp/SPEC.md

230 lines
14 KiB
Markdown
Raw Normal View History

2026-05-18 18:05:32 +03:00
# Монтана — 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/`).
Это **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.
---
## Сборка
```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`