Dos rutas pueden poner una interfaz IP frente a una aplicación Android, y ambas terminan en el mismo tipo de descriptor de archivo con dueños muy distintos. Una pasa por VpnService y su builder, la otra por un shell root y /dev/net/tun. Cada lectura en ese descriptor es un paquete de salida y cada escritura es un paquete de entrada. Lo que separa las rutas es quién tiene la política, quién sobrevive a una actualización del sistema operativo y cuánta tabla de enrutamiento puedes tocar.

Toma la ruta de la plataforma salvo que puedas nombrar la cosa concreta que se niega a hacer. Esa negativa suele ser TAP, la captura fuera de tu propio conjunto de aplicaciones, o una regla de enrutamiento que el framework nunca expone.

En la ruta de la plataforma declaras intención en lugar de comandos. Aportas al menos una dirección, las rutas que quieres, servidores DNS y una lista opcional de aplicaciones; después el servicio del sistema de Android realiza la configuración privilegiada con su propia identidad. Nunca conoces de antemano el nombre de la interfaz y nunca emites el ioctl que la crea, y de eso se trata.

Dos formas de obtener una interfaz IP

La ruta root es la receta más antigua. Abres /dev/net/tun, llamas a TUNSETIFF con IFF_TUN, levantas la interfaz con ip link y añades direcciones y rutas a mano. Cada paso es tuyo para acertarlo, incluidos los que el framework habría resuelto en silencio. Una ruta que falta produce el mismo informe de usuario que un servidor muerto.

La depuración difiere tanto como la configuración. En la ruta root la interfaz existe o tu ioctl devolvió un error que puedes leer directamente. En la ruta de la plataforma la interfaz solo existe después de que establish() tenga éxito, así que un fallo llega como excepción o como descriptor null en lugar de un nodo de dispositivo donde hurgar.

Qué devuelve realmente establish()

Builder.establish() tiene tres desenlaces y necesitan tres respuestas distintas. Un descriptor significa que la interfaz está configurada y es tuya para usarla, null significa que la solicitud no pasó, y una excepción lanzada significa que el servicio del sistema la rechazó. Colapsar null y éxito en un solo booleano es la forma más común en que una aplicación informa de un túnel que funciona y que nunca lleva un paquete. La referencia de Builder indica el requisito de dirección, que vale la pena leer antes de culpar al servidor.

Las direcciones las fija el descriptor, no tu protocolo. Las lecturas te dan paquetes de salida ya seleccionados para la VPN, y las escrituras inyectan paquetes de entrada de vuelta en la interfaz. Tu propio transporte cifra, autentica y enmarca esos paquetes, porque la plataforma se detiene en el límite del paquete.

Binder es un salto, JNI es otro eje

Una solicitud del builder cruza al sistema a través de Binder, y en algún punto por debajo la plataforma llama a código nativo para configurar el dispositivo. Son dos mecanismos distintos apilados dentro de una sola llamada. Binder es comunicación entre procesos entre tu aplicación y un servicio del sistema, mientras que JNI es el puente de Java a nativo dentro de un proceso. Leer un árbol AOSP de Android 9 hace que la pila parezca literal, y conviene recordar que los nombres de clase internos y las rutas de archivo se mueven entre versiones.

Lo que no se mueve es el contrato con el que puedes contar: el builder, el descriptor que devuelve y el ciclo de vida del servicio a su alrededor. Ninguna aplicación debería llamar a una transacción interna del servicio ni recrear un nodo de dispositivo para obtener una interfaz. Si tu código depende de un nombre de clase de AOSP, se romperá en una versión que no probaste.

TUN lleva IP, TAP lleva Ethernet

TUN es una interfaz IP. TAP es una interfaz Ethernet, y esa diferencia decide si el puente es siquiera posible. Un dispositivo TUN no tiene dirección MAC ni cabecera Ethernet que reenviar, así que un cliente TUN no puede unirse a un puente de capa 2, por mucho que se escriban las rutas.

VpnService te da la variante IP, que es lo que necesita una VPN a nivel de aplicación y lo que puede soportar sin una pila Ethernet cruda. Si tu diseño requiere puente, la ruta de la plataforma no te llevará allí, y la ruta root solo lo hará si el kernel y los permisos del dispositivo cooperan.

Dónde sigue ganando la ruta root

Tres trabajos empujan a la gente fuera de la ruta de la plataforma. El puente de capa 2 necesita TAP. Capturar tráfico de aplicaciones fuera de tu propio ámbito de VPN necesita visibilidad que el framework no concede. El enrutamiento por política que el builder nunca expone, como una tabla separada para un destino, necesita reglas que instales tú mismo.

Los costes llegan después: un bootloader desbloqueado o una imagen con root, una política SELinux que se mueve bajo tus pies y un dispositivo que deja de comportarse tras una OTA. Una receta con root tampoco puede distribuirse por la distribución normal de aplicaciones, así que la mayoría de los equipos la mantienen como una ruta de diagnóstico interna en lugar de un producto.

Elegir entre ellas

Empieza por la interfaz que necesitas, no por el código que tienes. Si la respuesta es un dispositivo TUN con rutas limitadas a la aplicación, la ruta de la plataforma es más corta y sobrevive a las actualizaciones. Si la respuesta implica tramas Ethernet, captura de otras aplicaciones o reglas de enrutamiento que el builder rechaza, estás en territorio root y deberías presupuestarlo como un coste operativo.

Una comprobación que resuelve la mayoría de las discusiones: construye primero la ruta de la plataforma, registra el resultado de establish() por separado del estado de la sesión remota y confirma un paquete en cada dirección. Solo entonces decide si la capacidad que falta justifica abandonar la ruta soportada.

Artículos relacionados