Il controllo che la maggior parte delle app Android usa per rilevare una VPN è un nome di interfaccia, e non è mai stato un controllo VPN. Riporta che esiste un'interfaccia con la stringa attesa dallo sviluppatore, che su un dispositivo significa OpenVPN e su un altro qualcosa di completamente estraneo. Un ping è lo stesso tipo di prova: dimostra che una destinazione ha risposto, non che un tunnel ha trasportato il pacchetto. Ciò che la piattaforma espone realmente è un insieme di reti e i transport che ciascuna dichiara, e questa è un'altra domanda con un'altra risposta.

Mito: se tun0 esiste, una VPN è attiva

Le app VPN Android sono implementazioni di VpnService che consegnano al sistema un descrittore di file tun. Il nome dell'interfaccia deriva da chi l'ha creata: OpenVPN produce di solito tun0, PPTP e L2TP producono ppp0, WireGuard nomina l'interfaccia dal tunnel, e un tunnel IPsec può non presentare alcun nome convenzionale. Leggere /proc/net/dev o enumerare oggetti NetworkInterface cerca quindi una convenzione, non un fatto.

Entrambe le direzioni d'errore sono vive. Un dispositivo può avere tun0 per un tunnel locale che non ha nulla a che fare con la VPN dell'utente, e un tunnel WireGuard perfettamente sano può essere invisibile a un controllo che conosce solo la stringa tun0. Falsi positivi e falsi negativi nella stessa funzione la rendono inutilizzabile come diagnostica, perché un ticket di assistenza che dice «nessuna VPN rilevata» non è affidabile in nessuna direzione.

Anche l'accesso si è ristretto. Le versioni più recenti di Android limitano ciò che un'app normale può leggere da /proc, quindi un controllo basato sul nome che funzionava su Android 8 può restituire un elenco vuoto su un dispositivo successivo senza alcuna modifica al codice. Quando il controllo fallisce silenziosamente verso il lato aperto, l'app conclude che non c'è VPN su un hardware dove il tunnel è a posto.

Mito: un ping riuscito significa che il tunnel trasporta traffico

Un ping verso un host interno riesce ogni volta che esiste una rotta verso quell'host. Con il tunnel diviso, l'host può essere raggiungibile via Wi-Fi mentre il tunnel è attivo e fa qualcosa di completamente diverso. Con una VPN per app, l'app che esegue il ping può essere esclusa dal tunnel, così l'eco ICMP esce fuori e ritorna, il che dimostra l'opposto di ciò che il controllo presuppone.

Anche ICMP e TCP sono filtrati separatamente. Un tunnel che consente richieste di eco ma scarta la porta TCP necessaria a un servizio supererà il test del ping e fallirà nell'applicazione, e questa è una regola del firewall dall'altra parte, non una proprietà del dispositivo. L'unica lettura onesta di un ping riuscito è che questa destinazione ha risposto a questo protocollo in questo momento.

Ciò che la piattaforma riporta realmente

Dall'API 21, ConnectivityManager.getAllNetworks() restituisce ogni rete che il sistema conosce, e getNetworkCapabilities(network) riporta ciò che quella rete dichiara. Una rete VPN porta TRANSPORT_VPN. Scorrere tutte le reti risponde a una domanda: c'è qualche VPN attiva su questo dispositivo adesso?

Una seconda domanda, più stretta, richiede un'altra chiamata. getActiveNetwork() restituisce la rete che il traffico dell'app userebbe adesso, e controllare la stessa capacità su di essa risponde: il mio traffico passerebbe attraverso una VPN? Le due possono discordare, e la discordanza è informativa. Una VPN attiva ma non corrente indica un routing per app o una configurazione a tunnel diviso, non un tunnel mancante.

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

Proteggete entrambe le chiamate. Le capacità possono essere null per una rete scomparsa tra l'elenco e la query, e la rete attiva può essere null durante un passaggio. NET_CAPABILITY_NOT_VPN è utile nella direzione opposta, per confermare che una rete non è affatto un tunnel invece di mancare solo del flag di transport VPN.

NetworkInfo era lo stesso mito con un'API

ConnectivityManager.TYPE_VPN con NetworkInfo è la seconda generazione di questo controllo, ed è deprecato per una ragione. Nelle versioni moderne l'oggetto restituito per una VPN può essere null mentre il tunnel è connesso, quindi il codice che tratta null come «nessuna VPN» riporta silenziosamente un falso negativo sui dispositivi dove la risposta conta di più.

Neanche la trasmissione che di solito lo accompagna è migliore. CONNECTIVITY_CHANGE ha una storia documentata di transizioni VPN mancate, comprese build in cui scattava solo quando una VPN si connetteva o disconnetteva invece che a ogni cambio di rete. Un listener costruito su di essa produce una variabile di stato che si allontana dalla visione della piattaforma fino alla successiva riconnessione completa.

Reagite alle callback invece di interrogare periodicamente

Registrate una NetworkRequest che richieda TRANSPORT_VPN e rimuova NET_CAPABILITY_NOT_VPN, poi chiamate registerNetworkCallback con quella richiesta. onAvailable e onLost vi dicono allora quando la dichiarazione cambia, ed è l'evento che vi interessa davvero. Annullate la registrazione della callback quando il componente si ferma, altrimenti l'app mantiene un riferimento a uno schermo morto e la callback continua a scattare.

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

Questo richiede ACCESS_NETWORK_STATE, che è un permesso normale: niente root, niente finestra di consenso dell'utente, niente permesso VPN. L'interrogazione a timer è l'alternativa a cui la gente ricorre, e consuma batteria mentre manca comunque una transizione più breve dell'intervallo, che è esattamente la transizione che un utente segnala.

Cosa la risposta non può ancora dirvi

Nessuna di queste API riporta se il tunnel è sano, su quale server termina o se il traffico di una particolare app è al suo interno. Una rete VPN può esistere senza rotte utilizzabili, e un tunnel per app può escludere l'app che pone la domanda.

Registrate quindi ciò che il controllo ha visto, non solo il booleano. Registrate l'identificatore di rete, i transport dichiarati e se quella rete era attiva. Un rapporto che dice «nessuna VPN rilevata» è inutilizzabile, mentre uno che dice che una rete ha dichiarato TRANSPORT_VPN ma getActiveNetwork() ha restituito Wi-Fi punta direttamente al routing o a una lista di esclusioni, e questa è una correzione su cui qualcuno può lavorare.

Articoli correlati