O túnel sobe, o ícone da chave aparece e cada pedido no navegador continua a expirar. Numa aplicação VpnService essa combinação costuma significar que o ciclo de leitura está vivo e a metade de saída está quebrada, não que o servidor rejeitou o handshake. O Android já criou a interface, moveu o tráfego da aplicação para dentro dela e entregou ao seu processo um descritor de ficheiro. Tudo depois desse descritor é código seu, e uma chamada em falta basta para engolir todo o fluxo.

O que o sistema dá e o que não dá

establish() devolve um ParcelFileDescriptor para uma interface tun. O sistema escreve pacotes IP de saída lá dentro e espera que o seu processo os leia, decida para onde vão e os envie por um socket comum. Por baixo não há reencaminhamento do kernel, por isso quando ninguém lê os pacotes são descartados e os contadores da interface nunca se mexem.

Comece por contar em vez de adivinhar. Incremente um contador em cada leitura bem-sucedida e registe o destino e o protocolo dos primeiros pacotes. Se o contador ficar em zero enquanto é gerado tráfego, a falha está acima do código de reencaminhamento, o que reduz a busca à configuração do builder e ao modo de leitura. Se o contador se mover e o navegador continuar a falhar, a falha está no caminho de envio ou no de regresso.

A leitura bloqueante decide se o ciclo avança

setBlocking(true) coloca o descritor em modo bloqueante e existe desde a API 21. Em versões antigas a solução é um pequeno auxiliar JNI que limpa O_NONBLOCK com fcntl no descritor em bruto. O modo importa porque decide o que uma leitura sem pacote pendente pode fazer.

Em modo não bloqueante essa leitura regressa de imediato com um valor negativo, e um ciclo que ignora o resultado gira um núcleo sem mover tráfego nenhum. Em modo bloqueante a leitura espera, que é o que um fio de reencaminhamento quer, mas o fio deixa de poder ser interrompido. Fechar o descritor é a forma suportada de fazer a leitura falhar para o ciclo poder sair. Matar o processo ou interromper o fio pode deixar o arranque seguinte com um descritor que já não corresponde à interface.

Leia um pacote por chamada para um buffer do tamanho da MTU da interface, e trate o zero ou um retorno negativo como erro, não como pacote vazio.

protect() antes do primeiro envio, não depois

Cada socket que transporta um pacote encapsulado tem de ser protegido primeiro. Um socket sem proteção obedece às mesmas regras de encaminhamento que acabou de instalar, por isso os pacotes voltam para o túnel e o seu próprio ciclo de leitura apanha-os outra vez. O túnel passa então a reencaminhar a sua própria saída sem fim, e de fora parece uma ligação que abre e nunca responde.

A ordem é protect, depois connect ou bind, depois send. Chamar protect depois de connect pode deixar de pé a decisão de rota anterior, por isso o socket continua a usar o caminho errado mesmo que a chamada tenha devolvido true. Verifique o booleano devolvido e falhe ruidosamente quando for false, em vez de entrar num envio que não pode funcionar.

Um erro de permissão de sendto num socket que nunca tocou em protect() é a assinatura habitual deste erro.

O exemplo UDP não transporta HTTPS

O exemplo de reencaminhamento que quase todos os tutoriais copiam trata UDP e mais nada. O TCP precisa de handshake, números de sequência, confirmações e retransmissão, e um ciclo que copia cargas entre um descritor tun e um socket não fornece nada disso. É por isso que uma consulta DNS por UDP pode passar enquanto cada pedido HTTPS encrava. Ou corre uma pilha TCP em espaço de utilizador, ou entrega as ligações a um proxy local que termina o TCP no dispositivo e reencaminha o fluxo.

Teste a divisão antes de reescrever seja o que for: envie uma consulta UDP e abra uma ligação TCP para o mesmo host. Se o caminho UDP funciona e o caminho TCP encrava no primeiro SYN, o problema é o código de reencaminhamento e não o servidor. Essa única comparação poupa muito tempo gasto a ler registos do servidor que não contêm nada de útil.

A lista de rotas e os filtros por aplicação limitam o que vê

addAddress, addRoute e addDns definem o que o sistema envia para o túnel. Um destino fora da lista de rotas nunca chega ao seu descritor, por isso nenhuma quantidade de lógica de reencaminhamento o corrige. Registe as chamadas ao builder que faz e compare-as com os endereços que a aplicação tenta realmente alcançar.

A seleção por aplicação tem o seu próprio mínimo: addAllowedApplication e addDisallowedApplication exigem a API 21, e em versões antigas os métodos simplesmente não existem. Um desenho que filtra por pacote não pode correr ali de todo.

Restabelecer deixa descritores obsoletos

Uma nova chamada a establish() devolve um descritor novo e fecha a interface anterior. O descritor antigo fica inválido, por isso um fio ainda bloqueado nele recebe um erro, e os sockets criados para o caminho antigo podem ter de ser substituídos. onRevoke é a chamada de retorno para um túnel que o utilizador ou o sistema retiraram. Manter o ciclo de vida num único dono, que para o leitor antes de restabelecer, evita a maioria dos estados ligado-mas-morto.

Leia cada erro conforme o passo a que pertence. Um reinício de ligação vindo de recvfrom significa que o extremo remoto fechou enquanto o seu código ainda lia. Um erro de endereço inválido aponta para a análise do cabeçalho ou para um erro de deslocamento, não para a rede.

Ordem de trabalho: modo bloqueante ligado, um pacote por leitura, cabeçalho analisado, socket protegido, carga enviada, fecho limpo na entrega e uma lista de rotas que cubra o destino.

Artigos relacionados