Att upptäcka en VPN på Android: varför tun0 och ping vilseleder
Ett gränssnittsnamn är en konvention; en ping bevisar att en värd var nåbar. Fråga NetworkCapabilities vad ett nätverk deklarerar och reagera sedan på callbacks.
Kontrollen som de flesta Android-appar använder för att upptäcka en VPN är ett gränssnittsnamn, och det har aldrig varit en VPN-kontroll. Den rapporterar att det finns något gränssnitt med en sträng som utvecklaren förväntade sig, vilket på en enhet betyder OpenVPN och på en annan betyder något helt orelaterat. En ping är samma slags bevis: den bevisar att en destination svarade, inte att en tunnel bar paketet. Det plattformen faktiskt exponerar är en uppsättning nätverk och de transporter som var och en deklarerar, och det är en annan fråga med ett annat svar.
Myt: om tun0 finns är en VPN igång
Android-VPN-appar är VpnService-implementeringar som ger systemet en tun-fildeskriptor. Gränssnittsnamnet kommer från den som skapade det: OpenVPN producerar vanligtvis tun0, PPTP och L2TP producerar ppp0, WireGuard namnger sitt gränssnitt efter tunneln, och en IPsec-tunnel kanske inte har något konventionellt namn alls. Att läsa /proc/net/dev eller räkna upp NetworkInterface-objekt söker därför efter en konvention, inte efter ett faktum.
Båda felriktningarna är levande. En enhet kan ha tun0 för en lokal tunnel som inte har något med användarens VPN att göra, och en helt frisk WireGuard-tunnel kan vara osynlig för en kontroll som bara känner till strängen tun0. Falska positiva och falska negativa i samma funktion gör den oanvändbar som diagnostik, eftersom en supportbiljett som säger ”ingen VPN hittades” inte går att lita på i någon riktning.
Åtkomsten har också minskat. Nyare Android-versioner begränsar vad en vanlig app får läsa från /proc, så en namnkontroll som fungerade på Android 8 kan returnera en tom lista på en senare enhet utan någon kodändring. När kontrollen tyst misslyckas öppet drar appen slutsatsen att det inte finns någon VPN på hårdvara där tunneln är i ordning.
Myt: en lyckad ping betyder att tunneln bär trafik
En ping till en intern värd lyckas när det finns en rutt till den värden. Med delad tunnling kan värden vara nåbar via Wi-Fi medan tunneln är igång och gör något helt annat. Med en per-app-VPN kan appen som pingar vara utesluten från tunneln, så ICMP-ekot går utanför den och kommer tillbaka, vilket bevisar motsatsen till vad kontrollen antar.
ICMP och TCP filtreras också separat. En tunnel som tillåter eko-förfrågningar men släpper TCP-porten som en tjänst behöver klarar ping-testet och misslyckas i applikationen, och det är en brandväggsregel på andra sidan, inte en egenskap hos enheten. Den enda ärliga läsningen av en lyckad ping är att denna destination svarade på detta protokoll i detta ögonblick.
Vad plattformen faktiskt rapporterar
Från API 21 returnerar ConnectivityManager.getAllNetworks() varje nätverk systemet känner till, och getNetworkCapabilities(network) rapporterar vad det nätverket deklarerar. Ett VPN-nätverk bär TRANSPORT_VPN. Att gå igenom alla nätverk svarar på en fråga: är någon VPN igång på den här enheten just nu?
En andra, snävare fråga kräver ett annat anrop. getActiveNetwork() returnerar det nätverk som appens trafik skulle använda just nu, och att kontrollera samma förmåga på det svarar: skulle min trafik gå genom en VPN? De två kan vara oense, och oenigheten är informativ. En VPN som är igång men inte aktuell pekar på per-app-routing eller en delad tunnel-konfiguration, inte på en saknad tunnel.
Network active = cm.getActiveNetwork();
NetworkCapabilities caps = cm.getNetworkCapabilities(active);
boolean viaVpn = caps != null
&& caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN);
Skydda båda anropen. Förmågor kan vara null för ett nätverk som försvann mellan uppräkning och förfrågan, och det aktiva nätverket kan vara null under en överlämning. NET_CAPABILITY_NOT_VPN är användbart i den andra riktningen, för att bekräfta att ett nätverk definitivt inte är en tunnel i stället för att bara sakna VPN-transportflaggan.
NetworkInfo var samma myt med ett API
ConnectivityManager.TYPE_VPN med NetworkInfo är andra generationen av denna kontroll, och den är föråldrad av en anledning. På moderna versioner kan objektet som returneras för en VPN vara null medan tunneln är ansluten, så kod som behandlar null som ”ingen VPN” rapporterar tyst en falsk negativ på de enheter där svaret betyder mest.
Inte heller den sändning som vanligtvis följer med den är bättre. CONNECTIVITY_CHANGE har en dokumenterad historia av att missa VPN-övergångar, inklusive byggen där den bara utlöstes när en VPN anslöts eller kopplades från i stället för vid varje nätverksändring. En lyssnare byggd på den producerar en tillståndsvariabel som driver bort från plattformens syn till nästa fullständiga återanslutning.
Reagera på callbacks i stället för att polla
Registrera en NetworkRequest som kräver TRANSPORT_VPN och tar bort NET_CAPABILITY_NOT_VPN, och anropa sedan registerNetworkCallback med den begäran. Då berättar onAvailable och onLost när deklarationen ändras, och det är händelsen du faktiskt bryr dig om. Avregistrera callbacken när komponenten stoppar, annars håller appen en referens till en död skärm och callbacken fortsätter att utlösas.
NetworkRequest req = new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build();
cm.registerNetworkCallback(req, callback);
Detta kräver ACCESS_NETWORK_STATE, vilket är en normal behörighet: ingen root, ingen dialog för användarens samtycke, ingen VPN-behörighet. Pollning med en timer är alternativet folk faller tillbaka på, och det bränner batteri medan det fortfarande missar en övergång kortare än intervallet, och det är just den övergången en användare rapporterar.
Vad svaret fortfarande inte kan berätta
Ingen av dessa API:er rapporterar om tunneln är frisk, på vilken server den avslutas eller om trafiken för en viss app finns inuti den. Ett VPN-nätverk kan existera utan användbara rutter, och en per-app-tunnel kan utesluta appen som ställer frågan.
Så registrera vad kontrollen såg, inte bara boolean. Logga nätverksidentifieraren, de deklarerade transporterna och om det nätverket var aktivt. En rapport som säger ”ingen VPN hittades” är oanvändbar, medan en som säger att ett nätverk deklarerade TRANSPORT_VPN men getActiveNetwork() returnerade Wi-Fi pekar direkt på routing eller på en undantagslista, och det är en rättelse som någon kan arbeta med.