Een VPN op Android detecteren: waarom tun0 en ping misleiden
Een interfacenaam is een conventie; een ping bewijst dat één host bereikbaar was. Vraag NetworkCapabilities wat een netwerk verklaart, en reageer dan op callbacks.
De controle die de meeste Android-apps gebruiken om een VPN te detecteren is een interfacenaam, en het is nooit een VPN-controle geweest. Hij meldt dat er een of andere interface bestaat met een string die de ontwikkelaar verwachtte, wat op het ene apparaat OpenVPN betekent en op het andere iets volstrekt ongerelateerds. Een ping is hetzelfde soort bewijs: het bewijst dat één bestemming antwoordde, niet dat een tunnel het pakket vervoerde. Wat het platform werkelijk blootlegt, is een verzameling netwerken en de transports die elk daarvan verklaart, en dat is een andere vraag met een ander antwoord.
Mythe: als tun0 bestaat, is er een VPN actief
Android-VPN-apps zijn VpnService-implementaties die het systeem een tun-bestandsdescriptor geven. De interfacenaam komt van wie hem heeft gemaakt: OpenVPN produceert meestal tun0, PPTP en L2TP produceren ppp0, WireGuard noemt zijn interface naar de tunnel, en een IPsec-tunnel heeft misschien helemaal geen conventionele naam. Het lezen van /proc/net/dev of het opsommen van NetworkInterface-objecten zoekt dus naar een conventie, niet naar een feit.
Beide foutrichtingen zijn levend. Een apparaat kan tun0 hebben voor een lokale tunnel die niets met de VPN van de gebruiker te maken heeft, en een volledig gezonde WireGuard-tunnel kan onzichtbaar zijn voor een controle die alleen de string tun0 kent. Valse positieven en valse negatieven in dezelfde functie maken hem onbruikbaar als diagnose, want een supportticket dat zegt «geen VPN gedetecteerd» is in geen enkele richting betrouwbaar.
De toegang is ook smaller geworden. Nieuwere Android-versies beperken wat een gewone app uit /proc mag lezen, zodat een op naam gebaseerde controle die op Android 8 werkte, op een later apparaat een lege lijst kan teruggeven zonder enige codewijziging. Als de controle stilzwijgend open faalt, concludeert de app dat er geen VPN is op hardware waar de tunnel in orde is.
Mythe: een geslaagde ping betekent dat de tunnel verkeer vervoert
Een ping naar een interne host slaagt zodra er een route naar die host bestaat. Met split tunnelling kan de host via Wi-Fi bereikbaar zijn terwijl de tunnel actief is en iets heel anders doet. Met een per-app-VPN kan de app die pingt van de tunnel zijn uitgesloten, zodat de ICMP-echo erbuiten vertrekt en terugkomt, wat het tegenovergestelde bewijst van wat de controle aanneemt.
ICMP en TCP worden ook apart gefilterd. Een tunnel die echo-verzoeken toestaat maar de TCP-poort laat vallen die een dienst nodig heeft, slaagt voor de ping-test en faalt in de applicatie, en dat is een firewallregel aan de andere kant, geen eigenschap van het apparaat. De enige eerlijke lezing van een geslaagde ping is dat deze bestemming dit protocol op dit moment beantwoordde.
Wat het platform werkelijk rapporteert
Vanaf API 21 retourneert ConnectivityManager.getAllNetworks() elk netwerk dat het systeem kent, en getNetworkCapabilities(network) rapporteert wat dat netwerk verklaart. Een VPN-netwerk draagt TRANSPORT_VPN. Door alle netwerken lopen beantwoordt één vraag: is er op dit moment een VPN actief op dit apparaat?
Een tweede, smallere vraag vereist een andere aanroep. getActiveNetwork() retourneert het netwerk dat het verkeer van de app nu zou gebruiken, en dezelfde mogelijkheid daarop controleren beantwoordt: zou mijn verkeer via een VPN gaan? De twee kunnen het oneens zijn, en die onenigheid is informatief. Een VPN die actief is maar niet de huidige, wijst op per-app-routing of een split-tunnel-configuratie, niet op een ontbrekende tunnel.
Network active = cm.getActiveNetwork();
NetworkCapabilities caps = cm.getNetworkCapabilities(active);
boolean viaVpn = caps != null
&& caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN);
Beveilig beide aanroepen. Mogelijkheden kunnen null zijn voor een netwerk dat tussen opsomming en navraag verdween, en het actieve netwerk kan null zijn tijdens een overdracht. NET_CAPABILITY_NOT_VPN is nuttig in de andere richting, om te bevestigen dat een netwerk beslist geen tunnel is in plaats van alleen de VPN-transportvlag te missen.
NetworkInfo was dezelfde mythe met een API
ConnectivityManager.TYPE_VPN met NetworkInfo is de tweede generatie van deze controle, en hij is om een reden verouderd. Op moderne versies kan het object dat voor een VPN wordt teruggegeven null zijn terwijl de tunnel verbonden is, zodat code die null als «geen VPN» behandelt stilzwijgend een vals negatief rapporteert op de apparaten waar het antwoord het meest uitmaakt.
De broadcast die er gewoonlijk bij hoort is niet beter. CONNECTIVITY_CHANGE heeft een gedocumenteerde geschiedenis van gemiste VPN-overgangen, inclusief builds waarin hij alleen afging wanneer een VPN verbond of loskoppelde in plaats van bij elke netwerkwijziging. Een luisteraar die daarop is gebouwd, produceert een toestandsvariabele die van de kijk van het platform wegdrijft tot de volgende volledige herverbinding.
Reageer op callbacks in plaats van te pollen
Registreer een NetworkRequest die TRANSPORT_VPN vereist en NET_CAPABILITY_NOT_VPN verwijdert, en roep dan registerNetworkCallback aan met dat verzoek. Dan vertellen onAvailable en onLost je wanneer de verklaring verandert, en dat is de gebeurtenis die je werkelijk interesseert. Meld de callback af wanneer de component stopt, anders houdt de app een verwijzing naar een dood scherm en blijft de callback vuren.
NetworkRequest req = new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build();
cm.registerNetworkCallback(req, callback);
Dit vereist ACCESS_NETWORK_STATE, een normale toestemming: geen root, geen dialoog voor gebruikersinstemming, geen VPN-toestemming. Pollen op een timer is het alternatief waar mensen op terugvallen, en het vreet batterij terwijl het nog steeds een overgang korter dan het interval mist, en dat is precies de overgang die een gebruiker meldt.
Wat het antwoord je nog steeds niet kan vertellen
Geen van deze API's rapporteert of de tunnel gezond is, op welke server hij eindigt, of het verkeer van een bepaalde app erin zit. Een VPN-netwerk kan bestaan zonder bruikbare routes, en een per-app-tunnel kan de app uitsluiten die de vraag stelt.
Leg dus vast wat de controle zag, niet alleen de boolean. Log de netwerkidentificatie, de verklaarde transports en of dat netwerk actief was. Een rapport dat zegt «geen VPN gedetecteerd» is onbruikbaar, terwijl een dat zegt dat een netwerk TRANSPORT_VPN verklaarde maar getActiveNetwork() Wi-Fi teruggaf, wijst direct op routing of op een uitsluitingslijst, en dat is een correctie waar iemand mee aan de slag kan.