El túnel se levanta, aparece el icono de la llave y cada petición del navegador sigue agotando el tiempo. En una aplicación VpnService esa combinación suele significar que el bucle de lectura está vivo y la mitad de salida está rota, no que el servidor haya rechazado el apretón de manos. Android ya creó la interfaz, movió el tráfico de la aplicación dentro de ella y entregó a su proceso un descriptor de archivo. Todo lo que va después de ese descriptor es código suyo, y una llamada que falte basta para tragarse el flujo entero.

Qué le da el sistema y qué no

establish() devuelve un ParcelFileDescriptor para una interfaz tun. El sistema escribe paquetes IP salientes en ella y espera que su proceso los lea, decida adónde van y los envíe por un socket corriente. Debajo no hay reenvío del núcleo, así que cuando nadie lee, los paquetes se descartan y los contadores de la interfaz nunca se mueven.

Empiece contando en lugar de adivinar. Incremente un contador en cada lectura correcta y registre el destino y el protocolo de los primeros paquetes. Si el contador se queda en cero mientras se genera tráfico, el fallo está por encima del código de reenvío, lo que reduce la búsqueda a la configuración del builder y al modo de lectura. Si el contador se mueve y el navegador sigue fallando, el fallo está en la ruta de envío o en la de vuelta.

La lectura bloqueante decide si el bucle avanza

setBlocking(true) pone el descriptor en modo bloqueante y existe desde API 21. En versiones anteriores la solución es un pequeño ayudante JNI que borra O_NONBLOCK con fcntl sobre el descriptor sin procesar. El modo importa porque decide qué puede hacer una lectura sin ningún paquete pendiente.

En modo no bloqueante esa lectura vuelve de inmediato con un valor negativo, y un bucle que ignora el resultado gira un núcleo sin mover tráfico alguno. En modo bloqueante la lectura espera, que es lo que quiere un hilo de reenvío, pero entonces el hilo ya no se puede interrumpir. Cerrar el descriptor es la forma soportada de hacer fallar la lectura para que el bucle pueda salir. Matar el proceso o interrumpir el hilo puede dejar el siguiente arranque con un descriptor que ya no corresponde a la interfaz.

Lea un paquete por llamada en un búfer dimensionado para la MTU de la interfaz, y trate el cero o un retorno negativo como error en lugar de como paquete vacío.

protect() antes del primer envío, no después

Todo socket que transporte un paquete encapsulado debe protegerse primero. Un socket sin proteger obedece las mismas reglas de encaminamiento que acaba de instalar, así que sus paquetes vuelven al túnel y su propio bucle de lectura los recoge otra vez. Entonces el túnel reenvía su propia salida sin fin, y desde fuera parece una conexión que se abre y nunca responde.

El orden es protect, luego connect o bind, luego send. Llamar a protect después de connect puede dejar en pie la decisión de ruta anterior, así que el socket sigue usando el camino equivocado aunque la llamada devolviera true. Compruebe el booleano que devuelve y falle ruidosamente cuando sea false en vez de entrar en un envío que no puede funcionar.

Un error de permisos de sendto en un socket que nunca tocó protect() es la firma habitual de este fallo.

El ejemplo UDP no transporta HTTPS

El ejemplo de reenvío que copian casi todos los tutoriales maneja UDP y nada más. TCP necesita un apretón de manos, números de secuencia, confirmaciones y retransmisión, y un bucle que copia cargas entre un descriptor tun y un socket no aporta nada de eso. Por eso una consulta DNS por UDP puede pasar mientras cada petición HTTPS se atasca. O ejecuta una pila TCP en espacio de usuario o entrega las conexiones a un proxy local que termina TCP en el dispositivo y reenvía el flujo.

Pruebe la división antes de reescribir nada: envíe una consulta UDP y abra una conexión TCP al mismo host. Si el camino UDP funciona y el camino TCP se cuelga en el primer SYN, el problema es el código de reenvío y no el servidor. Esa única comparación ahorra mucho tiempo leyendo registros del servidor que no contienen nada útil.

La lista de rutas y los filtros por aplicación limitan lo que ve

addAddress, addRoute y addDns definen qué envía el sistema al túnel. Un destino fuera de la lista de rutas nunca llega a su descriptor, así que ninguna cantidad de lógica de reenvío lo arreglará. Registre las llamadas al builder que hace y compárelas con las direcciones a las que la aplicación intenta llegar de verdad.

La selección por aplicación tiene su propio mínimo: addAllowedApplication y addDisallowedApplication necesitan API 21, y en versiones anteriores los métodos simplemente no existen. Un diseño que filtra por paquete no puede ejecutarse allí en absoluto.

Reestablecer deja descriptores obsoletos

Volver a llamar a establish() devuelve un descriptor nuevo y cierra la interfaz anterior. El descriptor viejo ya no es válido, así que un hilo que siga bloqueado en él recibe un error, y los sockets creados para el camino viejo quizá deban sustituirse. onRevoke es la devolución de llamada para un túnel que el usuario o el sistema han retirado. Mantener el ciclo de vida en un solo dueño, que detiene al lector antes de reestablecer, evita la mayoría de los estados conectado-pero-muerto.

Lea cada error según el paso al que pertenece. Un reinicio de conexión de recvfrom significa que el extremo remoto cerró mientras su código aún leía. Un error de dirección incorrecta apunta al análisis de cabeceras o a un fallo de desplazamiento, no a la red.

Orden de trabajo: modo bloqueante activado, un paquete por lectura, cabecera analizada, socket protegido, carga enviada, cierre limpio en el traspaso y una lista de rutas que cubra el destino.

Artículos relacionados