Dois sintomas aparecem no mesmo laço do VpnService e ambos são atribuídos à mesma coisa: um leitor que nunca retorna e uma escrita que falha com ENOBUFS. Não são variações de um único bug. Uma leitura travada é uma discordância sobre controle de fluxo com o peer; uma escrita falhada é um pacote que a interface não aceitará. Se os tratar como um só problema, passará um dia ajustando buffers que nunca estiveram envolvidos. A divisão abaixo é como decido qual metade do laço instrumentar primeiro.

Um laço, dois pontos de falha independentes

Seu serviço lê um pacote IP do descritor de arquivo TUN, inspeciona-o e escreve uma resposta de volta no mesmo descritor. Tudo o que quebra nesse caminho surge em uma dessas duas chamadas, e é por isso que ambas são reportadas como o túnel quebrado. As chamadas têm contratos diferentes. Uma leitura espera que o kernel lhe entregue um pacote, e uma escrita entrega ao kernel um pacote que ele tem direito de recusar.

As leituras são a metade fácil de raciocinar. Uma leitura que não devolve nada ou espera tráfego que nunca chega ou espera porque o peer parou de enviar.

Uma leitura travada é uma discordância de controle de fluxo

Ler um dispositivo TUN bloqueia até que um pacote esteja disponível. Esse é o comportamento projetado, e é por isso que uma thread parada dentro de read parece normal em um link ocioso. Torna-se um travamento quando sua própria pilha TCP continua aceitando dados do aplicativo local mas nunca anuncia janela suficiente para o peer remoto enviar mais. O peer enche a janela que você anunciou, espera um ACK e uma atualização de janela, e para. Então seu leitor não tem nada para devolver, e o aplicativo vê uma conexão aberta e silenciosa.

As causas comuns são uma pilha que escreve no socket sem acompanhar a janela anunciada, e outra que esquece de enviar uma atualização de janela depois de esvaziar seu buffer. De fora parecem idênticas: bytes saem, nada volta, nenhum erro é levantado. Bloquear a thread de leitura em parsing, resolução DNS ou E/S de disco produz a mesma forma de sintoma por uma razão não relacionada, então confirme que a thread está realmente dentro de read antes de culpar o controle de fluxo.

Verifique a fila de envio do peer e a janela que ele viu por último. Se o peer retém dados porque sua janela é zero, a correção está no seu caminho de ACK, não na leitura TUN.

ENOBUFS é um problema de tamanho, não de falta de buffer

Uma escrita que retorna ENOBUFS em uma interface TUN normalmente rejeita o comprimento do pacote. A interface tem uma MTU, um pacote maior não pode ser enfileirado, e tentar novamente o mesmo pacote falha da mesma forma todas as vezes. Esse determinismo é a pista. Uma falta real de buffer desaparece quando a pressão cai, enquanto uma rejeição de tamanho não se importa com quanto tempo você espera.

O TCP tem um mecanismo exatamente para isso, e é o passo que as pessoas pulam: o clamp de MSS. Quando você sintetiza segmentos no espaço do usuário, escolhe o tamanho do payload, e escolhê-lo da MTU da interface local em vez da MTU do túnel emite segmentos que o túnel não consegue carregar. Limite ao menor dos dois. O clamp cobre apenas TCP, então um datagrama UDP grande demais precisa ser fragmentado ou descartado pelo seu código.

Registre o comprimento de cada pacote que você escreve enquanto depura. O comprimento que falha é a resposta, e não mudará entre tentativas.

Três medições que separam os casos

Você não precisa de um depurador para nenhuma das duas falhas. Três observações bastam para direcioná-lo à seção correta, e levam menos tempo que uma recompilação.

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

Se a terceira linha corresponder, o laço em si está bem e o problema está onde o túnel não consegue vê-lo: um socket de transporte desprotegido que leva seu próprio tráfego VPN de volta ao túnel, o que costuma aparecer como uma conexão que tem sucesso e depois não entrega nada.

Conserte o encanamento do qual ambos os caminhos dependem

Nenhum sintoma vale a pena ser perseguido antes que a interface esteja corretamente construída. As chamadas do builder que importam são o endereço, a rota, a MTU e o consentimento, nessa ordem:

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

Defina a MTU para o valor real do túnel em vez de deixá-la no que o exemplo usou. Tudo o que você escrever depois fica limitado por esse número, e uma discrepância entre a MTU que você define e o MSS ao qual limita é a forma mais comum de reintroduzir a falha de escrita após uma correção. Depois proteja o socket que transporta o tráfego para o próprio servidor VPN. Sem protect, esses pacotes são roteados de volta para o seu próprio túnel e a conexão nunca se completa, o que parece uma leitura travada mas é um laço de roteamento.

O consentimento não pode ser contornado. Um serviço iniciado sem que prepare retorne null não obtém interface, e establish retorna null em vez de lançar, então verifique o descritor antes de iniciar o laço. A referência do VpnService é o lugar para confirmar o contrato do builder em vez de copiar um exemplo.

Verifique uma direção de cada vez

Envie um eco UDP pelo túnel e observe os comprimentos dos pacotes antes de tocar no TCP. Depois abra um handshake e confirme que os segmentos permanecem abaixo do MSS que você calculou. Conte pacotes em vez de conexões, para que um travamento apareça como uma lacuna no contador em vez de um socket pendurado. Se as leituras param após uma rajada de escritas, a janela é a suspeita; se as escritas falham imediatamente, é o comprimento.

Depois reinicie o dispositivo e repita uma vez, porque um VpnService que só funciona até a primeira mudança de rede é um problema de roteamento e não de controle de fluxo.

Artigos relacionados