La protezione di VpnService fallisce quando un altro VPN è già attivo
Android consente una sola interfaccia VPN per utente. Eseguite i controlli prepare, establish, protect e di proprietà in ordine, poi scegliete revoca o esclusione per app.
La vostra app chiama VpnService.prepare, riceve null, costruisce un tunnel e il descrittore sembra valido, ma nulla raggiunge il server. Oppure prepare restituisce un Intent, l’utente lo approva e establish fallisce comunque. Entrambi gli esiti di solito significano la stessa cosa: un’altra app possiede già l’interfaccia VPN per questo utente, e le vostre chiamate protect stanno marcando socket che il vostro tunnel non può usare. Percorrete i controlli qui sotto in ordine e fermatevi al primo che fallisce, perché un passo successivo non può riparare uno precedente.
Controllate lo slot VPN prima di toccare protect
Android concede a ogni utente esattamente un’interfaccia VPN attiva. È una regola della piattaforma, non un permesso che la vostra app può richiedere, quindi due client che vogliono entrambi essere il tunnel non possono coesistere.
Un VPN sempre attivo impostato per un’altra app è il caso più difficile, perché il sistema mantiene quell’app connessa e rifiuta una presa di controllo. Un filtro del traffico come AdGuard, un client aziendale o un VPN consumer possono occupare lo slot mentre l’utente ha dimenticato che è installato. Chiedete all’utente di aprire Impostazioni e leggere la voce elencata sotto VPN; il nome che c’è è il proprietario con cui state competendo. Finché quella voce non viene disconnessa o l’impostazione sempre attivo non viene cancellata, il vostro VpnService non può diventare l’interfaccia.
Passo 1: leggete cosa restituisce prepare
VpnService.prepare risponde a una sola domanda: questa app può diventare il VPN? Restituisce null quando il consenso esiste già, e un Intent quando l’utente deve ancora approvarlo, che dovete lanciare con startActivityForResult. Trattate un ritorno null come permesso per continuare, non come prova che establish riuscirà.
Intent intent = VpnService.prepare(context);
if (intent != null) {
startActivityForResult(intent, REQUEST_VPN);
}Se il percorso Intent ritorna e l’utente ha rifiutato, prepare continuerà a restituire un Intent al tentativo successivo. Registrate il codice di risultato invece di ritentare in un ciclo, perché un consenso rifiutato è una decisione dell’utente, non un errore transitorio.
Passo 2: confermate che l’interfaccia appartiene alla vostra app
Dopo establish, il ParcelFileDescriptor che ricevete è la vostra prova di proprietà, e vale la pena controllarlo prima di qualsiasi lavoro sui socket. Un descrittore null o già chiuso significa che l’interfaccia non è mai stata creata, anche se non è emersa alcuna eccezione. Confrontate il nome dell’interfaccia del builder con quello che il sistema riporta per il VPN attivo, usando dumpsys su un dispositivo di test. Se differiscono, il tunnel che avete costruito non è il tunnel che trasporta il traffico.
adb shell dumpsys connectivityLa stessa discrepanza spiega un’icona connessa con traffico morto. Il sistema mostra il tunnel dell’altra app, mentre il vostro codice tiene un descrittore per un’interfaccia che non possiede.
Passo 3: chiamate protect prima di connect
Una volta che l’interfaccia esiste, i socket aperti dalla vostra app vengono instradati nel tunnel per impostazione predefinita. Il trasporto esterno non può seguire quella regola o il pacchetto incapsulato tornerebbe nell’interfaccia, perciò VpnService protect marca un socket per aggirare il VPN e usare la rete sottostante. La chiamata restituisce un boolean, e ignorarlo è il modo in cui finite con un socket che sembra sano e non trasporta nulla.
Chiamate protect prima di connect o bind, mai dopo. La protezione deve essere in vigore prima che il socket sia connesso o associato, perché un socket connesso ha già scelto la sua rotta e un protect successivo non lo sposta in modo affidabile. Se protect restituisce false, fermatevi e ricontrollate la proprietà: fallisce quando l’interfaccia non appartiene alla vostra app, che è esattamente lo stato che lascia un altro VPN. Un ritorno false è un segnale per tornare al Passo 1, non per ritentare lo stesso socket.
DatagramSocket socket = new DatagramSocket();
if (!vpnService.protect(socket)) {
return;
}
socket.connect(serverAddress, serverPort);Passo 4: vincolate un processo quando un socket è scomodo
Alcuni trasporti non sono un singolo socket: una libreria può aprirne diversi, oppure potete volere che ogni connessione in un processo ausiliario usi la stessa uscita. ConnectivityManager.bindProcessToNetwork vincola il processo a una rete scelta, e il più vecchio setProcessDefaultNetwork faceva lo stesso prima di diventare obsoleto. Richiedete la connessione sottostante con NET_CAPABILITY_NOT_VPN così il vincolo seleziona la rete sotto il vostro tunnel e non il tunnel stesso.
NetworkRequest request = new NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build();Vincolare un processo non è un secondo VPN. Sceglie un’uscita per quel processo, cosa utile al vostro trasporto e inutile per gestire due tunnel insieme.
Riparate il dispositivo dell’utente, non il percorso del codice
Non esiste alcuna API che permetta a due app di condividere lo slot VPN, quindi la correzione riguarda lo stato del dispositivo. Offrite all’utente due vie concrete: disconnettere o cancellare il sempre attivo dell’altra app così il vostro prepare può subentrare, oppure aprire le impostazioni per app di quell’app ed escludere la vostra app dal suo tunnel. L’esclusione mantiene il filtro attivo per tutto il resto e permette alla vostra app di usare la connessione ordinaria, che spesso è il risultato che l’utente vuole davvero.
Scrivete entrambe le vie nella vostra documentazione di supporto, perché gli utenti raramente sanno quale app possiede il VPN. Chiedete loro di inviare il nome del VPN attivo dalle Impostazioni e i risultati di prepare, establish e protect dai vostri log, e in un minuto potete dire quale passo è fallito. Poi verificate con traffico reale: una risoluzione di nome, una connessione TCP e una richiesta attraverso il tunnel, non solo un’icona connessa. Un descrittore valido e un indicatore verde provano la proprietà nel migliore dei casi, mai che il trasporto abbia superato le regole di routing.
Se tutti e quattro i controlli passano e il traffico muore ancora, la proprietà non è più un vostro problema. Passate al routing e alla raggiungibilità del server, e provate le stesse credenziali da un dispositivo senza app VPN installata.