y83 code / инструмент

VPN

VPN-клиент на Hysteria2 (QUIC) со своим TUN: трафик устройства идёт либо весь через туннель, либо по приложениям. Это не обёртка над системным VPN — здесь собственный сервис с бинарём hysteria2 и JNI-мостом tun2socks.

раздел реестра
Безопасность и сеть
код
3 371 строк в 12 файлах
документ
23-vpn.md
Раздел: Security & Network
Идентификатор: vpn · Экран: ui/vpn/ (9 файлов, ~2500 строк) + vpn/ (10 файлов)
Снимок: 2026-10-15

1. Зачем и что это

Ключевая инженерная особенность — надёжность: туннель не «отваливается молча», а переподключается сам по заданной политике, а backend (runner) ходит мимо туннеля через прямой SOCKS5-прокси.

Подробный разбор надёжности и граблей — docs/tech/vpn-reliability.md.

2. Как попасть

Хаб инструментов → секция Security & Network → карточка «VPN» (hub_vpn_title, подпись «Трафик устройства через Hysteria2 — глобально или по приложению», тайл — «VPN»).

Откуда ещёПуть
Мостapp_navigate с screen: "vpn"

Маршрут: MainRouter.kt:326 (VpnScreen).

3. Экран: элементы и действия

СекцияЭлементЧто делает
ШапкаТерминальная шапка VPN со статусом и суффиксомvpnStatusColor, terminalMetaSuffixColor
Карточка статусаVpnStatusCardСостояние, попытки, причина реконнекта, кнопка Retry
HealthLineСтрока здоровьяРезультат проверки туннеля
// serverServerSectionСписок серверов, выбор, шторка
// modeModeSectionALL (весь трафик) или PER_APP (по приложениям)
// optionsOptionsSectionАвтостарт, сервер по умолчанию, выбор приложений
ConnectBarНижняя панельПодключиться / отключиться
Экран серверовVpnServerListScreenСписок, дефолтный, удаление
Экран добавленияVpnAddServerScreenРучной ввод или вставка ссылки
Экран правкиVpnEditServerScreenИзменение параметров сервера
Экран выбора приложенийVpnAppSelectionScreenПоиск, чипы «Все / Выбранные» (SelectChip), секция // selected, меню списка (системные приложения, порядок, «снять все»), лоадер первой загрузки; выбор применяется на живом туннеле

Экран выбора приложений держит общий вертикальный ритм приложения: поиск отстоит от терминальной шапки на 12dp, верх первого заголовка — FirstSectionTopPadding, шаг секций — SectionHeaderContentSpacing + sectionHeaderSlot.

4. Возможности подробно

Схема туннеля

приложения → TUN (10.0.88.88/16) → tun2socks (JNI) → SOCKS5 127.0.0.1:1080
                                                        → hysteria2 → сервер:443 (QUIC)
терминал (RemoteServer) → SOCKS5 127.0.0.1:1080 (тот же SOCKS5 hysteria2, внутрь туннеля)

Сервис живёт в отдельном процессе :vpn (Y83VpnService). Состояние передаётся broadcast com.y83.ws.vpn.STATE_CHANGED (connected / reconnecting / disconnected / error), статистика — раз в 3 секунды.

Политика переподключения

VpnReconnectPolicy: попытки 1 / 2 / 4 / 8 / 16 секунд, затем медленный режим раз в 60 секунд. Сервис сам по себе не останавливается. Сброс счётчика — успех, ручной vpn_retry или validated-смена сети. FATAL (нет бинаря или сервера) — единственный случай, когда ставится error и сервис останавливается.

Мягкий реконнект перезапускает только процесс hysteria2: TUN и tun2socks остаются поднятыми, поэтому утечки трафика нет и соединения не рвутся повторно. Жёсткий пересбор TUN происходит после трёх неудач подряд.

Готовность туннеля

После старта hysteria2 приложение ждёт не просто запуска процесса, а готовности туннеля: TunnelProbe проверяет SOCKS5-порт и делает реальный HTTP-запрос через туннель (до 8 секунд). Только после этого ставится connected. Это исправляет старую проблему шторма рестартов, когда connected выставлялся сразу после exec().

Реакция на сеть

VpnNetworkMonitor ловит validated-события с дебаунсом 1.5 секунды, затем пробует туннель: жив — ничего не делаем (WiFi-флап без обрыва), мёртв — мягкий реконнект. Если validated-сети нет — состояние Reconnecting(reason=no_network), попыты не тратятся.

Перед запуском hysteria2 приложение ждёт validated-сеть и резолв хоста: пакет исключён из VPN, поэтому резолв идёт по обычной сети (убирает «no such host»).

Режимы работы

VpnMode:

  • ALL — весь трафик устройства через VPN (по умолчанию);
  • PER_APP — только выбранные приложения, список хранится в prefs (vpn_selected_apps).

Режим пишется в vpn_mode (PREF_VPN_MODE) и читается менеджером при старте.

Смена режима и набора приложений применяется на живом туннеле: VpnManager после записи префов шлёт в сервис ACTION_APPLY_POLICY с дебаунсом 600 мс, сервис глушит tun2socks, пересоздаёт TUN с новым набором (establishTun) и поднимает tun2socks на новом fd — hysteria2 и время сессии не трогаются. Пустой набор в PER_APP не применяется: экран не пускает в подключение без выбранных приложений, а живой туннель остаётся на прежнем наборе и показывает предупреждение.

О факте применения сервис сообщает broadcast'ом com.y83.ws.vpn.POLICY_APPLIED (is_per_app, app_count): главный экран показывает плашку «Режим применён: …», а на время пересбора TUN в статус-карточке горит строка «применяются настройки…» (VpnManager.policyPending). Плашка не оптимистичная — она появляется по подтверждению, поэтому не врёт, если пересбор TUN не удался (тогда работает штатный реконнект с причиной policy_apply_failed).

Выбор приложений (per-app)

Экран VpnAppSelectionScreen держит три вещи, за которые отвечает чистая логика VpnAppSelection.kt (проверяется JVM-тестами):

  • Фильтр и группы — filterInstalledApps (имя или пакет), userInstalledApps / systemInstalledApps, selectedInstalledApps для секции // selected, unselectedInstalledApps — строки групп без выбранного.
  • Область списка — visibleInstalledApps: системные приложения по умолчанию скрыты (тумблер «Показывать системные» в меню ⋮, prefs vpn_apps_show_system), поэтому чип «Все» считает пользовательские приложения, а не 563 пакета.
  • Порядок — sortInstalledApps: по имени (по умолчанию) или по недавним (lastUpdateTime из PackageManager, prefs vpn_apps_sort).
  • Массовые действия только по видимым — selectVisible добавляет видимое к набору (выбранное вне фильтра не теряется), deselectVisible снимает только видимое; «снять все» уносит весь набор через диалог подтверждения.
  • Чистка набора — pruneUninstalled: пакет, которого больше нет на устройстве, нельзя снять в списке, поэтому он убирается при загрузке списка с сообщением «Убрано удалённых приложений: N».

Инвариант: секция // selected строится по поиску, но не подчиняется тумблеру системных — иначе выбранное системное приложение нельзя снять. Массовые действия считают то, что показано: в фильтре «Выбранные» — выбранные строки. Строка живёт ровно в одной секции: выбранное вырезано из групп (unselectedInstalledApps), иначе один пакет попадал в LazyColumn дважды и экран падал на дубликате ключа (Key … was already used).

Список читает loadInstalledApps в IO-корутине VM (метки, иконки, lastUpdateTime) — это пара секунд. Пока список пуст и проход идёт (appsLoading), шапка показывает «загрузка приложений…», а тело — лоадер VpnAppsLoadingState: без них экран встречал пользователя счётчиком «выбрано 0 / 0 приложений» и пустым списком, что читалось как поломка. Повторный заход перечитывает список молча (загруженное остаётся на экране, параллельный проход не стартует), лоадер гасится и при ошибке чтения.

Серверы

VpnServerEntity: имя, протокол (hysteria2), хост, порт, пароль, allowInsecure, SNI, пароль обфускации, флаг «по умолчанию». Серверы хранятся в Room; пароль и пароль обфускации — поля SecretString, в базе это шифротекст (AES-GCM на ключе AndroidKeystore, см. data/secrets), открытый текст только в памяти.

Добавление возможно вставкой ссылки: VpnLinkParser.parse понимает hysteria2:// и hy2://, вытаскивает хост, порт (по умолчанию 443), логин, пароль, SNI и параметры обфускации. Неподдерживаемый формат даёт «Unsupported link format».

Автостарт и перезапуск

  • Автостарт: prefs vpn_auto_start + vpn_auto_server_id — при заходе в приложение (MainActivity → VpnManager.autoStartIfNeeded) туннель поднимается сам. Включение тумблера сразу фиксирует выбранный сервер, а если его нет — берётся дефолтный (раньше автозапуск молча ничего не делал);
  • автозапуск не срабатывает повторно, если VPN уже connecting / connected / reconnecting: лишний ACTION_CONNECT перезапускал живой hysteria2 и рвал сессию;
  • база серверов поднимается самим геттером (App.vpnServerDao() → agentRepository()): на холодном старте автозапуск обращается к ней первым, а раньше agentDb!! падал с NullPointerException — и туннель не поднимался;
  • START_STICKY + префы vpn_desired_on / vpn_last_server_id: если систему перезапустили, VPN поднимается сам. Эти префы и зеркало политики (per_app + набор приложений) лежат в отдельном файле vpn_service_state, который пишет только процесс :vpn — общий файл настроек сервис не трогает, чтобы не затирать ключи главного процесса;
  • always-on VPN включается в системных настройках — код только переживает перезапуск, сам в системе ничего не меняет.

SSH и VPN

Терминал приложения ходит через SOCKS5 1080 hysteria2, то есть как обычный трафик внутри туннеля (SshPtyTransport, флаг useSocksProxy); без VPN — напрямую. Отдельного прокси мимо туннеля в приложении нет.

5. Данные: БД, файлы, настройки

ЧтоГде лежитКак меняетсяКто читает
СерверыRoom (VpnServerEntity; пароли — шифротекстом)Экраны серверовY83VpnService
Режимprefs vpn_modeСекция // modeVpnManager
Выбранные приложенияprefs vpn_selected_appsЭкран выбора приложенийСервис
Системные в спискеprefs vpn_apps_show_systemМеню ⋮ экрана выбораЭкран
Порядок спискаprefs vpn_apps_sortМеню ⋮ экрана выбораЭкран
Автостартprefs vpn_auto_start, vpn_auto_server_idСекция // optionsVpnManager
Желаемое состояниеprefs файла vpn_service_state: vpn_desired_on, vpn_last_server_idSTART_STICKY-восстановлениеСервис
Политика сервисаprefs файла vpn_service_state: vpn_service_per_app, vpn_service_appsЗапись при connect/apply policyСервис (восстановление)
Конфиг hysteria2Генерируется на летуHysteriaConfigGeneratorПроцесс hysteria2
БинарьjniLibs/arm64-v8a/libhysteria.soТолько сборкаСервис

6. Backend: REST/роуты

Неприменимо: VPN работает целиком на устройстве и не ходит в backend.

7. Мост и MCP

КомандаЧто делает
vpn_stateСостояние, попытка, причина
vpn_connectПодключиться
vpn_disconnectОтключиться
vpn_restartМягкий рестарт hysteria2
vpn_retryСброс попыток и немедленное подключение
vpn_appsРежим, число и список выбранных пакетов (PER_APP)
app_log {file: "vpn"}Лог процесса :vpn

Команды относятся к пространству vpn_* реестра BridgeCommands.kt. Логи процесса :vpn пишутся в filesDir/log-vpn.txt (зеркало в /sdcard/Download/y83ws/log-vpn.txt), main — в log.txt: два процесса больше не пишут в один файл.

8. Настройки, конфиг, квоты и лимиты

ПараметрЗначениеГде задаётся
Попытки реконнекта1/2/4/8/16 с, затем 60 сVpnReconnectPolicy
Дебаунс сетевого события1.5 сVpnNetworkMonitor
Таймаут пробы туннелядо 8 сTunnelProbe
Интервал статистики3 сСервис
Жёсткий пересбор TUNпосле 3 неудачПолитика
Порт hysteria2 SOCKS1080Конфиг
TUN-подсеть10.0.88.88/16Конфиг сервиса
Архитектуратолько arm64Нет armeabi-v7a бинаря hysteria

9. Ловушки и ограничения

  • Нужен arm64. Бинаря hysteria под armeabi-v7a нет — устройство должно быть arm64, иначе бинарь не запустится.
  • Обновлять hysteria2 только через jniLibs/arm64-v8a/libhysteria.so со сверкой sha256 с hashes.txt релиза.
  • Kill switch сделан через TUN, а не ip rule. Настройка правил и маршрутов требует root и не работает; роль kill switch выполняет сам поднятый TUN.
  • Во время реконнекта приложения видят «нет сети», а не утечку трафика. Это сознательный компромисс: TUN остаётся поднятым.
  • Always-on VPN — только в системных настройках. Приложение переживает перезапуск сервиса, но само в системе ничего не меняет.
  • Логи VPN в отдельном файле. Искать их надо через dev_log {source:"app", file:"vpn"}, а не в общем log.txt.
  • Сервер по умолчанию и автостарт — разные вещи. Можно задать дефолтный сервер, но не включить автостарт, и тогда туннель не поднимется сам.
  • Правка или удаление активного сервера переподключает туннель. Сохранение настроек уходит в VpnManager.onServerUpdated (мягкий реконнект тем же сервером), удаление активного — в onServerDeleted (туннель гасится, id автозапуска снимается); без этого hysteria2 работал бы по старым host/паролю.
  • Ссылка должна быть hysteria2:// или hy2://. Другие форматы парсер не понимает.

10. Источники правды

  • app/src/main/java/com/y83/ws/ui/vpn/VpnScreen.kt — главный экран (статус, режим, опции).
  • app/src/main/java/com/y83/ws/ui/vpn/VpnViewModel.kt — состояние, команды, серверы.
  • app/src/main/java/com/y83/ws/ui/vpn/VpnServerListScreen.kt, VpnAddServerScreen.kt, VpnEditServerScreen.kt — серверы.
  • app/src/main/java/com/y83/ws/ui/vpn/VpnAppSelectionScreen.kt — выбор приложений (PER_APP).
  • app/src/main/java/com/y83/ws/ui/vpn/VpnAppRows.kt — строки, счётчики, пустые состояния и тулбар экрана выбора приложений.
  • app/src/main/java/com/y83/ws/ui/vpn/VpnAppSelection.kt — чистая логика выбора (фильтр, группы, массовые действия, чистка набора).
  • app/src/main/res/values/strings_vpn.xml — строки раздела VPN (RU, пара — values-en).
  • app/src/main/java/com/y83/ws/vpn/Y83VpnService.kt — сервис в процессе :vpn.
  • app/src/main/java/com/y83/ws/vpn/VpnReconnectPolicy.kt — политика попыток.
  • app/src/main/java/com/y83/ws/vpn/VpnNetworkMonitor.kt, TunnelProbe.kt, NetworkState.kt — сеть и готовность.
  • app/src/main/java/com/y83/ws/vpn/HysteriaConfigGenerator.kt, Tun2socksWrapper.kt — генерация конфига и JNI.
  • app/src/main/java/com/y83/ws/data/vpn/VpnLinkParser.kt — разбор ссылки.
  • app/src/main/java/com/y83/ws/data/db/VpnServerEntity.kt — модель сервера.
  • docs/tech/vpn-reliability.md — надёжность, инварианты, диагностика.

Источник: docs/project/hub/23-vpn.md · Безопасность и сеть · 3 371 строк в 12 файлах. Страница собирается из документации проекта при сборке сайта.