Android VpnService si connette ma nessun pacchetto raggiunge la rete
Tunnel attivo e pagine ancora in errore: controllate modalità di lettura, protezione dei socket, supporto TCP e lista delle rotte prima di dare la colpa al server VPN.
Il tunnel sale, l'icona della chiave compare e ogni richiesta nel browser scade ancora. In un'app VpnService questa combinazione di solito significa che il ciclo di lettura è vivo e la metà di uscita è rotta, non che il server ha rifiutato la stretta di mano. Android ha già creato l'interfaccia, spostato il traffico dell'app al suo interno e consegnato al processo un descrittore di file. Tutto ciò che viene dopo quel descrittore è codice vostro, e una chiamata mancante basta a inghiottire l'intero flusso.
Cosa vi dà il sistema e cosa no
establish() restituisce un ParcelFileDescriptor per un'interfaccia tun. Il sistema vi scrive i pacchetti IP in uscita e si aspetta che il processo li legga, decida dove vanno e li invii tramite un socket ordinario. Sotto non c'è alcun inoltro del kernel, quindi quando nessuno legge i pacchetti vengono scartati e i contatori dell'interfaccia non si muovono mai.
Iniziate contando invece di indovinare. Incrementate un contatore a ogni lettura riuscita e registrate destinazione e protocollo dei primi pacchetti. Se il contatore resta a zero mentre viene generato traffico, il guasto è a monte del codice di inoltro, il che restringe la ricerca alla configurazione del builder e alla modalità di lettura. Se il contatore si muove e il browser fallisce ancora, il guasto è nel percorso di invio o in quello di ritorno.
La lettura bloccante decide se il ciclo avanza
setBlocking(true) mette il descrittore in modalità bloccante ed esiste da API 21. Sulle versioni precedenti la soluzione è un piccolo helper JNI che cancella O_NONBLOCK con fcntl sul descrittore grezzo. La modalità conta perché decide cosa può fare una lettura senza pacchetti in attesa.
In modalità non bloccante quella lettura torna subito con un valore negativo, e un ciclo che ignora il risultato fa girare un core senza spostare traffico. In modalità bloccante la lettura aspetta, che è ciò che vuole un thread di inoltro, ma il thread non può più essere interrotto. Chiudere il descrittore è il modo supportato di far fallire la lettura perché il ciclo possa uscire. Uccidere il processo o interrompere il thread può invece lasciare l'avvio successivo con un descrittore che non corrisponde più all'interfaccia.
Leggete un pacchetto per chiamata in un buffer dimensionato per la MTU dell'interfaccia, e trattate zero o un ritorno negativo come errore, non come pacchetto vuoto.
protect() prima del primo invio, non dopo
Ogni socket che trasporta un pacchetto incapsulato deve essere protetto prima. Un socket non protetto obbedisce alle stesse regole di routing appena installate, quindi i suoi pacchetti tornano nel tunnel e il vostro ciclo di lettura li raccoglie di nuovo. Il tunnel allora inoltra la propria uscita all'infinito, e dall'esterno sembra una connessione che si apre e non risponde mai.
L'ordine è protect, poi connect o bind, poi send. Chiamare protect dopo connect può lasciare in piedi la decisione di route precedente, quindi il socket continua a usare il percorso sbagliato anche se la chiamata ha restituito true. Controllate il booleano restituito e fallite rumorosamente quando è false invece di entrare in un invio che non può funzionare.
Un errore di permessi da sendto su un socket che non ha mai toccato protect() è la firma abituale di questo errore.
L'esempio UDP non trasporta HTTPS
L'esempio di inoltro che quasi tutti i tutorial copiano gestisce UDP e nient'altro. TCP richiede una stretta di mano, numeri di sequenza, conferme e ritrasmissione, e un ciclo che copia carichi tra un descrittore tun e un socket non ne fornisce nessuno. Per questo una query DNS su UDP può passare mentre ogni richiesta HTTPS si blocca. O eseguite uno stack TCP nello spazio utente o affidate le connessioni a un proxy locale che termina TCP sul dispositivo e inoltra il flusso.
Testate la separazione prima di riscrivere qualsiasi cosa: inviate una query UDP e aprite una connessione TCP verso lo stesso host. Se il percorso UDP funziona e il percorso TCP si blocca al primo SYN, il problema è il codice di inoltro e non il server. Quell'unico confronto fa risparmiare molto tempo passato a leggere log del server che non contengono nulla di utile.
La lista delle rotte e i filtri per app limitano ciò che vedete
addAddress, addRoute e addDns definiscono cosa il sistema invia nel tunnel. Una destinazione fuori dalla lista delle rotte non raggiunge mai il vostro descrittore, quindi nessuna quantità di logica di inoltro la sistemerà. Registrate le chiamate al builder e confrontatele con gli indirizzi che l'app cerca davvero di raggiungere.
La selezione per app ha il suo limite minimo: addAllowedApplication e addDisallowedApplication richiedono API 21, e sulle versioni precedenti i metodi semplicemente non ci sono. Un progetto che filtra per pacchetto non può funzionare lì.
Il ripristino lascia descrittori obsoleti
Una nuova chiamata a establish() restituisce un nuovo descrittore e chiude l'interfaccia precedente. Il vecchio descrittore ora non è valido, quindi un thread ancora bloccato su di esso riceve un errore, e i socket creati per il vecchio percorso potrebbero dover essere sostituiti. onRevoke è la callback per un tunnel che l'utente o il sistema hanno tolto. Tenere il ciclo di vita presso un solo proprietario, che ferma il lettore prima di ristabilire, evita la maggior parte degli stati connesso-ma-morto.
Leggete ogni errore secondo il passo a cui appartiene. Un reset di connessione da recvfrom significa che l'estremità remota si è chiusa mentre il codice leggeva ancora. Un errore di indirizzo errato indica l'analisi dell'intestazione o un bug di offset, non la rete.
Ordine di lavoro: modalità bloccante attiva, un pacchetto per lettura, intestazione analizzata, socket protetto, carico inviato, chiusura pulita al passaggio e una lista di rotte che copra la destinazione.