Перевірка, яку більшість застосунків Android використовують для виявлення VPN, — це ім'я інтерфейсу, і вона ніколи не була перевіркою VPN. Вона повідомляє, що існує якийсь інтерфейс із рядком, який очікував розробник, що на одному пристрої означає OpenVPN, а на іншому — щось цілком не пов'язане. ping — це той самий вид доказу: він доводить, що одна адреса відповіла, а не те, що тунель переніс пакет. Те, що платформа справді надає, — це набір мереж і транспорти, які кожна з них заявляє, і це інше питання з іншою відповіддю.

Міф: якщо існує tun0, VPN увімкнено

VPN-застосунки для Android — це реалізації VpnService, які передають системі дескриптор файлу tun. Ім'я інтерфейсу походить від того, хто його створив: OpenVPN зазвичай створює tun0, PPTP і L2TP створюють ppp0, WireGuard називає інтерфейс за тунелем, а тунель IPsec може взагалі не мати звичайного імені. Тому читання /proc/net/dev або перелік об'єктів NetworkInterface шукає домовленість, а не факт.

Обидва напрямки помилки живі. На пристрої може бути tun0 для локального тунелю, який не має нічого спільного з VPN користувача, а цілком здоровий тунель WireGuard може бути невидимим для перевірки, яка знає лише рядок tun0. Хибні спрацьовування й пропуски в одній функції роблять її непридатною як діагностику, бо зверненню до підтримки з текстом «VPN не виявлено» не можна вірити в жодному напрямку.

Доступ також звузився. Новіші версії Android обмежують, що звичайний застосунок може читати з /proc, тому перевірка за іменем, яка працювала на Android 8, може повернути порожній список на пізнішому пристрої без жодних змін у коді. Коли перевірка тихо відмовляє у відкритий бік, застосунок робить висновок, що на обладнанні, де тунель у порядку, VPN немає.

Міф: успішний ping означає, що тунель передає трафік

ping до внутрішнього хоста успішний щоразу, коли існує маршрут до цього хоста. За роздільного тунелю хост може бути доступним через Wi-Fi, поки тунель увімкнений і робить щось зовсім інше. За VPN для окремих застосунків застосунок, що виконує ping, може бути виключений із тунелю, тому відлуння ICMP виходить за його межі й повертається, що доводить протилежне тому, що припускає перевірка.

ICMP і TCP також фільтруються окремо. Тунель, який дозволяє запити відлуння, але відкидає TCP-порт, потрібний службі, пройде тест ping і зазнає невдачі в застосунку, і це правило брандмауера з іншого боку, а не властивість пристрою. Єдине чесне прочитання успішного ping — що ця адреса відповіла на цей протокол у цей момент.

Що платформа справді повідомляє

Починаючи з API 21, ConnectivityManager.getAllNetworks() повертає кожну мережу, відому системі, а getNetworkCapabilities(network) повідомляє, що заявляє ця мережа. VPN-мережа несе TRANSPORT_VPN. Перебір усіх мереж відповідає на одне питання: чи є зараз на цьому пристрої якийсь активний VPN?

Друге, вужче питання потребує іншого виклику. getActiveNetwork() повертає мережу, яку трафік застосунку використав би просто зараз, і перевірка тієї самої можливості на ній відповідає: чи піде мій трафік через VPN? Обидва можуть розходитися, і це розходження інформативне. VPN, який увімкнений, але не є поточним, вказує на маршрутизацію для окремих застосунків або конфігурацію роздільного тунелю, а не на відсутній тунель.

Network active = cm.getActiveNetwork();
NetworkCapabilities caps = cm.getNetworkCapabilities(active);
boolean viaVpn = caps != null
    && caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN);

Захищайте обидва виклики. Можливості можуть бути null для мережі, що зникла між переліком і запитом, а активна мережа може бути null під час передавання. NET_CAPABILITY_NOT_VPN корисна у зворотному напрямку — щоб підтвердити, що мережа точно не є тунелем, а не просто їй бракує прапорця транспорту VPN.

NetworkInfo був тим самим міфом з API

ConnectivityManager.TYPE_VPN з NetworkInfo — це друге покоління цієї перевірки, і воно застаріле недарма. У сучасних версіях об'єкт, що повертається для VPN, може бути null, поки тунель підключений, тому код, який трактує null як «немає VPN», тихо повідомляє хибний негативний результат на пристроях, де відповідь найважливіша.

Супровідна трансляція теж не краща. CONNECTIVITY_CHANGE має документовану історію пропусків переходів VPN, включно зі збірками, де вона спрацьовувала лише при підключенні чи відключенні VPN, а не при кожній зміні мережі. Слухач, побудований на ній, створює змінну стану, яка відходить від бачення платформи до наступного повного перепідключення.

Реагуйте на зворотні виклики замість опитування

Зареєструйте NetworkRequest, який вимагає TRANSPORT_VPN і видаляє NET_CAPABILITY_NOT_VPN, потім викличте registerNetworkCallback із цим запитом. Тоді onAvailable і onLost повідомлять, коли заява змінюється, і це саме та подія, яка вас справді цікавить. Скасуйте реєстрацію зворотного виклику, коли компонент зупиняється, інакше застосунок тримає посилання на мертвий екран, а зворотний виклик продовжує спрацьовувати.

NetworkRequest req = new NetworkRequest.Builder()
    .addTransportType(NetworkCapabilities.TRANSPORT_VPN)
    .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
    .build();
cm.registerNetworkCallback(req, callback);

Це потребує ACCESS_NETWORK_STATE — звичайного дозволу: без root, без діалогу згоди користувача, без дозволу VPN. Опитування за таймером — це альтернатива, до якої вдаються люди, і воно витрачає батарею, але все одно пропускає перехід, коротший за інтервал, а саме такий перехід користувач і описує.

Чого відповідь усе ще не може сказати

Жоден із цих API не повідомляє, чи здоровий тунель, на якому сервері він завершується і чи трафік певного застосунку всередині нього. VPN-мережа може існувати без придатних маршрутів, а тунель для окремих застосунків може виключити застосунок, що ставить питання.

Тому записуйте те, що побачила перевірка, а не лише логічне значення. Записуйте ідентифікатор мережі, заявлені транспорти й те, чи була ця мережа активною. Звіт, у якому сказано «VPN не виявлено», непридатний, а звіт, у якому сказано, що мережа заявила TRANSPORT_VPN, але getActiveNetwork() повернув Wi-Fi, вказує прямо на маршрутизацію або на список виключень, і це виправлення, над яким хтось може працювати.

Схожі статті