Android VPN: descriptor de VpnService contra un dispositiu tun de root
Les aplicacions Android no poden crear tun0 per si mateixes: què retorna establish(), on s'atura Binder i on comença JNI, i quan guanya un dispositiu creat per root.
Dos camins poden posar una interfície IP davant d'una aplicació Android, i tots dos acaben en el mateix tipus de descriptor de fitxer amb propietaris molt diferents. Un passa per VpnService i el seu constructor, l'altre per un shell root i /dev/net/tun. Cada lectura en aquest descriptor és un paquet de sortida i cada escriptura és un paquet d'entrada. El que separa els camins és qui té la política, qui sobreviu a una actualització del sistema operatiu i quanta taula d'encaminament pots tocar.
Pren el camí de la plataforma tret que puguis anomenar la cosa concreta que es nega a fer. Aquesta negativa sol ser TAP, la captura fora del teu conjunt d'aplicacions, o una regla d'encaminament que el framework mai no exposa.
Al camí de la plataforma declares intenció en lloc d'ordres. Proporciones almenys una adreça, les rutes que vols, servidors DNS i una llista opcional d'aplicacions; després el servei del sistema d'Android fa la configuració privilegiada amb la seva pròpia identitat. Mai no aprens el nom de la interfície per endavant i mai no emets l'ioctl que la crea, i aquest és el punt.
Dues maneres d'obtenir una interfície IP
El camí root és la recepta més antiga. Obres /dev/net/tun, crides TUNSETIFF amb IFF_TUN, aixeques la interfície amb ip link i afegeixes adreces i rutes a mà. Cada pas és teu per encertar-lo, inclosos els que el framework hauria resolt en silenci. Una ruta que falta produeix el mateix informe d'usuari que un servidor mort.
La depuració difereix tant com la configuració. Al camí root la interfície existeix o bé el teu ioctl ha retornat un error que pots llegir directament. Al camí de la plataforma la interfície només existeix després que establish() tingui èxit, així que un fracàs arriba com a excepció o com a descriptor null en lloc d'un node de dispositiu on furgar.
Què retorna realment establish()
Builder.establish() té tres desenllaços i necessiten tres respostes diferents. Un descriptor vol dir que la interfície està configurada i és teva per usar-la, null vol dir que la sol·licitud no ha passat, i una excepció llançada vol dir que el servei del sistema l'ha rebutjada. Col·lapsar null i èxit en un sol booleà és la manera més comuna que té una aplicació d'informar d'un túnel que funciona i que mai no porta cap paquet. La referència de Builder indica el requisit d'adreça, que val la pena llegir abans de culpar el servidor.
Les direccions les fixa el descriptor, no el teu protocol. Les lectures et donen paquets de sortida ja seleccionats per a la VPN, i les escriptures injecten paquets d'entrada de tornada a la interfície. El teu propi transport xifra, autentica i emmarca aquests paquets, perquè la plataforma s'atura a la frontera del paquet.
Binder és un salt, JNI és un altre eix
Una sol·licitud del constructor entra al sistema a través de Binder, i en algun punt per sota la plataforma crida codi natiu per configurar el dispositiu. Són dos mecanismes diferents apilats dins d'una sola crida. Binder és comunicació entre processos entre la teva aplicació i un servei del sistema, mentre que JNI és el pont de Java a natiu dins d'un procés. Llegir un arbre AOSP d'Android 9 fa que la pila sembli literal, i val la pena recordar que els noms de classe interns i els camins de fitxer es mouen entre versions.
El que no es mou és el contracte amb què pots comptar: el constructor, el descriptor que retorna i el cicle de vida del servei al seu voltant. Cap aplicació no hauria de cridar una transacció interna del servei ni recrear un node de dispositiu per obtenir una interfície. Si el teu codi depèn d'un nom de classe d'AOSP, es trencarà en una versió que no has provat.
TUN porta IP, TAP porta Ethernet
TUN és una interfície IP. TAP és una interfície Ethernet, i aquesta diferència decideix si el bridge és possible. Un dispositiu TUN no té adreça MAC ni capçalera Ethernet per reenviar, així que un client TUN no pot unir-se a un bridge de capa 2, per molt que s'escriguin les rutes.
VpnService et dona la variant IP, que és el que necessita una VPN a nivell d'aplicació i el que pot suportar sense una pila Ethernet crua. Si el teu disseny requereix bridge, el camí de la plataforma no t'hi portarà, i el camí root només ho farà si el nucli i els permisos del dispositiu hi col·laboren.
On el camí root encara guanya
Tres feines empenyen la gent fora del camí de la plataforma. El bridge de capa 2 necessita TAP. Capturar trànsit d'aplicacions fora del teu abast de VPN necessita una visibilitat que el framework no atorga. L'encaminament per política que el constructor mai no exposa, com ara una taula separada per a una destinació, necessita regles que instal·lis tu mateix.
Els costos arriben més tard: un bootloader desbloquejat o una imatge amb root, una política SELinux que es mou sota els teus peus i un dispositiu que deixa de comportar-se després d'una OTA. Una recepta amb root tampoc no es pot distribuir per la distribució normal d'aplicacions, així que la majoria d'equips la mantenen com a camí de diagnòstic intern en lloc de producte.
Triar entre ells
Comença per la interfície que necessites, no pel codi que tens. Si la resposta és un dispositiu TUN amb rutes limitades a l'aplicació, el camí de la plataforma és més curt i sobreviu a les actualitzacions. Si la resposta implica trames Ethernet, captura d'altres aplicacions o regles d'encaminament que el constructor rebutja, ets en territori root i hauries de pressupostar-ho com a cost operatiu.
Una comprovació que resol la majoria de discussions: construeix primer el camí de la plataforma, enregistra el resultat d'establish() per separat de l'estat de la sessió remota i confirma un paquet en cada direcció. Només llavors decideix si la capacitat que falta justifica abandonar el camí suportat.