Android VPN: descritor de VpnService fronte a un dispositivo tun de root
As aplicacións Android non poden crear tun0 por si mesmas: que devolve establish(), onde se detén Binder e onde comeza JNI, e cando gaña un dispositivo creado por root.
Dous camiños poden poñer unha interface IP diante dunha aplicación Android, e ambos rematan no mesmo tipo de descritor de ficheiro con propietarios moi distintos. Un pasa por VpnService e o seu builder, o outro por un shell root e /dev/net/tun. Cada lectura nese descritor é un paquete de saída e cada escritura é un paquete de entrada. O que separa os camiños é quen ten a política, quen sobrevive a unha actualización do sistema operativo e canta táboa de enrutamento podes tocar.
Toma o camiño da plataforma salvo que poidas nomear a cousa concreta que se nega a facer. Esa negativa adoita ser TAP, a captura fóra do teu propio conxunto de aplicacións, ou unha regra de enrutamento que o framework nunca expón.
No camiño da plataforma declaras intención en lugar de ordes. Aportas polo menos un enderezo, as rutas que queres, servidores DNS e unha lista opcional de aplicacións; despois o servizo do sistema de Android realiza a configuración privilexiada coa súa propia identidade. Nunca coñeces de antemán o nome da interface e nunca emites o ioctl que a crea, e diso se trata.
Dúas formas de obter unha interface IP
O camiño root é a receita máis antiga. Abres /dev/net/tun, chamas a TUNSETIFF con IFF_TUN, levantas a interface con ip link e engades enderezos e rutas a man. Cada paso é teu para acertalo, incluídos os que o framework tería resolto en silencio. Unha ruta que falta produce o mesmo informe de usuario que un servidor morto.
A depuración difire tanto como a configuración. No camiño root a interface existe ou o teu ioctl devolveu un erro que podes ler directamente. No camiño da plataforma a interface só existe despois de que establish() teña éxito, así que un fallo chega como excepción ou como descritor null en lugar dun nodo de dispositivo onde furgar.
Que devolve realmente establish()
Builder.establish() ten tres desenlaces e necesitan tres respostas distintas. Un descritor significa que a interface está configurada e é túa para usala, null significa que a solicitude non pasou, e unha excepción lanzada significa que o servizo do sistema a rexeitou. Colapsar null e éxito nun só booleano é a forma máis común en que unha aplicación informa dun túnel que funciona e que nunca leva un paquete. A referencia de Builder indica o requisito de enderezo, que paga a pena ler antes de culpar o servidor.
As direccións fíxaas o descritor, non o teu protocolo. As lecturas danche paquetes de saída xa seleccionados para a VPN, e as escrituras inxectan paquetes de entrada de volta na interface. O teu propio transporte cifra, autentica e enmarca eses paquetes, porque a plataforma detense no límite do paquete.
Binder é un salto, JNI é outro eixe
Unha solicitude do builder cruza ao sistema a través de Binder, e nalgún punto por baixo a plataforma chama código nativo para configurar o dispositivo. Son dous mecanismos distintos apilados dentro dunha soa chamada. Binder é comunicación entre procesos entre a túa aplicación e un servizo do sistema, mentres que JNI é a ponte de Java a nativo dentro dun proceso. Ler unha árbore AOSP de Android 9 fai que a pila pareza literal, e paga a pena recordar que os nomes de clase internos e as rutas de ficheiro se moven entre versións.
O que non se move é o contrato co que podes contar: o builder, o descritor que devolve e o ciclo de vida do servizo ao seu redor. Ningunha aplicación debería chamar unha transacción interna do servizo nin recrear un nodo de dispositivo para obter unha interface. Se o teu código depende dun nome de clase de AOSP, romperá nunha versión que non probaches.
TUN leva IP, TAP leva Ethernet
TUN é unha interface IP. TAP é unha interface Ethernet, e esa diferenza decide se a ponte é sequera posible. Un dispositivo TUN non ten enderezo MAC nin cabeceira Ethernet que reenviar, así que un cliente TUN non pode unirse a unha ponte de capa 2, por moito que se escriban as rutas.
VpnService dache a variante IP, que é o que necesita unha VPN a nivel de aplicación e o que pode soportar sen unha pila Ethernet crua. Se o teu deseño require ponte, o camiño da plataforma non te levará alí, e o camiño root só o fará se o núcleo e os permisos do dispositivo cooperan.
Onde o camiño root aínda gaña
Tres traballos empuxan a xente fóra do camiño da plataforma. A ponte de capa 2 necesita TAP. Capturar tráfico de aplicacións fóra do teu propio ámbito de VPN necesita visibilidade que o framework non concede. O enrutamento por política que o builder nunca expón, como unha táboa separada para un destino, necesita regras que instales ti mesmo.
Os custos chegan despois: un bootloader desbloqueado ou unha imaxe con root, unha política SELinux que se move baixo os teus pés e un dispositivo que deixa de comportarse tras unha OTA. Unha receita con root tampouco se pode distribuír pola distribución normal de aplicacións, así que a maioría dos equipos mantena como un camiño de diagnóstico interno en lugar dun produto.
Elixir entre eles
Comeza pola interface que necesitas, non polo código que tes. Se a resposta é un dispositivo TUN con rutas limitadas á aplicación, o camiño da plataforma é máis curto e sobrevive ás actualizacións. Se a resposta implica tramas Ethernet, captura doutras aplicacións ou regras de enrutamento que o builder rexeita, estás en territorio root e deberías orzamentalo como un custo operativo.
Unha comprobación que resolve a maioría das discusións: constrúe primeiro o camiño da plataforma, rexistra o resultado de establish() por separado do estado da sesión remota e confirma un paquete en cada dirección. Só entón decide se a capacidade que falta xustifica abandonar o camiño soportado.