At opdage en VPN på Android: hvorfor tun0 og ping vildleder
Et interfacenavn er en konvention; en ping beviser, at én vært var nåelig. Spørg NetworkCapabilities, hvad et netværk erklærer, og reagér så på callbacks.
Den kontrol, de fleste Android-apps bruger til at opdage en VPN, er et interfacenavn, og det har aldrig været en VPN-kontrol. Den rapporterer, at der findes et eller andet interface med en streng, som udvikleren forventede, hvilket på én enhed betyder OpenVPN og på en anden betyder noget helt urelateret. En ping er samme slags bevis: den beviser, at én destination svarede, ikke at en tunnel bar pakken. Det, platformen faktisk udstiller, er et sæt netværk og de transports, som hvert af dem erklærer, og det er et andet spørgsmål med et andet svar.
Myte: hvis tun0 findes, er en VPN oppe
Android VPN-apps er VpnService-implementeringer, der giver systemet en tun-fildeskriptor. Interfacenavnet kommer fra den, der oprettede det: OpenVPN producerer normalt tun0, PPTP og L2TP producerer ppp0, WireGuard navngiver sit interface efter tunnelen, og en IPsec-tunnel har måske slet ikke et konventionelt navn. At læse /proc/net/dev eller opregne NetworkInterface-objekter søger derfor efter en konvention, ikke efter et faktum.
Begge fejlretninger er levende. En enhed kan have tun0 til en lokal tunnel, der intet har at gøre med brugerens VPN, og en fuldstændig sund WireGuard-tunnel kan være usynlig for en kontrol, der kun kender strengen tun0. Falske positiver og falske negativer i samme funktion gør den ubrugelig som diagnostik, fordi en supportbillet, der siger „ingen VPN fundet“, ikke kan stoles på i nogen retning.
Adgangen er også blevet indsnævret. Nyere Android-udgivelser begrænser, hvad en almindelig app kan læse fra /proc, så en navnebaseret kontrol, der virkede på Android 8, kan returnere en tom liste på en senere enhed uden nogen kodeændring. Når kontrollen stiltiende fejler åbent, konkluderer appen, at der ikke er nogen VPN på hardware, hvor tunnelen er fin.
Myte: en vellykket ping betyder, at tunnelen bærer trafik
En ping til en intern vært lykkes, når der findes en rute til den vært. Med delt tunnelling kan værten være nåelig over Wi-Fi, mens tunnelen er oppe og laver noget helt andet. Med en per-app VPN kan den app, der pinger, være udelukket fra tunnelen, så ICMP-ekkoet forlader den udenfor og kommer tilbage, hvilket beviser det modsatte af, hvad kontrollen antager.
ICMP og TCP filtreres også separat. En tunnel, der tillader ekkoanmodninger, men dropper den TCP-port, en tjeneste har brug for, vil bestå ping-testen og fejle i applikationen, og det er en firewallregel på den anden side snarere end en egenskab ved enheden. Den eneste ærlige læsning af en vellykket ping er, at denne destination svarede denne protokol i dette øjeblik.
Hvad platformen faktisk rapporterer
Fra API 21 og frem returnerer ConnectivityManager.getAllNetworks() hvert netværk, systemet kender, og getNetworkCapabilities(network) rapporterer, hvad det netværk erklærer. Et VPN-netværk bærer TRANSPORT_VPN. At løbe alle netværk igennem besvarer ét spørgsmål: er nogen VPN i øjeblikket oppe på denne enhed?
Et andet, snævrere spørgsmål kræver et andet kald. getActiveNetwork() returnerer det netværk, som appens trafik ville bruge lige nu, og at kontrollere den samme kapabilitet på det besvarer: ville min trafik gå gennem en VPN? De to kan være uenige, og uenigheden er informativ. En VPN, der er oppe, men ikke aktiv, peger på per-app-routing eller en delt tunnel-konfiguration, ikke på en manglende tunnel.
Network active = cm.getActiveNetwork();
NetworkCapabilities caps = cm.getNetworkCapabilities(active);
boolean viaVpn = caps != null
&& caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN);
Beskyt begge kald. Kapabiliteter kan være null for et netværk, der forsvandt mellem opregning og forespørgsel, og det aktive netværk kan være null under en overdragelse. NET_CAPABILITY_NOT_VPN er nyttig i den anden retning, til at bekræfte, at et netværk bestemt ikke er en tunnel, snarere end blot at mangle VPN-transportflaget.
NetworkInfo var den samme myte med en API
ConnectivityManager.TYPE_VPN med NetworkInfo er anden generation af denne kontrol, og den er forældet af en grund. På moderne udgivelser kan det objekt, der returneres for en VPN, være null, mens tunnelen er forbundet, så kode, der behandler null som „ingen VPN“, stiltiende rapporterer en falsk negativ på de enheder, hvor svaret betyder mest.
Den broadcast, der normalt ledsager den, er ikke bedre. CONNECTIVITY_CHANGE har en dokumenteret historie med at overse VPN-overgange, inklusive builds, hvor den kun udløstes, når en VPN blev forbundet eller afbrudt, i stedet for ved hver netværksændring. En lytter bygget på den producerer en tilstandsvariabel, der driver væk fra platformens syn, indtil den næste fulde genopkobling.
Reagér på callbacks i stedet for at polle
Registrér en NetworkRequest, der kræver TRANSPORT_VPN og fjerner NET_CAPABILITY_NOT_VPN, og kald derefter registerNetworkCallback med den anmodning. onAvailable og onLost fortæller dig så, når erklæringen ændres, og det er den begivenhed, du faktisk interesserer dig for. Afregistrér callbacken, når komponenten stopper, ellers holder appen en reference til en død skærm, og callbacken bliver ved med at udløses.
NetworkRequest req = new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build();
cm.registerNetworkCallback(req, callback);
Det kræver ACCESS_NETWORK_STATE, som er en normal tilladelse: ingen root, ingen brugerens samtykkedialog, ingen VPN-tilladelse. Polling på en timer er det alternativ, folk falder tilbage på, og det brænder batteri, mens det stadig misser en overgang, der er kortere end intervallet, hvilket er præcis den overgang, en bruger rapporterer.
Hvad svaret stadig ikke kan fortælle dig
Ingen af disse API'er rapporterer, om tunnelen er sund, hvilken server den afsluttes på, eller om en bestemt apps trafik er inde i den. Et VPN-netværk kan eksistere uden brugbare ruter, og en per-app-tunnel kan udelukke den app, der stiller spørgsmålet.
Så registrér, hvad kontrollen så, ikke kun boolean. Log netværksidentifikatoren, de erklærede transports og om det netværk var aktivt. En rapport, der siger „ingen VPN fundet“, er ubrugelig, mens en, der siger, at et netværk erklærede TRANSPORT_VPN, men getActiveNetwork() returnerede Wi-Fi, peger direkte på routing eller på en udelukkelsesliste, og det er en rettelse, nogen kan handle på.