Añadiste un dominio a la lista de permitidos, la consulta llega a tu aplicación, el comparador devuelve permitido y la página sigue sin cargarse nunca. En ese punto la mayoría empieza a reescribir el comparador y a añadir comparaciones con comodines. Según mi experiencia, el comparador suele ser correcto y el fallo está una capa más abajo: en la respuesta DNS que sintetizaste, en la ruta que instalaste o en el socket que usaste para reenviar la consulta. La lista es la parte fácil; la ruta del paquete es donde se rompen las listas de permitidos.

El comparador rara vez es el culpable

Lo primero que hay que separar es bloqueado de roto. Un nombre bloqueado debería fallar rápido y de forma visible, porque el navegador recibe una respuesta que dice que el nombre no existe y se detiene. Un nombre roto se cuelga, reintenta por IPv6, pasa a otro transporte y te deja con un indicador girando. Si ves un indicador girando, tu lista de permitidos probablemente responde algo con lo que el cliente no puede hacer nada.

El registro A vacío es el error clásico. Devolver NOERROR con cero respuestas parece una respuesta válida, así que el resolutor stub la acepta, el navegador vuelve a intentarlo, prueba AAAA, prueba otro resolutor y se rinde mucho más tarde. Un código de respuesta 3, NXDOMAIN, con la sección de pregunta repetida y el identificador de transacción copiado, termina la conversación de inmediato. Úsalo para cada nombre que rechaces a propósito.

IPv6 merece la misma decisión y no un valor por defecto. Si respondes NXDOMAIN para A y dejas pasar AAAA, el cliente puede alcanzar el nombre bloqueado por IPv6 y el filtro tiene una fuga. Algunas implementaciones responden a las consultas AAAA bloqueadas con una dirección de loopback para que la conexión falle de inmediato en vez de buscar una alternativa. Elige una política, aplícala a ambos tipos de registro y registra ambos.

Una ruta que lo captura todo lo posee todo

Un diseño solo de DNS nunca añade addRoute("0.0.0.0", 0), así que solo las consultas entran en la interfaz TUN y todo lo demás sigue usando la red subyacente. Esa es la forma segura, y también explica por qué algunas listas de permitidos parecen funcionar en Wi-Fi y fallan en la red móvil.

En el momento en que existe una ruta por defecto, cada flujo es tuyo. Un paquete TCP que llega a la interfaz y ni se reenvía ni se responde se queda en la cola hasta que el cliente agota el tiempo, y no hay ningún error que registrar porque nada falló. Vi a alguien pasar una tarde con una lista de permitidos rota que en realidad era un reenviador ausente para el puerto 443. Registra el protocolo y el destino de cada paquete que decidas descartar.

El socket del resolutor tiene que salir del túnel

El reenvío es donde está la siguiente trampa. Los resolutores del operador suelen vivir en una dirección privada que el túnel no alcanza, así que la consulta que reenvías no va a ningún sitio y la aplicación informa de un tiempo de espera de lectura. Envía las consultas aprobadas a un resolutor que sepas que es accesible, y pasa primero ese socket a VpnService.protect(). Un socket sin proteger devuelve la consulta a tu propio túnel, donde vuelve a llegar a la aplicación y gira hasta que salta el tiempo de espera.

El fallo parece idéntico a una caída de red, y por eso sobrevive tanto tiempo en los informes de errores. Mantén contadores de consultas enviadas y respuestas recibidas: si el primer número sube y el segundo se queda en cero, estás mirando el socket, no la lista.

El tamaño y las sumas de verificación fallan sin mensaje

Los descartes silenciosos también vienen del tamaño. Una respuesta DNS con un conjunto grande de respuestas, o un saludo TCP con opciones generosas, puede superar una MTU de ruta que nunca fijaste explícitamente; el fragmento desaparece aguas arriba y el cliente o bien reintenta con menos o deja de intentarlo. Fijar la MTU de la interfaz en 1280 es una elección conservadora que sobrevive a la mayoría de rutas móviles, y ser explícito es mejor que heredar un valor por defecto que nadie midió.

Si construyes los paquetes tú mismo, la suma de verificación es el otro asesino silencioso. Una suma UDP calculada sobre una pseudo-cabecera que omite la longitud UDP o el campo de protocolo produce un paquete que en tu lado parece bien formado y que el receptor descarta sin rastro. Compara tu pseudo-cabecera con la especificación campo por campo, e imprime la suma calculada para un flujo conocido mientras depuras.

Lo que un VpnService no hará por ti

Merece la pena conocer dos límites antes de diseñar alrededor de ellos. Ninguna aplicación normal puede instalar un proxy o un archivo PAC de todo el sistema fuera del Wi-Fi, y esa parte no ha cambiado. La ruta por aplicación sí: antes de la API 29 VpnService.Builder no podía llevar ningún proxy, y desde la API 29 setHttpProxy(ProxyInfo.buildPacProxy(uri)) entrega a la plataforma un script PAC para evaluar para la red VPN. Evalúa las reglas en tu propio código cuando apuntes a API más antiguas o cuando el modelo PAC no pueda expresar la regla. UDP tampoco es gratis: los navegadores solo envían TCP a través de un proxy SOCKS, y la mayoría de clientes SOCKS5 nunca implementan UDP ASSOCIATE, así que un diseño basado en proxy descarta o filtra UDP mientras TCP parece sano.

Ese segundo límite es la razón por la que un protocolo basado en UDP puede funcionar en una red normal y fallar en el momento en que tu VPN se conecta. Si se espera que el túnel lleve UDP, el reenviador tiene que implementar la ruta de reenvío para ello, no solo la ruta TCP con otro tipo de socket.

Comprueba en este orden

Recorre la lista en orden y detente en la primera etapa que falle, porque toda comprobación posterior es ruido hasta que la anterior aguante.

  1. Enumera las rutas instaladas y confirma si los paquetes que no son DNS entran en la interfaz TUN.
  2. Registra el nombre de la consulta, la decisión y el código de respuesta para un nombre bloqueado y uno aprobado.
  3. Confirma que el socket del resolutor está protegido y que el resolutor elegido responde desde dentro del túnel.
  4. Confirma la MTU de la interfaz y la suma de verificación de una respuesta que hayas construido a mano.
  5. Repite la misma página en Wi-Fi y en red móvil, porque los resolutores del operador y la MTU de ruta difieren.

Si las cinco se cumplen y la página sigue colgándose, el siguiente sospechoso es el propio reenviador: compara una petición capturada con una respuesta capturada y busca un recuento de bytes que falta o un socket medio cerrado. La referencia de VpnService.Builder cubre los contratos de ruta, DNS y MTU, y protect() es una llamada VpnService aparte y no una opción del builder; una interfaz conectada solo significa que el primer contrato se cumplió.

Artículos relacionados