Нет, приложение VpnService не может включить VPN телефона
Приложению принадлежит один туннель и его жизненный цикл, а не переключатель VPN телефона. Согласие можно отозвать, одновременно работает только один VPN, а пакеты разбирать вам.
Ошибочное предположение состоит в том, что приложение VpnService переключает системный тумблер и с этого момента телефон защищён. На самом деле происходит более узкое: приложение запрашивает согласие, строит интерфейс, читает и пишет пакеты через файловый дескриптор и возвращает интерфейс, когда останавливается. Одновременно может существовать только один такой туннель, и система может забрать его, не спрашивая. Большая часть путаного поведения, которое люди приписывают протоколу, происходит именно из этой модели владения.
Рассматривайте каждый переход жизненного цикла как часть проекта, а не как крайний случай.
Миф: приложение включает VPN телефона
Поток начинается с VpnService.prepare(), который возвращает Intent, обязательный к принятию пользователем, и продолжается establish(), который после этого всё ещё может не сработать по причинам, не подконтрольным приложению. Создание второго туннеля деактивирует первый интерфейс и заменяет системное уведомление. Нигде в этой последовательности приложение не владеет переключателем для всего устройства; оно владеет выданным ему дескриптором и пакетами, которые на него попадают. Даже постоянное уведомление принадлежит системе, а не дизайну приложения.
Поскольку интерфейс выдаётся, а не захватывается, другой клиент может занять место в любой момент. Приложение узнаёт об этом через обратный вызов, а не через возвращаемое значение.
Принудительная остановка приложения видна в панели уведомлений по той же причине: система сообщает, что она выдала, а не то, что приложение решило показать.
Миф: согласие даётся один раз
Когда система передаёт туннель другому клиенту, она отправляет транзакцию отзыва по binder, который базовый класс возвращает из onBind(). Приложение, заменяющее этот binder своим, должно само переслать транзакцию отзыва, иначе VpnService.onRevoke() никогда не сработает и служба продолжит писать в дескриптор, которого больше нет. Согласие может отозвать и пользователь, а система может остановить службу при перезапуске или переподключении в режиме постоянной работы.
Разбираться с этим — конкретная работа, а не строка журнала: прекратите читать дескриптор, закройте его, сбросьте флаг подключения и скажите пользователю, в каком он состоянии. Затем решите, должно ли приложение восстановить соединение, спросить снова или подождать, ведь в конфигурации постоянной работы система может вернуть службу сама.
Приложение, которое считает отзыв исключением, а не состоянием, будет сообщать о подключении, пока каждый записываемый им пакет отбрасывается, а это худший возможный режим отказа для инструмента приватности.
Миф: интерфейс фильтрует трафик за вас
Приходит сырой IP. Захват или фильтрация означают разбор заголовков IP, TCP, UDP и IPv6, отслеживание потоков и запись ответов обратно в дескриптор, что делает приложение захвата небольшим сетевым стеком, а не системным средством. protect() существует, чтобы собственный транспортный сокет приложения обходил свой туннель, а не захватывал сам себя, и возвращает логическое значение, которое стоит проверять на каждом сокете, созданном после поднятия туннеля.
Списки разрешённых и запрещённых приложений в конструкторе определяют, чей трафик попадает в интерфейс. Они не дают доступа к полезной нагрузке за пределами IP-пакетов, которые несёт интерфейс, и ни один API не раскрывает туннель другого клиента или перечисляет VPN-профили системы. Сетевые возможности могут сказать, что сеть — это транспорт VPN, но не чей это туннель.
Поэтому вопросы о состоянии других приложений получают противоречивые ответы: информация просто не раскрывается вашему процессу.
Что на самом деле требует жизненный цикл
Отслеживайте состояния, которыми управляете: подготовлено, установлено, отозвано, остановлено. Переподключение должно быть разборкой и затем установлением, с единственным владельцем дескриптора и транспортного сокета, потому что два компонента, соревнующиеся за установление, будут драться за единственное свободное место. Фоновые запуски ограничены в современном Android, поэтому широковещательная рассылка при загрузке не гарантия; используйте службу переднего плана с её уведомлением там, где уровень API это позволяет.
Записывайте переход, а не только конечное состояние, и держите машину состояний достаточно маленькой, чтобы проверить каждый путь. Состояние, до которого нельзя дойти в тесте, — это состояние, с которым вы встретитесь в поле без журнала.
Проверьте жизненный цикл на устройстве
Эмуляторы умеют подделывать подключение, но не путь согласия и отзыва убедительно, поэтому запускайте это на реальном устройстве с установленным вторым VPN-клиентом, который украдёт место.
adb shell dumpsys connectivity | grep -i -A3 vpn
adb shell dumpsys activity services | grep -i -A2 vpnservice
adb logcat -s VpnService:V AndroidRuntime:E
adb shell am force-stop com.example.vpnapp
Запустите туннель, подтвердите уведомление и интерфейс, затем подключите другой клиент и смотрите, выполняется ли обратный вызов отзыва и очищается ли состояние. Принудительно остановите приложение и проверьте, не сообщает ли что-то ещё о живом дескрипторе.
Повторите последовательность после перезапуска в режиме постоянной работы, потому что этот путь отличается от соединения, начатого пользователем. Вы ищете не зелёный значок, а доказательство того, что приложение перестало использовать дескриптор, которым больше не владело.