Проверката, която повечето приложения за 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, сочи директно към маршрутизация или към списък с изключения, а това е поправка, с която някой може да работи.

Подобни статии