Provjera koju većina Android aplikacija koristi za otkrivanje VPN-a jest naziv sučelja i nikada nije bila VPN provjera. Ona javlja da postoji neko sučelje s nizom koji je razvijatelj očekivao, što na jednom uređaju znači OpenVPN, a na drugom nešto posve nepovezano. Ping je ista vrsta dokaza: dokazuje da je jedno odredište odgovorilo, a ne da je tunel prenio paket. Ono što platforma zapravo izlaže jest skup mreža i transporta koje svaka od njih deklarira, a to je drugo pitanje s drugim odgovorom.

Mit: ako tun0 postoji, VPN je aktivan

Android VPN aplikacije su implementacije VpnService koje sustavu predaju datotečni deskriptor tun. Naziv sučelja dolazi od onoga tko ga je stvorio: OpenVPN obično stvara tun0, PPTP i L2TP stvaraju ppp0, WireGuard naziva sučelje po tunelu, a IPsec tunel možda uopće neće imati uobičajen naziv. Stoga čitanje /proc/net/dev ili nabrajanje objekata NetworkInterface traži konvenciju, a ne činjenicu.

Oba smjera pogreške su živa. Uređaj može imati tun0 za lokalni tunel koji nema veze s korisnikovim VPN-om, a posve zdrav WireGuard tunel može biti nevidljiv provjeri koja poznaje samo niz tun0. Lažno pozitivni i lažno negativni rezultati u istoj funkciji čine je neupotrebljivom kao dijagnostiku, jer zahtjevu za podršku koji kaže „VPN nije otkriven“ ne može se vjerovati ni u jednom smjeru.

Pristup se također suzio. Novije Android verzije ograničavaju što obična aplikacija može čitati iz /proc, pa provjera temeljena na nazivu koja je radila na Androidu 8 može vratiti prazan popis na kasnijem uređaju bez ikakve promjene koda. Kad provjera tiho zakaže u otvorenom smjeru, aplikacija zaključi da nema VPN-a na hardveru na kojem je tunel ispravan.

Mit: uspješan ping znači da tunel prenosi promet

Ping na unutarnji host uspijeva kad god postoji ruta do tog hosta. Uz podijeljeni tunel host može biti dostupan preko Wi-Fi-ja dok je tunel aktivan i radi nešto posve drugo. Uz VPN po aplikaciji aplikacija koja izvodi ping može biti isključena iz tunela, pa ICMP odjek izlazi izvan njega i vraća se, što dokazuje suprotno od onoga što provjera pretpostavlja.

ICMP i TCP filtriraju se odvojeno. Tunel koji dopušta odjek zahtjeva, ali odbacuje TCP port koji usluga treba, proći će ping test i pasti u aplikaciji, a to je pravilo vatrozida na drugoj strani, a ne svojstvo uređaja. Jedino iskreno čitanje uspješnog pinga jest da je to odredište odgovorilo na taj protokol u tom trenutku.

Što platforma zapravo izvješćuje

Od API 21 nadalje ConnectivityManager.getAllNetworks() vraća svaku mrežu koju sustav zna, a getNetworkCapabilities(network) izvješćuje što ta mreža deklarira. VPN mreža nosi TRANSPORT_VPN. Petlja po svim mrežama odgovara na jedno pitanje: je li na ovom uređaju trenutačno aktivna ikakva VPN?

Drugo, uže pitanje zahtijeva drugi poziv. getActiveNetwork() vraća mrežu koju bi promet aplikacije sada koristio, a provjera iste sposobnosti na njoj odgovara: bi li moj promet išao kroz VPN? Njih dvoje mogu se ne slagati, a to neslaganje je informativno. VPN koji je aktivan, ali nije trenutačni, upućuje na usmjeravanje po aplikaciji ili na konfiguraciju podijeljenog tunela, a ne na tunel koji nedostaje.

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

Čuvajte oba poziva. Sposobnosti mogu biti null za mrežu koja je nestala između nabrajanja i upita, a aktivna mreža može biti null tijekom prijelaza. NET_CAPABILITY_NOT_VPN korisna je u drugom smjeru — da potvrdite da mreža zasigurno nije tunel, a ne samo da joj nedostaje oznaka VPN transporta.

NetworkInfo bio je isti mit s API-jem

ConnectivityManager.TYPE_VPN s NetworkInfo drugi je naraštaj te provjere i zastario je s razlogom. U modernim verzijama objekt vraćen za VPN može biti null dok je tunel spojen, pa kod koji null tretira kao „nema VPN-a“ tiho izvješćuje lažno negativan rezultat na uređajima gdje je odgovor najvažniji.

Ni emitiranje koje ga obično prati nije bolje. CONNECTIVITY_CHANGE ima dokumentiranu povijest propuštanja VPN prijelaza, uključujući gradnje u kojima se aktiviralo samo pri spajanju ili odspajanju VPN-a, a ne pri svakoj promjeni mreže. Slušatelj izgrađen na njemu stvara varijablu stanja koja se odmiče od viđenja platforme do sljedećeg punog ponovnog spajanja.

Reagirajte na povratne pozive umjesto anketiranja

Registrirajte NetworkRequest koji zahtijeva TRANSPORT_VPN i uklanja NET_CAPABILITY_NOT_VPN, zatim pozovite registerNetworkCallback s tim zahtjevom. onAvailable i onLost tada vam kažu kad se deklaracija promijeni, a to je događaj koji vas doista zanima. Odjavite povratni poziv kad komponenta stane, inače aplikacija drži referencu na mrtav zaslon i povratni poziv nastavlja se aktivirati.

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

To zahtijeva ACCESS_NETWORK_STATE, što je uobičajena dozvola: bez roota, bez dijaloga za korisnikovu suglasnost, bez VPN dozvole. Anketiranje na timer je alternativa kojoj se ljudi vraćaju, a ono troši bateriju i pritom propušta prijelaz kraći od intervala, a to je upravo prijelaz koji korisnik prijavi.

Što vam odgovor još uvijek ne može reći

Nijedan od tih API-ja ne izvješćuje je li tunel zdrav, na kojem poslužitelju završava i je li promet određene aplikacije unutar njega. VPN mreža može postojati bez upotrebljivih ruta, a tunel po aplikaciji može isključiti aplikaciju koja postavlja pitanje.

Zato zabilježite što je provjera vidjela, ne samo boolean. Zabilježite identifikator mreže, deklarirane transporte i je li ta mreža bila aktivna. Izvješće koje kaže „VPN nije otkriven“ neupotrebljivo je, dok ono koje kaže da je mreža deklarirala TRANSPORT_VPN, ali je getActiveNetwork() vratio Wi-Fi, upućuje izravno na usmjeravanje ili na popis isključenja, a to je ispravak na kojem se može raditi.

Povezani članci