Due percorsi possono mettere un'interfaccia IP davanti a un'app Android, e entrambi finiscono nello stesso tipo di descrittore di file con proprietari molto diversi. Uno passa per VpnService e il suo builder, l'altro per una shell root e /dev/net/tun. Ogni lettura su quel descrittore è un pacchetto in uscita e ogni scrittura è un pacchetto in entrata. Ciò che separa i percorsi è chi detiene la politica, chi sopravvive a un aggiornamento del sistema e quanta tabella di routing puoi toccare.

Prendi il percorso della piattaforma a meno che tu non possa nominare la cosa specifica che rifiuta di fare. Quel rifiuto è di solito TAP, la cattura fuori dal tuo insieme di app, o una regola di routing che il framework non espone mai.

Sul percorso della piattaforma dichiari un'intenzione invece di comandi. Fornisci almeno un indirizzo, le rotte desiderate, server DNS e un elenco facoltativo di app, poi il servizio di sistema di Android esegue la configurazione privilegiata con la propria identità. Non conosci mai in anticipo il nome dell'interfaccia e non emetti mai l'ioctl che la crea, e questo è il punto.

Due modi per ottenere un'interfaccia IP

Il percorso root è la ricetta più vecchia. Apri /dev/net/tun, chiami TUNSETIFF con IFF_TUN, alzi l'interfaccia con ip link e aggiungi indirizzi e rotte a mano. Ogni passo è tuo da fare bene, compresi quelli che il framework avrebbe gestito in silenzio. Una rotta mancante produce la stessa segnalazione all'utente di un server morto.

Il debug differisce quanto la configurazione. Sul percorso root l'interfaccia esiste oppure il tuo ioctl ha restituito un errore che puoi leggere direttamente. Sul percorso della piattaforma l'interfaccia esiste solo dopo il successo di establish(), quindi un guasto arriva come eccezione o come descrittore null invece che come nodo di dispositivo in cui frugare.

Cosa restituisce davvero establish()

Builder.establish() ha tre esiti e richiedono tre risposte diverse. Un descrittore significa che l'interfaccia è configurata ed è tua da usare, null significa che la richiesta non è passata, e un'eccezione lanciata significa che il servizio di sistema l'ha rifiutata. Collassare null e successo in un solo booleano è il modo più comune con cui un'app segnala un tunnel funzionante che non trasporta mai un pacchetto. Il riferimento di Builder indica il requisito di indirizzo, e vale la pena leggerlo prima di dare la colpa al server.

Le direzioni sono fissate dal descrittore, non dal tuo protocollo. Le letture ti danno pacchetti in uscita già selezionati per il VPN, e le scritture iniettano pacchetti in entrata di nuovo nell'interfaccia. Il tuo trasporto cifra, autentica e incornicia quei pacchetti, perché la piattaforma si ferma al confine del pacchetto.

Binder è un salto, JNI è un altro asse

Una richiesta del builder attraversa il sistema tramite Binder, e da qualche parte sotto questo la piattaforma chiama codice nativo per configurare il dispositivo. Sono due meccanismi diversi impilati in un'unica chiamata. Binder è comunicazione tra processi tra la tua app e un servizio di sistema, mentre JNI è il ponte Java-nativo dentro un processo. Leggere un albero AOSP di Android 9 rende lo stack letterale, e vale la pena ricordare che nomi di classi interne e percorsi di file si spostano tra le versioni.

Ciò che non si sposta è il contratto su cui puoi contare: il builder, il descrittore che restituisce e il ciclo di vita del servizio intorno a esso. Nessuna app dovrebbe chiamare una transazione di servizio interna o ricreare un nodo di dispositivo per ottenere un'interfaccia. Se il tuo codice dipende da un nome di classe AOSP, si romperà su una versione che non hai testato.

TUN trasporta IP, TAP trasporta Ethernet

TUN è un'interfaccia IP. TAP è un'interfaccia Ethernet, e quella differenza decide se il bridging è anche solo possibile. Un dispositivo TUN non ha indirizzo MAC né intestazione Ethernet da inoltrare, quindi un client TUN non può unirsi a un bridge di livello 2, per come sono scritte le rotte.

VpnService ti dà la variante IP, che è ciò di cui ha bisogno un VPN a livello di app e che può supportare senza uno stack Ethernet grezzo. Se il tuo design richiede bridging, il percorso della piattaforma non ti porterà lì, e il percorso root solo se il kernel e i permessi del dispositivo collaborano.

Dove il percorso root vince ancora

Tre lavori spingono la gente fuori dal percorso della piattaforma. Il bridging di livello 2 richiede TAP. Catturare il traffico di app fuori dal tuo perimetro VPN richiede una visibilità che il framework non concede. Il routing basato su policy che il builder non espone mai, come una tabella separata per una destinazione, richiede regole che installi tu stesso.

I costi arrivano dopo: un bootloader sbloccato o un'immagine con root, una policy SELinux che si sposta sotto di te e un dispositivo che smette di comportarsi correttamente dopo un OTA. Una ricetta con root non può nemmeno essere distribuita tramite la normale distribuzione di app, quindi la maggior parte dei team la tiene come percorso diagnostico interno invece che come prodotto.

Scegliere tra i due

Parti dall'interfaccia che ti serve, non dal codice che hai. Se la risposta è un dispositivo TUN con rotte limitate all'app, il percorso della piattaforma è più corto e sopravvive agli aggiornamenti. Se la risposta implica frame Ethernet, cattura di altre app o regole di routing che il builder rifiuta, sei in territorio root e dovresti metterlo a budget come costo operativo.

Un controllo che risolve la maggior parte delle discussioni: costruisci prima il percorso della piattaforma, registra l'esito di establish() separatamente dallo stato della sessione remota e conferma un pacchetto in ciascuna direzione. Solo allora decidi se la capacità mancante giustifichi lasciare il percorso supportato.

Articoli correlati