Anubis: заморозка приложений по состоянию VPN

от автора

Anubis: pm disable вместо песочницы — почему заморозка приложений надёжнее Island, Shelter и Knox

Представьте: вы подключились к рабочему VPN – нужно зайти на корпоративный сервер или проверить доступность сервиса из другой юрисдикции. Потом, не выключая его, открыли приложение популярного маркетплейса – проверить, прибыл ли в пункт выдачи корм для почтовых воробьев. В этот момент приложение тихо просканировало localhost, нашло SOCKS5-порт вашего VPN-клиента, отправило через него запрос и узнало выходной IP вашего сервера. Завтра этот IP окажется в блэклисте. Сервер, за который вы или ваша компания платите $5 в месяц, вдруг внезапно деградировал. А вы даже не узнаете, кто вас сдал.

коллеги пытаются понять, почему

коллеги пытаются понять, почему

Эта статья о том, как я решил эту проблему. Не теоретически, а в виде работающего open-source приложения.

Anubis на GitHub – код, APK и инструкция по настройке. А ниже – как это устроено под капотом.

Предыстория

В апреле 2026 года Минцифры разослало крупнейшим российским площадкам методичку по обнаружению VPN-трафика. Компании получили конкретные инструкции: как выявлять VPN, как ограничивать доступ пользователям, и как передавать обнаруженные IP-адреса VPN-серверов в РКН для последующей блокировки.

Фактически каждое крупное российское приложение теперь потенциально содержит модуль, который:

  • определяет наличие VPN через ConnectivityManager

  • проверяет доступность заблокированных ресурсов

  • передаёт IP-адреса VPN-серверов для блокировки

Это не теория — статья Российский мессенджер MAX замечен в обращении к иностранным сервисам определения IP и серверам конкурентов описывает то, как обнаружили подобное поведение, статья “Месседжер MAX следит за пользователями VPN? Реверс инжиниринг говорит — да” показала конкретные обращения к серверам Telegram и WhatsApp для определения обхода блокировок.

Параллельно была обнаружена критическая уязвимость во всех популярных VLESS-клиентах: они поднимают локальный SOCKS5 прокси без авторизации, через который любое приложение на устройстве может узнать выходной IP VPN-сервера.

На фоне всего этого на Хабре появилась статья с предложением использовать Island/Insular для изоляции приложений. В комментариях развернулась дискуссия о том, насколько это реально защищает. Позже тот же автор опубликовал продолжение — серверное решение в виде трёхкаскадного VPN, где точка входа в инфраструктуру (внутри РФ) скрыта, а выходной узел делается расходным: шпион всё равно его сдаст, но это уже не больно — поднимаем новый и перенастраиваем. Подход рабочий, но реактивный: схема живёт в постоянной ротации выходных узлов. Я решил подойти с другой стороны — со стороны клиента: не дать шпиону вообще ничего узнать, отключив его до включения VPN.

Песочница: что она реально даёт, а что нет

Идея простая: помещаем приложения в рабочий профиль Android, и они не видят VPN. Insular (форк Island) — это приложение-песочница, создающее рабочий профиль.

Как работает рабочий профиль под капотом

Рабочий профиль — это отдельный пользователь Android (например, user 10), привязанный к основному (user 0). Приложения не клонируются: APK один, но у каждого профиля своя директория данных (/data/user/0/ vs /data/user/10/). Изолированы: файлы, контакты, аккаунты, буфер обмена, хранилище ключей. Не изолирована: сеть.

Что песочница действительно скрывает

Автор статьи про Island утверждает, что Т-Банк после помещения в песочницу “перестал жаловаться на VPN”. Проверка показала, что это правда — и вот почему:

ConnectivityManager в Android фильтрует сети по userId. VPN, созданный в user 0, не виден через ConnectivityManager.getAllNetworks() из user 10 (рабочего профиля). Это framework-level фильтр в ConnectivityService — не ядерная изоляция, а программная. Но для приложений, которые проверяют VPN простейшим способом (а таких большинство), этого достаточно:

// Этот код из рабочего профиля НЕ увидит VPN основного профиляval cm = getSystemService<ConnectivityManager>()val vpnActive = cm.allNetworks.any {    cm.getNetworkCapabilities(it)        ?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true}// vpnActive == false, хотя VPN включён
Что такое ConnectivityManager

ConnectivityManager — системный сервис Android для информации о сетевых подключениях. Любое приложение с разрешением ACCESS_NETWORK_STATE (есть практически у всех, не требует подтверждения) может спросить систему: «есть ли VPN?». В рамках одного профиля система ответит честно. Но между профилями — фильтрует. Это задокументированное поведение Android, не баг.

Так что песочница — не бесполезна. Она закрывает самый простой и распространённый вектор детектирования.

Что песочница НЕ скрывает

Проблема в том, что сетевой стек Android — один на всё ядро. Рабочий профиль не использует Linux network namespaces (хотя ядро их поддерживает — на этом построены Docker и LXC). Это значит, что из рабочего профиля доступны три канала утечки:

Что видит приложение из рабочего профиля

Как проверяет

Нужны права?

Интерфейс tun0

NetworkInterface.getNetworkInterfaces()

Нет

Маршрут через tun0

/proc/net/route

Нет

SOCKS5 прокси на localhost

Подключение к 127.0.0.1:<port>

Нет

Все три проверки работают без каких-либо разрешений. SOCKS5 на localhost — самая опасная: приложение в рабочем профиле может не просто обнаружить VPN, но и воспользоваться им, узнав выходной IP-адрес сервера.

Утилиты YourVPNDead и RKNHardering проверяют все каналы, а не только ConnectivityManager — поэтому они успешно детектят VPN из любого рабочего профиля: Island, Insular, Knox, Shelter, второе пространство Xiaomi.

А методичка Минцифры явно описывает проверку характерных портов прокси: SOCKS (1080, 9000, 5555), HTTP (3128, 8080), Tor (9050). Именно те проверки, от которых рабочий профиль не защищает.

Возможна ли полноценная сетевая изоляция на Android?

Может, Google просто доделает изоляцию? На настольных ОС виртуализация (VirtualBox, QEMU, Hyper-V) создаёт гостевую систему с собственным сетевым стеком. Гостевая ОС не видит VPN хоста – это настоящая изоляция. Linux-ядро, на котором построен Android, тоже так умеет – через network namespaces.

Но Android не пользуется network namespaces для профилей. VpnService, ConnectivityManager и весь сетевой фреймворк не рассчитаны на per-profile сетевую изоляцию. Чтобы песочница реально изолировала сеть, Google пришлось бы переработать сетевую подсистему. Этого нет, и в обозримом будущем не предвидится.

А что на других платформах?

**На ПК** проблема решается тривиально: помещаем приложения, которым не доверяем, в виртуальную машину (VirtualBox, QEMU). Даже если VPN на хосте работает в TUN-режиме и перехватывает весь трафик — гостевая ОС в режиме NAT этого не видит. Трафик гостя пойдёт через VPN хоста, но гость видит только свою виртуальную сетевую карту. У него нет доступа к интерфейсам хоста (tun0, wg0 и т.д.), к его таблице маршрутизации, к списку адаптеров. Факт наличия VPN для него невидим. Это полноценная изоляция. **На iOS** ситуация хуже, чем на Android: нет даже рабочих профилей, нет per-app VPN для произвольных приложений. Единственный вариант — второй телефон. **На Android** полноценной виртуализации нет, но есть другой механизм — полное отключение приложения на уровне системы. Об этом ниже.

Проблема глубже: фоновая активность

Но допустим, чудо произошло и Google добавил настоящую сетевую изоляцию для профилей. Спасло бы это нас? Нет.

Android архитектурно позволяет приложениям вести активность без явного действия пользователя. BroadcastReceiver, JobScheduler, WorkManager, push-уведомления через FCM – всё это запускает код приложения в фоне. Приложение, которое вы запустили два дня назад и давно свернули, продолжает жить: получает события, обрабатывает данные, отправляет запросы.

Это не баг, а “by design”. Android построен вокруг модели, где приложения реагируют на системные события. Но эта же модель означает, что приложение в любой момент может:

  • проверить состояние VPN

  • просканировать открытые порты на localhost

  • отправить HTTP-запрос к произвольному серверу

  • передать собранную информацию

Агрессивное энергосбережение (Doze mode, App Standby, OEM-оптимизации) может замедлить эту активность, но не гарантирует её полного отсутствия. Системные приложения и приложения, которым вы разрешили работу в фоне, вообще не подпадают под ограничения. А банковские приложения, маркетплейсы, мессенджеры – чаще других просят разрешить работу в фоне, потому что иначе пользователи жалуются на задержку уведомлений.

Песочница от фоновой активности не защищает – приложение в рабочем профиле работает точно так же, как в основном. Единственный способ гарантированно закрыть все векторы утечки – сделать так, чтобы приложение не существовало в системе в момент, когда VPN активен.

Итого по песочнице

Рабочий профиль – не бесполезен, но недостаточен:

  • Скрывает VPN от ConnectivityManager – работает против простых проверок (банковские приложения, Т-Банк)

  • Не скрывает tun0, маршруты и SOCKS5 – не работает против продвинутых проверок (методичка Минцифры, YourVPNDead)

  • Не блокирует фоновую активность – приложение продолжает сканировать сеть, даже если вы его не открывали

  • Имеет встроенную заморозку отдельных приложений и целого профиля (“Pause work apps”) – но это всё ещё ручное действие

  • Полноценная сетевая изоляция на Android невозможна архитектурно – и ждать её от Google не приходится

Единственный гарантированный способ запретить приложению любую активность – полностью его отключить. Отключённое приложение не может проверить VPN, просканировать порты или отправить запрос – его просто не существует в системе. В Island, кстати, тоже есть кнопка «заморозить» – и замороженное приложение точно так же мертво и, следовательно, безопасно. Но в Island вы делаете это вручную. Забыли заморозить перед включением VPN – всё, данные утекли.

Нам нужно не просто отключать приложения – нам нужно делать это автоматически, по состоянию VPN. Но сначала разберёмся, как вообще отключить произвольное приложение на Android.

Заморозка вместо изоляции

Кнопка «Отключить» – она уже существует

Android уже умеет полностью отключать приложения – но прячет эту возможность. Если вы зайдёте в Настройки → Приложения и откроете какое-нибудь предустановленное системное приложение (например, Google Фото или YouTube), вы увидите кнопку “Отключить”. Нажатие на неё полностью останавливает приложение: оно больше не запускается, не работает в фоне, не получает уведомления, его иконка пропадает из лаунчера.

Это именно то, что нам нужно. Отключённое приложение не может ничего детектить — оно мертво.

Но есть подвох: для приложений, установленных пользователем (а не предустановленных), этой кнопки нет. Для них Android предлагает только “Удалить”. Это ограничение интерфейса, не системы — под капотом механизм отключения работает для любых приложений. Просто Android не даёт к нему доступ через UI.

pm disable-user: та самая кнопка, но для всех

Системная команда pm disable-user делает то же самое, что кнопка “Отключить”, но для любого приложения:

pm disable-user --user 0 com.example.app

pm — это Package Manager, встроенный в каждый Android. disable-user отключает приложение для пользователя. --user 0 — основной профиль. com.example.app — имя пакета (например, ru.sberbankmobile для Сбера).

После этой команды приложение:

  • не имеет процесса в системе

  • не может запустить Service, BroadcastReceiver, ContentProvider

  • не получает Intent’ы и push-уведомления

  • не может обратиться ни к какому API, включая ConnectivityManager

  • не может просканировать localhost порты и отправить HTTP-запрос

  • иконка пропадает из лаунчера

Это не изоляция, а анабиоз. Приложение мертво на уровне PackageManager. Обратная операция – pm enable <package> – оживляет его мгновенно. Дальше в статье я буду называть это «заморозкой» – отключение, заморозка, «приложение мертво» означают одно и то же: pm disable-user.

Проблема доступа: ADB и его ограничения

Команда pm disable-user требует повышенных привилегий — уровня shell (UID 2000). На обычном Android приложения работают в своей “песочнице” и такие команды выполнять не могут.

Стандартный способ получить shell-доступ — ADB (Android Debug Bridge). Вы подключаете телефон к компьютеру по USB, устанавливаете Android SDK и выполняете:

adb shell pm disable-user --user 0 com.example.app

Это работает, но неудобно: нужен компьютер, USB-кабель, и каждый раз вводить команды вручную. Автоматизировать заморозку при включении VPN таким способом невозможно.

Shizuku: ADB без компьютера

Shizuku решает эту проблему. Это приложение, которое один раз запускается с правами shell (через ADB или Wireless Debugging на Android 11+), а потом предоставляет эти права другим приложениям — прямо на телефоне, без компьютера.

После настройки Shizuku любое приложение с его разрешением может выполнять shell-команды: pm disable-user, pm enable и другие — всё, что доступно в adb shell. Какие именно команды и зачем — станет понятно дальше.

Критически важно: Shizuku — не root. Он не ломает Knox, не теряет гарантию, не вызывает проблем с банковскими приложениями и проверками SafetyNet/Play Integrity. Shell-доступ — привилегии ADB, а не суперпользователя. Shizuku нужно перезапускать после перезагрузки телефона (или настроить автозапуск). Его используют десятки приложений — это зрелый инструмент с открытым исходным кодом.

Бонус: не только безопасность

Инструменты для заморозки давно существуют: Hail, IceBox. Замороженные приложения не расходуют заряд батареи, не занимают RAM, не создают фоновый трафик, не шлют уведомления – пользователь получает бонус к автономности и производительности. Но в этих инструментах триггером был “приложение мне редко нужно”. Теперь триггер другой: состояние VPN-подключения.

По степени защиты pm disable-user уступает только одному варианту — второму телефону, где потенциально опасные приложения физически находятся на другом устройстве. Но второй телефон — это неудобство, расходы и необходимость носить два устройства. Заморозка даёт 99% той же защиты на одном устройстве, без побочных эффектов.

Так появился Anubis — менеджер приложений, который автоматически связывает заморозку с состоянием VPN.

Архитектура Anubis

Три группы приложений

Вместо бинарного “заморозить / не заморозить” — три группы с разной сетевой политикой:

Группа

Состояние приложения по умолчанию

При запуске из Anubis

Без VPN

Заморожено

Выключает VPN → размораживает → запускает

Только VPN

Заморожено

Включает VPN → размораживает → запускает

Запуск с VPN

Активно

Включает VPN → запускает

“Без VPN” и “Только VPN” – всегда заморожены по умолчанию. Размораживаются только при явном запуске из Anubis или через ярлык. При смене состояния VPN – замораживаются обратно: включили VPN – заморозились приложения “Без VPN”, выключили – заморозились “Только VPN”.

“Запуск с VPN” – для приложений, которые морозить нет смысла (браузер, Telegram, YouTube), но которым для стабильной работы требуется VPN-подключение. Они живут как обычные приложения, получают пуши, но при запуске из Anubis автоматически поднимается VPN.

Таким образом, приложения “Без VPN” и “Только VPN” живут в постоянном анабиозе и оживают только по вашему явному запросу.

Главный экран: серые иконки — замороженные приложения, цветные — активные

Главный экран: серые иконки — замороженные приложения, цветные — активные

По сути, инверсия привычной модели Android: вместо “приложения работают всегда, если пользователь не вмешался” – “приложения мертвы всегда, если пользователь не запросил иное”.

Осознанный trade-off: замороженное приложение не получает push-уведомления. Если банк в группе “Без VPN” заморожен, пуш о переводе не придёт, пока вы не запустите его через Anubis. Такова цена за безопасность: либо приложение молчит и не шпионит, либо живёт и потенциально сливает данные. Anubis выбирает первое — а уведомления вы увидите, когда осознанно откроете приложение. Да и многие ли приложения шлют вам ВАЖНЫЕ пуши? Да и нейроресурс тоже стоит беречь.

Shizuku: shell-доступ без root

Чтобы вся эта оркестрация с заморозкой работала автоматически, нужен надёжный способ дёргать pm disable-user прямо из приложения. Anubis использует Shizuku (о котором мы говорили выше) – конкретно AIDL UserService паттерн из API 13.

// IUserService.aidl - выполняется в процессе Shizuku с правами shellinterface IUserService {    void destroy() = 16777114;    int execCommand(in String[] command) = 1;    String execCommandWithOutput(in String[] command) = 2;}
Почему AIDL, а не Shizuku.newProcess()

В Shizuku v13 метод `newProcess()` стал приватным. Вместо него используется паттерн UserService: мы определяем AIDL-интерфейс, реализуем его в классе `UserService`, который запускается в процессе Shizuku с правами shell. Связь через `Shizuku.bindUserService()`. UserService выполняет команды через `Runtime.getRuntime().exec()` — те же команды, что вы набираете в `adb shell`.

UserService инициализируется один раз в Application.onCreate() и живёт, пока работает приложение. Все компоненты (Activity, TileService, ShortcutActivity) используют один и тот же экземпляр. Это критично для отзывчивости — никаких задержек на подключение к Shizuku при каждом действии.

Управление VPN-клиентами

Anubis умеет запускать VPN-клиенты через shell-команды:

Клиент

Метод

Команда

v2rayNG

Toggle (widget broadcast)

am broadcast -a com.v2ray.ang.action.widget.click

NekoBox

Start/Stop (exported Activity)

am start -n moe.nb4a/...QuickEnableShortcut

Happ

Toggle (widget broadcast)

am broadcast -a com.happproxy.action.widget.click

v2rayTun

Toggle (widget broadcast)

am broadcast -a com.v2raytun.android.action.widget.click

V2Box

Toggle (widget broadcast)

am broadcast -a dev.hexasoftware.v2box.action.widget.click

Любой другой

Ручной

Открывает приложение

Toggle — та же команда, что отрабатывает при нажатии на виджет клиента на рабочем столе: если VPN выключен — включить, если включён — выключить. NekoBox — исключение: у него отдельные Activity для включения и выключения.

Что такое am broadcast и am start

`am` — Activity Manager, системная утилита Android. `am broadcast` отправляет broadcast Intent — сообщение, на которое может отреагировать любое зарегистрированное приложение. Так работают виджеты на рабочем столе: при нажатии на виджет VPN-клиента, система отправляет broadcast, а клиент его получает и включает/выключает VPN. Мы делаем то же самое, но из командной строки через Shizuku. `am start` запускает Activity — экран приложения. У NekoBox есть специальные Activity для включения и выключения VPN, которые не показывают UI, а просто выполняют действие и закрываются. Обе команды доступны через `adb shell` или через Shizuku с правами shell.

Пользователь также может выбрать любое установленное приложение в качестве VPN-клиента — оно будет работать в ручном режиме (Anubis открывает его для включения, а для выключения использует механизмы, описанные ниже).

Определение активного VPN-клиента

Когда пользователь нажимает “выключить”, нам нужно знать, какой именно клиент сейчас держит VPN. Пользователь мог переключить клиент в настройках, мог вручную запустить другой — мы не можем полагаться на то, что помним.

Система знает, какое приложение владеет VPN-сетью. Мы извлекаем это через Shizuku:

dumpsys connectivity | grep -A 30 'type: VPN\[' | grep -oE 'OwnerUid: [0-9]+'
Что такое dumpsys и UID

`dumpsys` — диагностическая утилита Android, которая выводит внутреннее состояние системных сервисов. `dumpsys connectivity` показывает все сетевые подключения, включая VPN. Для каждого подключения система хранит UID (User ID) — числовой идентификатор приложения, которое его создало. Зная UID, через `pm list packages —uid` получаем имя пакета. Это та же информация, которую Android показывает в шторке уведомлений: «Подключено через Happ» — просто мы извлекаем её программно.

Получаем UID владельца VPN-сети, резолвим в package name. Это работает для любого клиента — известного, неизвестного, кастомного. Даже если пользователь установил VPN-клиент, которого нет в нашем списке, мы покажем его package name в интерфейсе и сможем корректно его остановить.

Путь к работающему решению: три неудачные попытки

**Попытка 1: reflection на NetworkCapabilities.mOwnerUid.** На Android 11 поле попало в hidden API blocklist — getDeclaredField() бросает NoSuchFieldException. На Android 10 поле вообще не существует в публичном API. Отброшено. **Попытка 2: dumpsys connectivity | grep -i vpn.** Ловило строку NOT_VPN из WiFi-сети! Каждая WiFi-запись содержит NOT_VPN в capabilities. В результате возвращался UID 1000 (system) и package com.android.dynsystem. Полностью мимо. **Попытка 3: pidof для каждого известного клиента.** Находило все запущенные VPN-клиенты, а не тот, который реально держит VPN. v2rayNG мог висеть в фоне, пока Happ реально обеспечивал VPN. В итоге мы пытались остановить не тот клиент. **Работающее решение:** grep -A 30 ‘type: VPN\[‘ — паттерн, уникальный для VPN-сети. Не совпадает с NOT_VPN. Тестировано на Pixel 5, Android 11.

Изящный хак: как отключить любой VPN-клиент без API и без root

Итак, мы умеем запускать VPN и определять, кто его держит. Осталась обратная задача: как выключить VPN-клиент?

Почему обычные способы не работают

Для NekoBox это тривиально – у него есть отдельная Activity для отключения. Но для toggle-клиентов (v2rayNG, Happ и др.) всё сложнее: toggle-команда ненадёжна для остановки. При тестировании Happ обнаружилось: мы отправляем toggle, VPN на мгновение выключается, Anubis начинает размораживать приложения – а Happ уже переподключился. Приложения оказываются разморожены при активном VPN.

am force-stop тоже не панацея – не всегда срабатывает мгновенно. А для произвольного клиента, о котором мы ничего не знаем, API остановки нет вообще.

Dummy VPN: перехват через архитектуру Android

Я перепробовал всё что мог – и решение пришло из самой архитектуры Android: система разрешает только один VPN одновременно. Когда приложение вызывает VpnService.establish(), система автоматически отзывает предыдущий VPN.

Как работает VPN на Android

На Android VPN реализован через системный API VpnService. Приложение создаёт виртуальный сетевой интерфейс (tun0), через который система направляет весь (или часть) трафика. Но система разрешает только один такой интерфейс одновременно. Если второе приложение создаёт свой VPN — первый автоматически отключается. Это как розетка, в которую можно воткнуть только одну вилку. Мы используем это: создаём «пустой» VPN на долю секунды — система отзывает чужой — мы тут же закрываем свой. Результат: ни одного VPN. При первом использовании Android покажет системный диалог «Разрешить VPN?» — это одноразовое действие.

Мы создали StealthVpnService — минимальный VPN-сервис, который делает одну вещь:

class StealthVpnService : VpnService() {    private fun doDisconnect() {        // Устанавливаем свой VPN - система отзывает чужой        val fd = Builder()            .addAddress("10.255.255.1", 32)            .setSession("stealth-disconnect")            .establish()        // Немедленно закрываем - VPN нет вообще        fd?.close()        stopSelf()    }}

Результат: любой VPN-клиент отключён, и нам даже не нужно знать его package name. Требуется VPN permission (одноразовый системный диалог при первом использовании).

Поэтому toggle используется только для включения (когда VPN точно выключен). Для выключения – эскалация:

  1. API stop — только для клиентов с явной командой (NekoBox: QuickDisableShortcut)

  2. Dummy VPN — перехватываем VPN, закрываем свой → ни одного VPN нет

  3. am force-stop – останавливаем процесс обнаруженного клиента

Приложения не размораживаются, пока VPN реально не отключился. Состояние проверяется каждые 200мс через ConnectivityManager. Если после всех трёх шагов VPN всё ещё активен — остаёмся в защитном режиме и показываем ошибку.

Нюанс: если в системных настройках Android включена “Постоянная VPN” (Always-on VPN) для клиента, система будет автоматически перезапускать его после нашего перехвата. В этом случае Always-on нужно выключить.

Обнаружение API закрытых VPN-клиентов через jadx

Изначально Anubis поддерживал автоматический запуск только для v2rayNG и NekoBox — у них open-source, и API легко находится прямо в коде. Остальные клиенты работали в ручном режиме: Anubis открывал приложение, а пользователь сам нажимал “Подключить”. Выключение делалось через dummy VPN — для него реверс не нужен.

Но переключение контекста (открыл шторку → нажал на ярлык → подождал → переключился в чужое приложение → нажал там кнопку → вернулся в исходное) убивало весь смысл оркестрации. Хотелось одного нажатия. Так появилась задача — реверсить closed-source клиенты, чтобы и для них работала автоматика.

Целевые клиенты — Happ, v2rayTun, V2Box. Все три closed source, исходный код недоступен, документации по внешним API нет. Как был найден broadcast action для управления ими?

Ключевое наблюдение

Ресурсы Android (строки, XML-разметка, манифест) не обфусцируются. Обфускатор R8/ProGuard работает только с Java/Kotlin кодом: переименовывает классы, методы, переменные. Но strings.xml, AndroidManifest.xml, XML-описания виджетов остаются нетронутыми.

Это даёт нам точку входа: если у приложения есть виджет на рабочий стол (а у всех VPN-клиентов с поддержкой toggle он есть), мы можем найти его через ресурсы.

Пошаговая методика

  1. Декомпилируем APK через jadx

  2. Ищем виджет в ресурсах: в res/values/strings.xml находим app_widget_name – имя виджета (“Switch”, “Toggle”). Ресурсы не обфусцированы.

  3. Находим receiver в манифесте: в AndroidManifest.xml ищем <receiver> с <meta-data android:name="android.appwidget.provider"> – это класс AppWidgetProvider.

Важно: не перепутать с shortcuts

В APK может быть несколько компонентов с похожими именами. Например, в V2Box есть и `ScSwitchActivity` (в `shortcuts.xml`), и `WidgetProvider` (receiver в манифесте). `shortcuts.xml` — это ярлыки для лаунчера, а нам нужен именно **receiver виджета рабочего стола**. Отличить легко: у receiver’а есть «, а в `shortcuts.xml` перечислены « с « на Activity. Почему receiver, а не activity? Потому что `am broadcast` можно отправить в фоне без UI, а `am start` на Activity может показать экран. Виджет по определению работает через broadcast — нажатие на виджет отправляет PendingIntent с broadcast, receiver его ловит и дёргает toggle.

  1. Извлекаем broadcast action из кода: в Java-коде receiver’а ищем setAction("...") – эта строка тоже не обфусцирована, потому что broadcast action’ы являются runtime-константами.

  2. Проверяем логику: в onReceive() ищем паттерн toggle:

// Декомпилированный Happ (jadx output)// Классы и методы обфусцированы (kd5, r25), но строка action и структура видныintent.setAction("com.happproxy.action.widget.click");// ...if (kd5.a.getIsRunning()) {    r25.L(context);   // stop} else {    r25.J(context);   // start}
  1. Проверяем exported: в манифесте смотрим android:exported="true" у receiver’а – если да, broadcast можно отправить из любого приложения. У всех проверенных клиентов WidgetProvider имеет exported=true (системе нужно отправлять ему APPWIDGET_UPDATE). Но даже если бы exported=false – через Shizuku shell (UID 2000) broadcast доставляется в любой компонент, потому что shell обходит эту проверку.

Паттерн v2ray/xray форков

Все четыре проверенных форка (v2rayNG, Happ, v2rayTun, V2Box) используют одинаковую структуру:

Action:   <package>.action.widget.clickReceiver: <package>.receiver.WidgetProvider (или WidgetProvider1x1 у v2rayTun)

Зная package name нового форка, можно предсказать broadcast action без декомпиляции.

Shell-команда для toggle:

am broadcast -a <package>.action.widget.click -n <package>/.receiver.WidgetProvider

Это стандартный Android IPC — broadcast action’ы являются публичными интерфейсами по определению. Обнаружение API для целей совместимости защищено законодательством (EU Software Directive 2009/24/EC, ст. 1280 ГК РФ).

Важно: toggle используется только для включения VPN. Почему для выключения он ненадёжен и что используется вместо – описано выше в секции про dummy VPN.

Ярлыки на рабочий стол

Техническая часть позади. Под капотом Anubis умеет: замораживать/размораживать приложения, запускать и останавливать VPN, определять активного клиента. Осталось сделать это удобным – чтобы пользователь просто нажимал на иконку приложения, а вся оркестрация происходила за кадром.

Ярлыки Anubis на рабочем столе — выглядят как обычные иконки приложений

Ярлыки Anubis на рабочем столе — выглядят как обычные иконки приложений

Для каждого приложения из любой группы можно создать pinned shortcut через ShortcutManager. Ярлык выглядит как обычная иконка приложения и ведёт себя как обычный запуск. Но под капотом:

  1. Запускается ShortcutActivity — прозрачная Activity без UI

  2. Она определяет группу приложения (Без VPN / Только VPN / Запуск с VPN)

  3. Замораживает/размораживает нужные группы

  4. Включает/выключает VPN через соответствующий клиент

  5. Дожидается подтверждения состояния (VPN включился/выключился)

  6. Размораживает целевое приложение

  7. Запускает его

  8. Закрывается

Пользователь видит: нажал ярлык → приложение открылось. 200мс задержки, если Shizuku уже подключён. Вся оркестрация за кадром.

Дополнительные детали

Grayscale для замороженных приложений

На домашнем экране иконки замороженных приложений отображаются в grayscale:

private val grayscaleFilter = ColorFilter.colorMatrix(    ColorMatrix().apply { setToSaturation(0f) })

Визуально видно, что приложение отключено. При размораживании иконка мгновенно становится цветной — для этого используется реактивный счётчик frozenVersion, который инкрементируется при каждой операции заморозки или разморозки.

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

Если при запуске Anubis обнаруживает активный VPN, он автоматически замораживает приложения группы “Без VPN”. Не нужно ничего нажимать — приложения защищены с первой секунды.

Проверка сети

Встроенная карточка “Сеть” показывает ping (время ответа ipinfo.io), страну и город. IP и провайдер скрыты за спойлером — это приватная информация, если пользователь работает через личный сервер. Проверка автоматически запускается при каждом изменении состояния VPN — видно сразу: VPN включился → страна изменилась.

Quick Settings Tile

Плитка в системной шторке определяет состояние из реальности (через ConnectivityManager), а не из внутренней переменной. Это важно: состояние могло измениться из ShortcutActivity или другого компонента, и плитка должна это отражать.

Фоновый мониторинг VPN (опционально)

Без фонового мониторинга есть один зазор: пользователь включает VPN-клиент напрямую (не через Anubis), а приложения группы “Без VPN” в этот момент разморожены. Они могут увидеть VPN.

Для закрытия этого зазора в настройках есть опция “Фоновый мониторинг VPN”. При включении запускается Foreground Service (сервис с постоянной нотификацией, который Android гарантированно не убивает) с подписчиком на изменения сети NetworkCallback. Он мгновенно реагирует на изменение VPN-состояния:

  • VPN включился → замораживает группу “Без VPN”

  • VPN выключился → замораживает группу “Только VPN”

Цена — постоянная нотификация в шторке. Поэтому это осознанно вынесено в настройку: если вы управляете VPN только через Anubis (или через ярлыки), фоновый мониторинг не нужен — оркестратор и так всё контролирует.

А что если просто firewall?

Казалось бы, можно просто заблокировать доступ приложений к SOCKS5 порту через iptables. Но:

  1. iptables требует root. Shizuku даёт UID 2000 (shell), а для модификации firewall-правил нужен UID 0. AFWall+ и аналоги — только с root.

  2. Даже root-firewall обходится. Статья про уязвимость VLESS-клиентов показала, что приложение может обратиться напрямую к tun0 через setsockopt(SO_BINDTODEVICE), обходя per-app правила VpnService. Firewall iptables этот вызов тоже не блокирует.

  3. Отключённому приложению firewall не нужен. Если приложение отключено через pm disable-user, у него нет процесса — некому подключаться к SOCKS5, некому обращаться к tun0, некому вообще что-либо делать.

Более того, наш подход даёт двойную защиту даже в момент, когда приложение разморожено. Вспомним порядок действий при запуске приложения из группы “Без VPN”:

  1. Выключаем VPN → процесс VPN-клиента завершается → SOCKS5 порт закрывается → tun0 интерфейс исчезает

  2. Проверяем что VPN действительно выключен (поллинг ConnectivityManager каждые 200мс)

  3. Только после подтверждения размораживаем приложение

То есть к моменту, когда приложение оживает, прокси-порт уже закрыт и VPN-интерфейс уничтожен. Приложению просто нечего сканировать – ни открытого порта, ни tun0, ни активного VPN в ConnectivityManager.

Если собрать все подходы вместе – песочницу, firewall, заморозку – картина выглядит так:

Сравнение подходов (итого)

Island/Insular/Shelter

Firewall (root)

Anubis

Метод

Рабочий профиль

iptables

pm disable-user

Приложение работает?

Да

Да

Нет (мертво)

ConnectivityManager видит VPN?

Нет (фильтр по userId)

Зависит от правил

Невозможно

Видит tun0?

Да (ядро общее)

Да (SO_BINDTODEVICE)

Невозможно

Видит SOCKS5?

Да (loopback общий)

Зависит от правил

Невозможно

Видит маршруты?

Да (/proc/net/route)

Да

Невозможно

Фоновая активность?

Да

Да

Невозможно

Защита от простых проверок

Да

Зависит

Да

Защита от методички Минцифры

Нет

Частично

Да

Управление VPN

Нет

Нет

Да (5 клиентов + любой)

Root нужен?

Нет

Да

Нет (Shizuku)

Удобство

Ручная заморозка

Настройка правил

Один тап по ярлыку

Стек

  • Kotlin + Jetpack Compose (Material 3, dynamic colors)

  • Shizuku API 13.1.5 — AIDL UserService для shell-команд без root

  • Room с TypeConverters — хранение групп приложений

  • ConnectivityManager NetworkCallback — мониторинг VPN в реальном времени

  • ShortcutManager — pinned shortcuts с оркестрацией freeze/VPN/launch

Что дальше

  • Self-hosted app_process daemon — для работы без Shizuku

  • Экспорт/импорт конфигурации групп

  • Поддержка дополнительных VPN-клиентов по мере обнаружения их API

Быстрый старт

  1. Установите Shizuku (на Android 11+ — через Wireless Debugging прямо с телефона, без компьютера, за 1 минуту)

  2. Установите Anubis

  3. Дайте разрешения (Shizuku + VPN)

  4. Поместите приложения, для которых VPN нежелателен (банки, маркетплейсы), в группу “Без VPN”

  5. Всё. Теперь при включении VPN они замораживаются автоматически

Как помочь проекту

Anubis — полностью open-source (MIT). Проект на ранней стадии, и ему нужна помощь:

  • Звезда на GitHub — лучшая мотивация развивать проект

  • Какого VPN-клиента не хватает? Пишите в комментариях или в Issues. Добавить поддержку нового v2ray-форка — это 5 строк кода, а инструкцию по реверсу через jadx я оставил выше

  • Нашли баг? Issues на GitHub открыты

  • Хотите контрибьютить? PR приветствуются. Ближайшие задачи: self-hosted daemon без Shizuku, экспорт/импорт конфигурации

Ссылки

Благодарности

Отдельное спасибо @linux-over — за его серию статей про Island и трёхкаскадный VPN, которые задали контекст для этой работы, и за инвайт, без которого вы бы это не читали. Наши подходы хорошо комбинируются: его схема обеспечивает живучесть VPN-инфраструктуры (точка входа из РФ остаётся в тени, выходные узлы быстро ротируются), Anubis устраняет источник проблемы на клиенте. Вместе получается дешевле и спокойнее, чем по отдельности.


На этом технические решения проблемы, пожалуй, исчерпаны. А нетехнические… Как известно, или Internet Explorer, или падишах.

Anubis – MIT License. Приложение не предоставляет VPN-сервис, не содержит средств обхода блокировок и не предназначено для нарушения законодательства. Оно управляет жизненным циклом установленных приложений через стандартные механизмы Android (pm disable-user, VpnService API) и может использоваться для любых задач, связанных с приватностью и контролем фоновой активности приложений. Автор не призывает к нарушению действующего законодательства.

ссылка на оригинал статьи https://habr.com/ru/articles/1023352/