Как проверить в коде, подключён ли VPN на Android
В Android нет getter'а состояния VPN-подключения. Запрашивайте возможности сети, следите за изменениями транспорта и держите отдельно согласие приложения, состояние устройства и доказательство на сервере.
Нельзя спросить Android, подключён ли VPN. VpnService не предоставляет getter состояния подключения, а значок ключа в строке состояния рисует система, а не что-то, что ваше приложение может прочитать. Что можно сделать, так это задать стеку подключений более узкий вопрос: какую сеть использует трафик этого приложения и несёт ли эта сеть транспорт VPN. Это руководство проходит проверки в порядке, который исключает ложные срабатывания, от команды shell на устройстве до callback, который вы оставляете работать в приложении.
Проверьте устройство из shell, прежде чем трогать код приложения
Сначала получите опорное состояние. adb shell dumpsys connectivity печатает каждую сеть, известную системе, и каждый блок перечисляет свои транспорты, поэтому сеть VPN появляется в этом списке с VPN. Сначала прочитайте блок сети по умолчанию, затем ищите другие сети, несущие тот же транспорт.
Два вывода отвечают на разные вопросы. Наличие сети VPN не то же самое, что её использование вашим приложением, и на устройстве с рабочим профилем или вторым пользователем оба утверждения могут быть верны одновременно, пока ваш процесс обращается в интернет напрямую. Именно строка активной сети решает, что делают ваши сокеты.
Спросите, какую сеть использует приложение
В коде проверка возможностей это четыре строки. Запросите ACCESS_NETWORK_STATE в манифесте, затем сравните возможности активной сети с транспортом VPN:
ConnectivityManager cm = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
Network active = cm.getActiveNetwork();
NetworkCapabilities caps = cm.getNetworkCapabilities(active);
boolean vpnInUse = caps != null
&& caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN);
Ответ намеренно узкий: является ли сеть по умолчанию для приложения VPN прямо сейчас. Он ничего не говорит о том, какой это VPN, ваш ли он и доходит ли туннель до другого конца. Воспринимайте true как «трафик выходит через какой-то туннель» и ведите собственный учёт отдельно. Логическое значение, которое молча означает три вещи сразу, и есть причина, по которой экран настроек показывает VPN выключенным, пока пользователь работает через него.
Когда, наоборот, нужно узнать, присутствует ли хоть какая-то сеть VPN, перебирайте cm.getAllNetworks() и проверяйте каждый набор возможностей. Два ответа расходятся на устройстве с VPN для отдельных приложений, который охватывает другие приложения, но не ваше, и это расхождение источник большинства сообщений об ошибках, где говорится, что приложение не видит явно включённый VPN.
Следите за изменениями вместо опроса
Опрос по таймеру неправильная форма для этого вопроса, потому что изменения состояния приходят как события. Зарегистрируйте запрос, соответствующий транспорту VPN, и позвольте платформе сообщить вам, когда подходящая сеть появляется, исчезает или меняется:
NetworkRequest request = new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build();
cm.registerNetworkCallback(request, callback);
Сеть VPN никогда не несёт NET_CAPABILITY_NOT_VPN, поэтому явное удаление документирует намерение. Без него запрос всё равно совпадает, но следующему человеку, читающему код, придётся догадаться самому.
Регистрация принадлежит области, которой нужно наблюдение, обычно экрану или службе переднего плана. Отменяйте регистрацию в onStop или onDestroy и не оставляйте callback привязанным к активности, которой уже нет. Приложение без видимых компонентов плохой хозяин для постоянного наблюдателя, а забытые callback те, что падают после изменения конфигурации.
Держите три вопроса отдельно
Большая часть путаницы в этой области возникает из-за того, что три разные проверки считают одной. Они используют разные API, и каждая может быть истинной, пока остальные ложны.
| Question | How you answer it | What a yes means |
|---|---|---|
| Did the user grant VPN consent to my app? | VpnService.prepare() returns null | Consent exists; the tunnel may still be down |
| Is the device sending default traffic through a VPN? | TRANSPORT_VPN on the active network | Something is tunneling, not necessarily your app |
| Did this request arrive through the VPN? | Source address seen by the server | Proof for that connection, nothing wider |
Третья строка та, которую люди пытаются ответить на клиенте, и там на неё ответить нельзя. Дайте VPN-клиентам собственный пул адресов и читайте адрес источника на сервере; это единственное доказательство, которое переживает смену туннеля пользователем посреди сеанса.
Порядок проверки, исключающий ложные срабатывания
Старый пример кода, всё ещё находящийся в обращении, вызывает getNetworkInfo(ConnectivityManager.TYPE_VPN). Этот вызов устарел, и в нескольких версиях он возвращал null даже при подключённом VPN, поэтому многие скопированные фрагменты выглядят сломанными на работающем устройстве. Проверки возможностей обходят эту ловушку и ведут себя одинаково начиная с Android 5.0. Если вы поддерживаете кодовую базу, которая всё ещё его вызывает, замените вызов, а не оборачивайте его в запасной вариант, потому что именно запасной вариант и дал неверный ответ.
Одно сокращение заслуживает предостережения. Сравнение публичного адреса, который сообщает внешний сервис, с адресом выхода VPN что-то доказывает о пути, но добавляет сторонний запрос в каждую проверку, зависит от доступности этого сервиса и может быть закэшировано или заблокировано. Оставьте это ручной диагностикой и никогда не делайте проверкой состояния внутри приложения.
Выполняйте проверки в этом порядке и остановитесь на первой, которая расходится с сообщением пользователя: список транспортов из shell, возможности активной сети, события callback при включении и выключении туннеля, затем адрес источника на стороне сервера. Если все четыре совпадают, а приложение всё равно ведёт себя неправильно, ошибка в вашей собственной машине состояний, а не в представлении Android о сети.