Nada en VpnService intercepta tráfico por sí solo. Pide al sistema una interfaz tun, le entrega a esa interfaz un conjunto de direcciones y rutas y, desde ese momento, el núcleo copia cada paquete que coincide con esas rutas en un descriptor de archivo que la aplicación puede leer. El código de la aplicación nunca ve los paquetes que las rutas no seleccionaron, así que el alcance de un túnel es una cuestión de tabla de enrutamiento antes que una cuestión de programación. Por eso la misma implementación del servicio puede tragarse cada flujo en un teléfono y solo una parte en otro, y por eso añadir una ruta a veces repara un túnel que parecía roto. Lo que obtienes es lo que declaraste, menos todo lo que el constructor rechazó en silencio.

Las rutas deciden qué paquetes llegan

Builder.addRoute(prefix, prefixLength) instala un prefijo en la tabla de enrutamiento de la red VPN. El sistema trata esa red como predeterminada para las aplicaciones del alcance, así que sus entradas se consultan antes que la ruta predeterminada de la red física y un prefijo coincidente basta para desviar un flujo. Por eso una ruta para 0.0.0.0/0 toma IPv4 entero, incluidos la impresora, el destino de cast y la página de administración del router.

Los clientes que quieren esa cobertura sin borrar la entrada predeterminada física usan en cambio el par 0.0.0.0/1 y 128.0.0.0/1. Dos rutas más específicas ganan a una ruta predeterminada, juntas cubren todo el espacio de direcciones y la entrada predeterminada del propio teléfono se queda en la tabla para los sockets que siguen ligados a la red física.

Las direcciones y las entradas DNS vienen del mismo constructor y responden a otra pregunta. addAddress da a la interfaz una dirección con una longitud de prefijo, y addDnsServer instala un resolutor que las aplicaciones del alcance consultarán a través de la interfaz. Ambas familias pueden convivir en una interfaz, así que un conjunto de rutas solo IPv4 junto a una dirección IPv6 es una configuración que se lee correctamente y aun así filtra. Si enrutas una familia en la que la interfaz no tiene dirección, el núcleo no tiene dirección de origen para esos paquetes y nunca aparecen en el descriptor.

Qué devuelve establish()

establish() devuelve un ParcelFileDescriptor, o null cuando la aplicación no está autorizada. Null es un estado de consentimiento y no un error de red: VpnService.prepare() ya debe haber devuelto null, o el usuario debe haber aprobado el diálogo del sistema antes de la llamada. El consentimiento puede revocarse mientras el servicio se ejecuta, y la interfaz se cierra bajo tus pies sin ninguna devolución de llamada a tu bucle de lectura.

El descriptor no bloquea salvo que digas lo contrario, así que una lectura vuelve de inmediato sin datos en lugar de aparcar el hilo. Un bucle escrito para un socket bloqueante quema un núcleo, y un bucle que trata una lectura vacía como fatal rompe el túnel en un segundo inactivo. Builder.setBlocking(true) cambia ese comportamiento, y el precio es un hilo que espera dentro de la lectura.

Los argumentos de ruta y dirección se validan como valores LinkAddress, y los destinos de multicast como 224.0.0.0/4 se rechazan de plano. Una interfaz tun punto a punto tampoco tiene dominio de broadcast, así que el descubrimiento de multicast a través de ella necesita otro diseño en lugar de una ruta más. La referencia de VpnService.Builder es la lista que hay que revisar antes de concluir que una llamada se ignoró.

El alcance por aplicación estrecha el conjunto, no el túnel

Desde el API 21 el constructor acepta addAllowedApplication y addDisallowedApplication, y las dos listas son mutuamente excluyentes dentro de un mismo constructor. Una lista de permitidos hace que el túnel exista para esos paquetes mientras todo lo demás conserva la red física; una lista de denegados captura todo excepto los paquetes nombrados, que es lo que un cliente ofrece como túnel dividido. Antes del API 21 no hay ningún mecanismo de plataforma, y el comportamiento por aplicación venía de clientes de terceros que filtraban por UID dentro de su propio reenviador. La distinción importa al depurar, porque un túnel por aplicación con una lista de un solo paquete apenas produce tráfico y eso es un comportamiento correcto, no una ruta rota.

Sea cual sea el alcance, el socket que tu aplicación abre hacia el servidor VPN no debe viajar por el túnel que está construyendo. VpnService.protect(socket) liga ese socket a la red subyacente para que sus paquetes salten el dispositivo tun. Si te saltas la llamada, el apretón de manos se lo traga la interfaz que debería levantarlo.

VpnService.setUnderlyingNetworks cierra el círculo diciendo al sistema qué red física transporta el túnel. Ese valor determina cómo informa la VPN de la conectividad a las demás aplicaciones, y un valor null o desactualizado es una causa habitual de un túnel conectado que el resto del teléfono trata como sin conexión. Actualízalo cuando cambie la red subyacente, no solo al arrancar.

Sumas de verificación y MTU: donde una ruta correcta sigue fallando

Una vez que los paquetes llegan, dos detalles a nivel de paquete deciden si algo funciona de extremo a extremo. El primero es la dirección de origen: con varias direcciones de interfaz el núcleo puede emitir paquetes que llevan la primera, así que un reenviador que reescribe cabeceras debe corregir la dirección y la suma de verificación que la cubre.

El segundo es el alcance de la suma de verificación. Cuando la aplicación construye ella misma paquetes TCP o UDP, la suma cubre una pseudocabecera con la dirección de origen, la dirección de destino, el número de protocolo y la longitud de transporte, y omitir cualquiera de ellos produce paquetes que llegan y se descartan. Ese fallo parece un tiempo de espera tras una respuesta que nunca llega, lo que devuelve a la mayoría de la gente a las rutas. setMtu fija la MTU de la interfaz, y cada lectura devuelve exactamente un paquete IP completo, así que un búfer escrito para un flujo de bytes fragmentará tu análisis, no el paquete.

Una pasada de verificación que separa las capas

Para encontrar dónde se torció el alcance, prueba las capas en orden. Pregunta al dispositivo qué rutas mantiene realmente la red VPN, luego comprueba si un flujo hacia una dirección conocida aparece como una lectura en el descriptor, y luego comprueba si el paquete que emite tu reenviador está bien formado. Un problema de ruta no muestra nada en el descriptor, mientras que un problema de suma de verificación muestra un paquete que llega y desaparece.

Si nada llega al descriptor, el sospechoso es el conjunto de rutas y no el lector: confirma los prefijos, confirma que hay una dirección en la misma familia y confirma que la aplicación está dentro del alcance que fijaste. Si los paquetes llegan al descriptor pero no vuelve nada, inspecciona protect() y el valor de la red subyacente antes de tocar otra vez una ruta. Si el lado remoto informa de paquetes mal formados, la suma de verificación y la MTU son los sitios donde mirar. Una sonda por capa ahorra una tarde de conjeturas.

Artículos relacionados