Android VpnService: lecturas TUN bloqueadas fronte a erros de escritura ENOBUFS
Unha lectura que nunca volve e unha escritura que falla con ENOBUFS teñen causas opostas. Mide a ventá anunciada e a lonxitude do paquete para escoller a corrección.
Dous síntomas aparecen no mesmo bucle de VpnService e cúlpase do mesmo: un lector que nunca volve, e unha escritura que falla con ENOBUFS. Non son variantes dun só erro. Unha lectura atascada é un desacordo sobre o control de fluxo co peer; unha escritura fallida é un paquete que a interface non aceptará. Se os tratas como un só problema, pasarás un día axustando buffers que nunca estiveron implicados. A división de abaixo é como decido que metade do bucle instrumentar primeiro.
Un bucle, dous puntos de fallo independentes
O teu servizo le un paquete IP do descritor de ficheiro TUN, inspeciona e escribe unha resposta de volta ao mesmo descritor. Todo o que rompe nese camiño aparece nunha desas dúas chamadas, e por iso ambas se reportan como o túnel roto. As chamadas teñen contratos distintos. Unha lectura espera que o kernel che entregue un paquete, e unha escritura entrega ao kernel un paquete que ten dereito a rexeitar.
As lecturas son a metade fácil de razoar. Unha lectura que non devolve nada ou espera tráfico que nunca chega ou espera porque o peer deixou de enviar.
Unha lectura atascada é un desacordo de control de fluxo
Ler un dispositivo TUN bloquea ata que hai un paquete dispoñible. Ese é o comportamento deseñado, e por iso un fío aparcado dentro de read parece normal nunha ligazón inactiva. Convértese nun colgue cando a túa propia pila TCP segue aceptando datos da aplicación local pero nunca anuncia suficiente ventá para que o peer remoto envie máis. O peer enche a ventá que anunciaches, espera un ACK e unha actualización de ventá, e detense. Entón o teu lector non ten nada que devolver, e a aplicación ve unha conexión aberta e silenciosa.
As causas habituais son unha pila que escribe no socket sen seguir a ventá anunciada, e outra que esquece enviar unha actualización de ventá tras baleirar o seu buffer. Desde fóra parecen idénticas: saen bytes, non volve nada, non se produce ningún erro. Bloquear o fío do lector en parseo, resolución DNS ou E/S de disco produce a mesma forma de síntoma por unha razón non relacionada, así que confirma que o fío está realmente dentro de read antes de culpar o control de fluxo.
Revisa a cola de envío do peer e a ventá que viu por última vez. Se o peer retén datos porque a túa ventá é cero, a corrección está na túa ruta de ACK, non na lectura TUN.
ENOBUFS é un problema de tamaño, non de falta de buffer
Unha escritura que devolve ENOBUFS nunha interface TUN adoita rexeitar a lonxitude do paquete. A interface ten unha MTU, un paquete maior non se pode encolar, e reintentar o mesmo paquete falla igual cada vez. Ese determinismo é a pista. Unha falta real de buffer despexa cando baixa a presión, mentres que un rexeitamento por tamaño non se preocupa por canto esperes.
TCP ten un mecanismo xusto para isto, e é o paso que a xente salta: o clamp de MSS. Cando sintetizas segmentos en espazo de usuario elixes o tamaño da carga útil, e escollelo da MTU da interface local en lugar da MTU do túnel emite segmentos que o túnel non pode transportar. Limita ao menor dos dous. O clamp só cobre TCP, así que un datagrama UDP demasiado grande debe ser fragmentado ou descartado polo teu código.
Rexistra a lonxitude de cada paquete que escribes mentres depuras. A lonxitude que falla é a resposta, e non cambiará entre reintentos.
Tres medicións que separan os casos
Non necesitas un depurador para ningún dos dous fallos. Tres observacións abondan para dirixirte á sección correcta, e levan menos tempo que unha recompilación.
| What you see | Where to look |
|---|---|
| Reader parked, peer send queue empty, last advertised window zero | Window updates in your ACK path |
| Write fails on one length and succeeds on a smaller one | MSS clamp and tunnel MTU |
| Both directions look fine, app still sees no data | Protected socket and routing loop |
Se coincide a terceira fila, o bucle está ben e o problema está onde o túnel non o pode ver: un socket de transporte sen protexer que leva o teu propio tráfico VPN de volta ao túnel, o que adoita aparecer como unha conexión que ten éxito e despois non entrega nada.
Repara a instalación da que dependen ambos camiños
Ningún síntoma paga a pena perseguilo ata que a interface estea ben construída. As chamadas do builder que importan son o enderezo, a ruta, a MTU e o consentimento, nesa orde:
Builder()
.addAddress(10.8.0.2, 32)
.addRoute(0.0.0.0, 0)
.setMtu(1400)
.establish()Fixa a MTU ao valor real do túnel en lugar de deixala no que usou o exemplo. Todo o que escribas despois queda limitado por ese número, e unha discrepancia entre a MTU que fixas e o MSS ao que limitas é a forma máis común de reintroducir o fallo de escritura tras unha corrección. Despois protexe o socket que transporta o tráfico ao propio servidor VPN. Sen protect, eses paquetes enrútanse de volta ao teu propio túnel e a conexión nunca se completa, o que parece unha lectura atascada pero é un bucle de enrutamento.
O consentimento non se pode saltar. Un servizo iniciado sen que prepare devolva null non obtén interface, e establish devolve null en lugar de lanzar, así que comproba o descritor antes de iniciar o bucle. A referencia de VpnService é o lugar para confirmar o contrato do builder en lugar de copiar un exemplo.
Verifica unha dirección á vez
Envía un eco UDP polo túnel e observa as lonxitudes de paquete antes de tocar TCP. Despois abre un handshake e confirma que os segmentos quedan por baixo do MSS que calculaches. Conta paquetes en lugar de conexións, para que un atasco apareza como un oco no contador en lugar dun socket colgado. Se as lecturas se deteñen tras unha ráfaga de escrituras, a ventá é a sospeitosa; se as escrituras fallan de inmediato, é a lonxitude.
Despois reinicia o dispositivo e repite unha vez, porque un VpnService que só funciona ata o primeiro cambio de rede é un problema de enrutamento e non de control de fluxo.