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

230 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Монтана — 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`