Як перевірити в коді, чи підключено 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 про мережу.