Tunnel diviso su Android VpnService: cosa puoi davvero instradare
VpnService inoltra solo i pacchetti che il builder mette nel suo descrittore. Controlla prefissi, autenticazione del proxy, DNS ed elenchi di app prima di dare la colpa al tunnel diviso.
Il tunnel diviso in un'app VpnService si rompe in pochi punti precisi, e la riparazione è diversa per ciascuno. Il client mostra solo una destinazione che fallisce, quindi il primo lavoro è capire quale confine si sta colpendo: le rotte ricevute dal builder, il proxy che non riesci ad autenticare, il resolver che hai solo annunciato o l'elenco di app che la piattaforma applica per utente. Proprio per questo tengo un controllo in quattro passi. Va nell'ordine qui sotto e si ferma al primo errore.
Conferma cosa ha consegnato il builder alla piattaforma
VpnService.Builder è un elenco di dichiarazioni. establish() restituisce un ParcelFileDescriptor per un'interfaccia virtuale, e la piattaforma invia a quel descrittore solo il traffico che corrisponde ai prefissi di addAddress e addRoute; addDnsServer dice ai client quale resolver interrogare. Una destinazione fuori da tutti i prefissi aggiunti non raggiunge mai la tua app, qualunque cosa prometta l'interfaccia. Questo singolo fatto spiega la maggior parte delle segnalazioni di tunnel diviso rotto prima di toccare il codice.
Registra gli elenchi di indirizzi, rotte e DNS che il tuo builder ha composto, poi confrontali con le destinazioni che falliscono. Un /32 mancante per un host di gestione è molto più comune di un tunnel rotto. Se l'elenco è corretto, prosegui; se è sbagliato, il resto della lista di controllo è fatica sprecata.
excludeRoute esiste solo dall'API 33
L'esclusione basata sulla destinazione è la richiesta che più spesso diventa un thread di supporto. builder.excludeRoute(IpPrefix) è arrivato con l'API 33; prima, l'unica approssimazione supportata è aggiungere rotte per tutto tranne il prefisso escluso, il che significa calcolare da soli i prefissi complementari. Le due chiamate non sono immagini speculari: una sottrae dall'insieme di rotte dell'interfaccia, l'altra costruisce l'insieme da zero, e un dispositivo più vecchio lancerà o ignorerà silenziosamente una chiamata che non riconosce. Ramifica su Build.VERSION.SDK_INT e prova entrambi i percorsi su hardware reale.
Quando calcoli i complementi, controlla l'allineamento dei prefissi. Un'esclusione /24 dentro una rotta /16 richiede i prefissi di copertura che descrivono davvero il resto, non un /8 malformato. Registra l'elenco finale e verifica che l'host escluso si risolva ancora tramite un percorso fuori dal tunnel.
Le credenziali del proxy non sono esprimibili
setHttpProxy(ProxyInfo) punta l'interfaccia su un proxy HTTP o SOCKS, e un URL PAC può scegliere tra i due per destinazione. Né ProxyInfo né il formato PAC trasportano nome utente e password, quindi un proxy autenticato resta fuori dall'API della piattaforma. Le app che ne hanno bisogno integrano di solito uno stack in spazio utente, tun2socks o un motore simile, e rispondono da sole alla sfida 407.
Questa scelta ha conseguenze. Uno stack in spazio utente vede solo il traffico che gli instradi, quindi deve vivere dentro lo stesso confine del descrittore e eredita lo stesso requisito protect() per il proprio socket di uplink. Salta protect() e il tunnel cattura il proprio trasporto, cosa che appare come un handshake che si blocca dopo il primo pacchetto. Prova contro un proxy che richiede credenziali, perché un proxy aperto passa sul percorso della piattaforma e non dimostra nulla.
Anche gli elenchi per app e il DNS sono dichiarazioni
Builder ha sia addAllowedApplication sia addDisallowedApplication, e si escludono a vicenda: chiamarli entrambi lancia un'eccezione. In ogni caso l'elenco viene applicato per utente alla configurazione dell'interfaccia, quindi un'app del profilo di lavoro è governata dal tunnel del profilo di lavoro e non dal tuo. Ciò che nessuna API offre è il controllo sulla decisione di un'altra app: non puoi spingere un pacchetto nel tuo tunnel dall'esterno, né chiudere o riconfigurare una VPN già attiva.
Il DNS segue lo stesso schema. addDnsServer annuncia un indirizzo di resolver; non permette alla tua app di rispondere alle query. Se vuoi risolvere i nomi da solo, esegui un resolver e instrada il suo indirizzo nel tunnel. Le app che usano DoT o DoH verso un nome host fisso possono aggirare l'indirizzo annunciato, quindi tratta la risoluzione dei nomi come miglior sforzo a meno che tu non intercetti anche il percorso della query.
Manifest, always-on e il test in quattro passi
L'idoneità si dichiara nel manifest: BIND_VPN_SERVICE, il filtro di intent android.net.VpnService e un servizio in primo piano se il tunnel deve sopravvivere in background. L'always-on VPN richiede inoltre un onRevoke pulito, perché il sistema lo chiama quando l'utente cambia impostazioni o un'altra app prende il controllo.
Esegui i controlli in ordine e fermati al primo errore. Primo, esporta gli elenchi di indirizzi, rotte e DNS passati al builder. Secondo, conferma che establish() abbia restituito un descrittore e che il socket di trasporto sia protetto. Terzo, richiedi una destinazione dentro un prefisso incluso e una dentro un prefisso escluso. Quarto, verifica quale resolver ha risposto davvero interrogando un nome host che solo il tuo resolver può risolvere.
Un handshake completato non dimostra nessuno dei passi successivi. Ogni controllo isola uno strato, quindi un successo al secondo passo non dice nulla sul quarto.
Il riferimento di VpnService.Builder elenca tutta la superficie del builder. Tutto ciò che serve oltre è un'altra API, un'altra app o il tuo stack in spazio utente, e questa è una decisione di progetto, non un bug del tunnel diviso.