Провера коју већина 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 указује директно на рутирање или на листу искључења, и то је исправка на којој неко може да ради.

Повезани чланци