Dos síntomas aparecen en el mismo bucle de VpnService y se culpa a lo mismo: un lector que nunca regresa y una escritura que falla con ENOBUFS. No son variaciones de un solo error. Una lectura atascada es un desacuerdo sobre el control de flujo con el peer; una escritura fallida es un paquete que la interfaz no aceptará. Si los trata como un solo problema, pasará un día ajustando búferes que nunca estuvieron implicados. La división de abajo es cómo decido qué mitad del bucle instrumentar primero.

Un bucle, dos puntos de fallo independientes

Su servicio lee un paquete IP del descriptor de archivo TUN, lo inspecciona y escribe una respuesta de vuelta al mismo descriptor. Todo lo que se rompe en esa ruta aparece en una de esas dos llamadas, y por eso ambas se reportan como el túnel roto. Las llamadas tienen contratos distintos. Una lectura espera a que el kernel le entregue un paquete, y una escritura entrega al kernel un paquete que tiene derecho a rechazar.

Las lecturas son la mitad fácil de razonar. Una lectura que no devuelve nada o bien espera tráfico que nunca llega o bien espera porque el peer dejó de enviar.

Una lectura atascada es un desacuerdo de control de flujo

Leer un dispositivo TUN bloquea hasta que hay un paquete disponible. Ese es el comportamiento diseñado, y por eso un hilo aparcado dentro de read parece normal en un enlace inactivo. Se convierte en un cuelgue cuando su propia pila TCP sigue aceptando datos de la aplicación local pero nunca anuncia suficiente ventana para que el peer remoto envíe más. El peer llena la ventana que usted anunció, espera un ACK y una actualización de ventana, y se detiene. Entonces su lector no tiene nada que devolver, y la aplicación ve una conexión abierta y silenciosa.

Las causas habituales son una pila que escribe en el socket sin seguir la ventana anunciada, y otra que olvida enviar una actualización de ventana tras vaciar su búfer. Desde fuera parecen idénticas: salen bytes, no vuelve nada, no se produce ningún error. Bloquear el hilo del lector en parseo, resolución DNS o E/S de disco produce la misma forma de síntoma por una razón no relacionada, así que confirme que el hilo está realmente dentro de read antes de culpar al control de flujo.

Revise la cola de envío del peer y la ventana que vio por última vez. Si el peer retiene datos porque su ventana es cero, la corrección está en su ruta de ACK, no en la lectura de TUN.

ENOBUFS es un problema de tamaño, no de falta de búfer

Una escritura que devuelve ENOBUFS en una interfaz TUN suele rechazar la longitud del paquete. La interfaz tiene una MTU, un paquete mayor no se puede encolar, y reintentar el mismo paquete falla igual cada vez. Ese determinismo es la pista. Una falta real de búfer se despeja cuando baja la presión, mientras que un rechazo por tamaño no se preocupa por cuánto espere.

TCP tiene un mecanismo justo para esto, y es el paso que la gente se salta: el clamp de MSS. Cuando sintetiza segmentos en espacio de usuario usted elige el tamaño de la carga útil, y elegirlo de la MTU de la interfaz local en lugar de la MTU del túnel emite segmentos que el túnel no puede transportar. Limítelo al menor de los dos. El clamp solo cubre TCP, así que un datagrama UDP demasiado grande debe ser fragmentado o descartado por su código.

Registre la longitud de cada paquete que escribe mientras depura. La longitud que falla es la respuesta, y no cambiará entre reintentos.

Tres mediciones que separan los casos

No necesita un depurador para ninguno de los dos fallos. Tres observaciones bastan para dirigirse a la sección correcta, y tardan menos que una recompilación.

What you seeWhere to look
Reader parked, peer send queue empty, last advertised window zeroWindow updates in your ACK path
Write fails on one length and succeeds on a smaller oneMSS clamp and tunnel MTU
Both directions look fine, app still sees no dataProtected socket and routing loop

Si coincide la tercera fila, el bucle está bien y el problema está donde el túnel no puede verlo: un socket de transporte sin proteger que lleva su propio tráfico VPN de vuelta al túnel, lo que suele aparecer como una conexión que tiene éxito y luego no entrega nada.

Repare la instalación de la que dependen ambos caminos

Ningún síntoma merece la pena perseguirse hasta que la interfaz esté bien construida. Las llamadas del builder que importan son la dirección, la ruta, la MTU y el consentimiento, en ese orden:

Builder()
  .addAddress(10.8.0.2, 32)
  .addRoute(0.0.0.0, 0)
  .setMtu(1400)
  .establish()

Fije la MTU al valor real del túnel en lugar de dejarla en lo que usó el ejemplo. Todo lo que escriba después queda acotado por ese número, y una discrepancia entre la MTU que fija y el MSS al que limita es la forma más común de reintroducir el fallo de escritura tras una corrección. Luego proteja el socket que transporta el tráfico al propio servidor VPN. Sin protect, esos paquetes se enrutan de vuelta a su propio túnel y la conexión nunca se completa, lo que parece una lectura atascada pero es un bucle de enrutamiento.

El consentimiento no se puede saltar. Un servicio iniciado sin que prepare devuelva null no obtiene interfaz, y establish devuelve null en lugar de lanzar, así que compruebe el descriptor antes de iniciar el bucle. La referencia de VpnService es el lugar para confirmar el contrato del builder en lugar de copiar un ejemplo.

Verifique una dirección a la vez

Envíe un eco UDP por el túnel y observe las longitudes de paquete antes de tocar TCP. Luego abra un handshake y confirme que los segmentos se quedan por debajo del MSS que calculó. Cuente paquetes en lugar de conexiones, para que un atasco aparezca como un hueco en el contador en lugar de un socket colgado. Si las lecturas se detienen tras una ráfaga de escrituras, la ventana es la sospechosa; si las escrituras fallan de inmediato, lo es la longitud.

Después reinicie el dispositivo y repita una vez, porque un VpnService que solo funciona hasta el primer cambio de red es un problema de enrutamiento y no de control de flujo.

Artículos relacionados