The Jerusalem PostHouthis reiterate warning against airlines, travelers using Saudi airports after attack kills 12InquirerCandon City gives diesel subsidy to rice farmers amid El Niño threatESPNReal or not: Fury to KO Joshua? Dubois-Wardley 2 a classic? Garcia-Haney 2 next?Inquirer EntertainmentSarah Lahbati says Barbie doll ‘ganda lang,’ Annabelle scarierCNN TürkGalatasaray - Barcelona maçının hakemi Guida olduZDF heuteEntdecken Sie das ZDF-NachrichtenstudioSportstarUdhayveer Sidhu wins men’s 10m air pistol gold at ISSF World Cup 2026CumhuriyetAnkara 2 No'lu Baro Başkanı Ağdemir'in 'itiraf'ı Akın Gürlek'e soruldu: 'Bu ayrıcalık hangi uygulamaya dayanıyor?'Observador DesportoA1 cortada em Aveiras de Cima após acidente com camiãoVariety‘Between Two Lovers’ Review: Nanako Hirose’s Contentious Family Drama Examines an Ill-Fated Throuplen-tvTochter erwartet Nachwuchs: Bruce Springsteen wird zum zweiten Mal GroßvaterFootball ItaliaSerie A Liveblog including Como-Roma, Sassuolo-Milan, Cagliari-Juventus
The Daily Newsstand · Free, Always
Sunday, October 11, 2026

Почему значок VPN в Android не гарантирует, что VPN действительно работает

Translate

В последнее время в России, VPN стал неотъемлемой частью жизни множества людей, так как без него определённая часть сервисов попросту недоступна. В связи с этим в повседневную жизнь вошло в привычку постоянно гадать из за чего именно не получается получить доступ к тому или иному сервису. Это плохое соединение? Упал сайт? Или VPN в очередной раз попал под блокировку и теперь не пропускает трафик?

Казалось бы, чтобы проверить работает ли VPN, достаточно посмотреть на соотвествующий значок в шторке телефона. Если горит - значит дело не в VPN. Не горит - сразу ясно кто виноват.

Проблема в том, что сам значок символизирует совсем не то, что кажется на первый взгляд. Он обозначает существование VPN интерфейса, а вот проверка проходит ли через этот интерфейс трафик не входит в зону его компетенции. Казалось бы какая разница, звучит похоже интерфейс есть, значит должно работать. А вот на практике получается, что VPN клиент завис, сервер на другом конце попал под блокировку или недоступен и соединение фактически мертво, а иконка как горела, так и горит, потому что с точки зрения системы интерфейс никуда не делся.

Это не редкие гипотетические ситуации, а повседневные сценарии в условиях, когда VPN сервисы регулярно блокируются. Соединение может “подвиснуть” ровно в тот момент, когда провайдер блокировки нашёл и прижал конкретный IP или протокол, и клиент ещё не успел на это отреагировать разрывом туннеля. Пользователь в этот момент смотрит на ярко горящий значок, и даже не может представить, что происходит на обратной стороне.

Дальше я расскажу о том, как Android на самом деле определяет когда показывать значок с VPN, почему этого не достаточно, какие есть способы проверить состояние VPN интерфейса, и что из этого выросло в отдельный pet проект - виджет для Android, показывающий состояние соединения в удобном формате без необходимости открывать VPN приложение каждый раз, чтобы убедится в причине проблемы.

Как Android определяет наличие VPN

Самый простой способ проверить наличие VPN интерфейса - это использовать Connectivity Manager и NetworkCapabilities:

val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManagerval 
network = cm.activeNetwork ?: return false
val caps = cm.getNetworkCapabilities(network) ?: return false
val isVpnActive = caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)

Логика простая: проверяется наличие транспорта TRANSPORT_VPN и если он есть значит, VPN интерфейс существует. Это официальный и задокументированный способ, который абсолютно корректно отвечает на вопрос “существует ли сейчас VPN интерфейс в системе”.

Беда в том, что нас интересует совсем другой вопрос, не “существует ли интерфейс”, а “работат ли он на самом деле” это два разных вопроса, и TRANSPORT_VPN в этом не сильно нам помогает.

Почему этого недостаточно

Интерфейс - это виртуальное сетевое устройство. Система создаёт его в момент, когда VPN клиент устанавливает соединение, и убирает, когда клиент его закрывает. Всё, что происходит внутри этого интерфейса - доходят ли пакеты до сервера, отвечает ли сервер, не завис ли туннель, уже не входит в зону ответственности TRANSPORT_VPN. Флаг остаётся true даже если:

сервер VPN недоступен и все запросы уходят в таймаут; сам процесс VPN клиента завис, но не успел закрыть интерфейс; соединение “просело” при переключении Wi-Fi на мобильную сеть

Казалось бы, для этого в Android есть более подходящий флаг - NET_CAPABILITY_VALIDATED, который по документации должен означать “система подтвердила, что через эту сеть реально проходит трафик”. На бумаге это ровно то, что нужно. На практике этот флаг ведёт себя нестабильно, не обновляется вовремя или не выставляется вовсе. Полагаться на него в продакшене оказалось рискованно: он либо давал ложные срабатывания, либо просто не менялся тогда, когда было нужно.

Следуя старому доброму принципу “Если хочешь сделать хорошо, сделай это сам”, я решил, что разобраться во всём этом должно быть не так уж и сложно и приступил к работе.

Возможные способы проверки VPN

Раз системных флагов недостаточно, единственный вариант - активная проверка. У неё тоже есть несколько подходов, и ни один не идеален сам по себе.

Проверка интерфейса TRANSPORT_VPN. Самый простой способ - не требует сети, не тратит трафик. Но, как сказано выше, отвечает только на вопрос “существует ли интерфейс”, а не “работает ли он”.

Запрос к внешнему сервису поверх активного соединения. Если запросить, например, свой публичный IP через HTTP запрос, и получить осмысленный ответ - значит, трафик реально доходит и возвращается. Это самая честная проверка из всех: она тестирует именно то, что нужно пользователю “мой трафик сейчас реально куда-то идёт”. Минусы тоже понятны: зависимость от стороннего сервиса (если он сам недоступен можно ошибочно решить, что виноват VPN), дополнительный сетевой запрос и расход батареи при частой проверке.

TCP пинг вместо ICMP. Настоящий ICMP echo request (классический ping) на Android недоступен обычным приложениям без root - сырые сокеты закрыты для непривилегированных процессов. Стандартный обходной путь, которым пользуется большинство “ping” приложений - замерять не ICMP эхо, а время установки TCP-соединения до заведомо доступного порта (например, 443 у публичного DNS сервера). Это не совсем то же самое, что классический пинг, но даёт сопоставимую по смыслу метрику задержки. У подхода есть и свои минусы: ложные срабатывания в обе стороны: ложноположительное - порт открыт и отвечает быстро, но это никак не говорит о качестве конкретного VPN туннеля, если проверка идёт мимо него; ложноотрицательное - файрвол на пути может не пропустить такой тип запроса, хотя сам VPN в полном порядке.

Комбинированный подход. На практике использование оидиночных проверок самим по себе не имеет смысла и поэтому их стоит объединить: интерфейс TRANSPORT_VPN есть, и внешний запрос через него прошёл - состояние “OK”. Интерфейс есть, но запрос не прошёл - “DEAD” (соединение подвисло). Интерфейса нет вообще - “OFF”. Тройное состояние вместо привычного бинарного “подключено / не подключено” честнее отражает реальность.

От исследования к собственному решению

Отсюда и родилась идея pet проекта - собрать собственный инструмент под себя, который будет отображать именно то, что мне нужно и избавит от лишних действий для проверки состояния VPN.

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

Неожиданные сложности разработки

Ограничения Jetpack Glance

Виджет реализован на Jetpack Glance - современном Compose подобном API для виджетов рабочего стола. У Glance есть одно не самое очевидное ограничение, а именно лимит на количество прямых дочерних элементов в Column. При попытке впихнуть весь список полей (статус VPN, IP, страна, тип сети, скорость, качество соединения, DNS и т.д.) в один сплошной Column упираешься в этот лимит, а помимо лимита, просто в клиппинг: виджет на маленьком размере превращается в обрезанную кашу текста.

Решение состояло из двух частей. Во-первых, SizeMode.Responsive - позволяет задать несколько опорных размеров виджета и рендерить для каждого свой набор контента, ориентируясь на LocalSize.current. Во-вторых - разбить сам виджет на переиспользуемые секции (Status / Network / Speed / Quality / Footer), которые собираются в разной комбинации в зависимости от того, к какой конфигурации (small / medium / large) ближе всего текущий размер виджета. Маленький виджет показывает только статус VPN, IP и скорость; большой разворачивает полный набор вплоть до DNS-серверов и информации о хосте.

Жёсткий потолок в 15 минут для фонового обновления

Это оказалось самым архитектурно значимым ограничением всего проекта. WorkManager - стандартный и рекомендуемый Google механизм для периодических фоновых задач на Android, не позволяет указать период обновления чаще, чем раз в 15 минут. Это не настройка, которую можно подкрутить, а жёстко зашитый в платформу минимум.

Для приложения весь смысл которого состоит в том, чтобы постоянно показывать актуальную информацию по поводу статуса VPN, эти 15 минут являются очень грубым ограничением, которое заметно подрывает смысл вообще всей затеи.

Рабочее решение оказалось не в том, чтобы обмануть WorkManager, а в том, чтобы для части сценариев вообще не зависеть от него. У Android есть принципиально другой класс процессов - foreground сервисы, которые система не убивает произвольно (в отличие от фоновых процессов или обычного таймера, которые могут быть прибиты в любой момент под предлогом экономии батареи). Если у пользователя включено постоянное уведомление в шторке, оно уже обязано быть foreground сервисом по требованиям Android к такого рода уведомлениям. А раз процесс и так вынужден жить постоянно, почему бы не привязать к нему ещё пару обязанностей, чтобы он мог крутить цикл обновлений в обход временных ограничений WorkManager.

Так появился обход этого ограничения, чтобы использовать интервалы обновления меньше 15 минут нужно включить постоянное уведомление, как уже было описано выше это не мой личный каприз а уловка против встроенных ограничений самой системы. У этого foreground цикла при этом есть и собственный нижний защитный порог: 30 секунд, не позволяющий интервалу уйти в ноль, отдельная страховка от просадки батареи, независимая от 15-минутного лимита WorkManager.

Отдельным и, возможно, более важным механизмом внутри этого же foreground сервиса стал ConnectivityManager.NetworkCallback - подписка на смену сети. Он даёт мгновенную реакцию на смену статуса сети и доступен ровно в тех же условиях, что и ускоренное обновление. Только пока постоянное уведомление включено у нас есть доступ к функционалу foreground сервисов. Первая версия такого механизма быстро столкнулась с проблемой: не любое изменение сети достойно вызывать обновление. Сила сигнала Wi-Fi меняется постоянно, и реагировать на неё как на смену VPN статуса означало бы обновляться по несколько раз в секунду. Пришлось сузить фильтр до одного конкретного перехода - подтверждения NET_CAPABILITY_VALIDATED и добавить окно задержки в полторы секунды, потому что один сетевой хэндовер (напрмер, переустановка VPN-туннеля) реально способен выстрелить несколько callback событий подряд за доли секунды.

В сумме получилось три независимых триггера обновления: периодическая задача WorkManager (постоянный фон, минимум раз в 15 минут), ручное нажатие пользователя, и при включённом постоянном уведомлении foreground цикл с собственным интервалом и сетевой сторож поверх него. Все три сходятся в одной точке - координаторе с mutex, вместо того, чтобы каждый писал состояние по отдельности и рисковал гонкой или слишком частым обновлением.

Самовосстановление фоновых сервисов

Оверлей, рисующий плавающую полоску текста поверх других приложений, сознательно сделан не foreground сервисом (в отличие от постоянного уведомления). Это значит, что система в любой момент может тихо его убить без единого предупреждения пользователю.

Попытка сделать его неубиваемым - тупиковый путь: система целенаправленно проектирует эти ограничения именно для того, чтобы такие процессы можно было останавливать. Рабочий подход - не бороться с этим, а закладывать в архитектуру постоянную самопроверку: при каждом обновлении данных и при каждом возврате пользователя в приложение onResumeсостояние оверлея перепроверяется и, если нужно, поднимается заново.

Различия между устройствами

Отдельная категория сложностей - расхождения в поведении между производителями. Один конкретный пример: модификатор cornerRadius() в Jetpack Glance, отвечающий за скруглённые углы виджета, корректно работает на большинстве устройств, но ломается на прошивках Huawei/EMUI. Обходной путь - не полагаться на программный модификатор Glance, а вернуться к старому доброму XML drawable ресурсу для скругления, который у системы рендеринга виджетов уже давно и стабильно поддерживается на всех прошивках.

Мой первый опыт публикация в Google Play

Одним из самый неприятных препятствий при публикации стало формальное требование Google для новых приложений на аккаунтах разработчика - закрытое тестирование: минимум 12 тестировщиков, активных 14 дней подряд, прежде чем открыть доступ к продакшен-релизу.

На первый взгляд это выглядит мелкой бюрократической формальностью, а на практике превращается в самое узкое место всего процесса публикации: одиночному разработчику попросту негде взять 12 живых людей, готовых две недели подряд держать приложение установленным.

Практическое решение, которым пользуется сообщество инди-разработчиков - группы взаимного тестирования: ты ставишь чужие приложения, тебе - твоё. Но не всё так просто как кажется на первый взгляд. Чтобы гарантировано набрать 12 тестировщиков лучше брать с запасом, так как часть обязательно не досидит до конца и отвалится. При таком объеме 15-20 чужих приложений одновременно ни у кого уже не остаётся ни то что мотивации, а даже времени, чтобы с толком честно тестировать каждое приложение. В итоге всё сводится к тому, что человек заходит в приложение, делает скриншот, и отправляет его как подтверждение нужному разработчику. Формальное требование выполняется Goggle счастлив, а значит и все остальные тоже должны быть счастливы.

Как по мне то порог тестировщиков в 5 человек был бы намного уместнее. Пятерых человек одиночный разработчик вполне может найти среди друзей и знакомых, и в этом случае у них действительно будет мотивация честно протестировать приложение, а не просто выполнять заданную кем то формальность.

Заключение


Возвращаясь к вопросу из начала статьи: значок VPN в шторке телефона показывает лишь существование интерфейса, а не факт того, что VPN реально работает. Для того, чтобы быть абсолютно уверенным в работе VPN нужно посылать через него тестовый трафик и проверять доходит ли он обратно, а операционная система сознательно не желает брать на себя дополнительную ответственность за трату батареи и трафика на эту дополнительную проверку.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.