Å oppdage en VPN på Android: hvorfor tun0 og ping villeder
Et grensesnittnavn er en konvensjon; en ping beviser at én vert var nåbar. Spør NetworkCapabilities hva et nettverk erklærer, og reager deretter på callbacks.
Sjekken de fleste Android-apper bruker for å oppdage en VPN, er et grensesnittnavn, og den har aldri vært en VPN-sjekk. Den rapporterer at det finnes et eller annet grensesnitt med en streng utvikleren forventet, som på én enhet betyr OpenVPN og på en annen betyr noe helt urelatert. En ping er samme slags bevis: den beviser at én destinasjon svarte, ikke at en tunnel bar pakken. Det plattformen faktisk eksponerer, er et sett nettverk og transportene hvert av dem erklærer, og det er et annet spørsmål med et annet svar.
Myte: hvis tun0 finnes, er en VPN oppe
Android VPN-apper er VpnService-implementasjoner som gir systemet en tun-fildeskriptor. Grensesnittnavnet kommer fra den som opprettet det: OpenVPN produserer vanligvis tun0, PPTP og L2TP produserer ppp0, WireGuard navngir grensesnittet etter tunnelen, og en IPsec-tunnel har kanskje ikke noe konvensjonelt navn i det hele tatt. Å lese /proc/net/dev eller telle NetworkInterface-objekter leter derfor etter en konvensjon, ikke etter et faktum.
Begge feilretningene er levende. En enhet kan ha tun0 for en lokal tunnel som ikke har noe med brukerens VPN å gjøre, og en helt sunn WireGuard-tunnel kan være usynlig for en sjekk som bare kjenner strengen tun0. Falske positive og falske negative i samme funksjon gjør den ubrukelig som diagnostikk, fordi en støttebillett som sier «ingen VPN funnet» ikke kan stoles på i noen retning.
Tilgangen er også blitt snevret inn. Nyere Android-utgivelser begrenser hva en vanlig app kan lese fra /proc, så en navnebasert sjekk som virket på Android 8, kan returnere en tom liste på en senere enhet uten noen kodeendring. Når sjekken stille feiler åpent, konkluderer appen at det ikke finnes noen VPN på maskinvare der tunnelen er i orden.
Myte: en vellykket ping betyr at tunnelen bærer trafikk
En ping til en intern vert lykkes når det finnes en rute til den verten. Med delt tunnelling kan verten være nåbar over Wi-Fi mens tunnelen er oppe og gjør noe helt annet. Med en per-app-VPN kan appen som pinger være utelukket fra tunnelen, så ICMP-ekkoet går utenfor den og kommer tilbake, noe som beviser det motsatte av det sjekken antar.
ICMP og TCP filtreres også separat. En tunnel som tillater ekko-forespørsler men dropper TCP-porten en tjeneste trenger, vil bestå ping-testen og feile i applikasjonen, og det er en brannmurregel på den andre siden, ikke en egenskap ved enheten. Den eneste ærlige lesningen av en vellykket ping er at denne destinasjonen svarte denne protokollen i dette øyeblikket.
Hva plattformen faktisk rapporterer
Fra API 21 og utover returnerer ConnectivityManager.getAllNetworks() hvert nettverk systemet kjenner, og getNetworkCapabilities(network) rapporterer hva det nettverket erklærer. Et VPN-nettverk bærer TRANSPORT_VPN. Å gå gjennom alle nettverk svarer på ett spørsmål: er noen VPN oppe på denne enheten akkurat nå?
Et andre, snevrere spørsmål trenger et annet kall. getActiveNetwork() returnerer nettverket appens trafikk ville bruke akkurat nå, og å sjekke den samme egenskapen på det svarer: ville trafikken min gå gjennom en VPN? De to kan være uenige, og uenigheten er informativ. En VPN som er oppe men ikke aktuell, peker på per-app-ruting eller en delt tunnel-konfigurasjon, ikke på en manglende tunnel.
Network active = cm.getActiveNetwork();
NetworkCapabilities caps = cm.getNetworkCapabilities(active);
boolean viaVpn = caps != null
&& caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN);
Beskytt begge kallene. Egenskaper kan være null for et nettverk som forsvant mellom opplisting og spørring, og det aktive nettverket kan være null under en overlevering. NET_CAPABILITY_NOT_VPN er nyttig i den andre retningen, for å bekrefte at et nettverk definitivt ikke er en tunnel i stedet for bare å mangle VPN-transportflagget.
NetworkInfo var den samme myten med en API
ConnectivityManager.TYPE_VPN med NetworkInfo er andre generasjon av denne sjekken, og den er utdatert av en grunn. På moderne utgivelser kan objektet som returneres for en VPN være null mens tunnelen er tilkoblet, så kode som behandler null som «ingen VPN» stilltiende rapporterer en falsk negativ på enhetene der svaret betyr mest.
Kringkastingen som vanligvis følger den, er ikke bedre. CONNECTIVITY_CHANGE har en dokumentert historie med å overse VPN-overganger, inkludert bygg der den bare utløstes når en VPN koblet til eller fra, i stedet for ved hver nettverksendring. En lytter bygget på den produserer en tilstandsvariabel som driver bort fra plattformens syn til neste fulle gjenoppkobling.
Reager på callbacks i stedet for å polle
Registrer en NetworkRequest som krever TRANSPORT_VPN og fjerner NET_CAPABILITY_NOT_VPN, og kall deretter registerNetworkCallback med den forespørselen. Da forteller onAvailable og onLost deg når erklæringen endres, og det er hendelsen du faktisk bryr deg om. Avregistrer callbacken når komponenten stopper, ellers holder appen en referanse til en død skjerm og callbacken fortsetter å utløses.
NetworkRequest req = new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build();
cm.registerNetworkCallback(req, callback);
Dette krever ACCESS_NETWORK_STATE, som er en normal tillatelse: ingen root, ingen brukersamtykkedialog, ingen VPN-tillatelse. Polling på en timer er alternativet folk faller tilbake på, og det brenner batteri mens det fortsatt overser en overgang som er kortere enn intervallet, og det er nettopp overgangen en bruker rapporterer.
Hva svaret fortsatt ikke kan fortelle deg
Ingen av disse API-ene rapporterer om tunnelen er sunn, hvilken server den avsluttes på, eller om trafikken til en bestemt app er inne i den. Et VPN-nettverk kan eksistere uten brukbare ruter, og en per-app-tunnel kan utelukke appen som stiller spørsmålet.
Så registrer hva sjekken så, ikke bare boolean. Logg nettverksidentifikatoren, de erklærte transportene og om det nettverket var aktivt. En rapport som sier «ingen VPN funnet» er ubrukelig, mens en som sier at et nettverk erklærte TRANSPORT_VPN men getActiveNetwork() returnerte Wi-Fi, peker direkte på ruting eller på en utelukkelsesliste, og det er en rettelse noen kan handle på.