Por que seu túnel Android VpnService para de rotear o tráfego
Quatro crenças sobre o VpnService que quebram em produção: um túnel por vez, escopo das rotas, listas por aplicativo e ordem de desligamento, com uma verificação para cada.
Quatro suposições sobre o Android VpnService já me custaram a maior parte de uma semana, e nenhuma delas era sobre o protocolo de rede. O aplicativo dizia conectado, o ícone da chave ficava na barra de status, e o tráfego continuava saindo pela rota normal. Eu seguia supondo que o túnel era meu até eu pará-lo, que as rotas que eu adicionava eram as rotas que eu obtinha, e que chamar stopSelf levaria a interface junto. O serviço se comportava exatamente como documentado. A parte quebrada era meu modelo mental.
Mito 1: o túnel fica de pé até você pará-lo
O Android mantém uma interface VPN por vez. Estabeleça uma segunda e a plataforma não a coloca em fila: ela desativa a sua e chama onRevoke() no seu serviço. Se o seu código só desmonta ao apertar um botão, uma tomada de controle deixa para trás um descritor e uma thread leitora, e a próxima inicialização herda os dois.
A referência do VpnService diz explicitamente que prepare() retorna null quando o aplicativo já tem consentimento e um Intent quando o usuário ainda precisa aprovar. Esse consentimento só chega se você entregar a intenção via startActivityForResult(). Pule a entrega e establish() retorna null, o que se lê como falha silenciosa e não como problema de permissão.
O Always-on VPN adiciona um dono que você não controla. O sistema pode trazer o túnel de volta após um boot ou após um crash, e ele passa à frente de um aplicativo VPN que o usuário iniciou manualmente. Construa onRevoke() para fechar os mesmos recursos do seu desligamento normal, e trate isso como caminho principal de parada e não como caso extremo. Usuários trocam de aplicativo VPN; seu código precisa sobreviver a isso.
Mito 2: addRoute captura exatamente o que você listou
As rotas decidem quais pacotes chegam ao seu descritor, e um erro de escopo produz o caso exasperante em que parte do tráfego passa pelo túnel e parte não. addRoute("0.0.0.0", 0) pega todo o IPv4, o que significa que cada pacote agora chega até você e precisa ser encaminhado ou descartado. Um /32 para um único host não pega quase nada, e todo o resto se comporta como se o seu VPN nunca tivesse sido instalado.
As famílias são contadas separadamente. Uma rota só IPv4 deixa o IPv6 na rede subjacente, então um host de pilha dupla ainda pode resolver e conectar fora do túnel enquanto parece perfeitamente conectado dentro dele.
O DNS segue a mesma lógica. Servidores adicionados com addDnsServer() vão para os aplicativos que usam o resolvedor do túnel, enquanto um aplicativo com endpoint DNS criptografado próprio ou Private DNS definido para um nome de host os ignora por completo. Quando nomes falham para um aplicativo e funcionam para outro, verifique o caminho do resolvedor antes de tocar em uma única rota.
Mito 3: regras por aplicativo são um interruptor que você vira para qualquer lado
addDisallowedApplication() e addAllowedApplication() são dois modos, não dois filtros. O construtor aceita um modelo por túnel, então um aplicativo que quer tudo exceto três pacotes usa a lista de proibidos e deixa a lista de permitidos vazia. Os dois são mutuamente exclusivos, e as pessoas descobrem isso em tempo de execução depois que a tela de configurações já ofereceu ambos os interruptores.
A visibilidade de pacotes também morde. No Android 11 e adiante você só pode nomear pacotes que consegue ver, então uma lista de exclusões construída a partir de uma consulta que seu aplicativo não pode executar vai perder entradas em silêncio. Compare a lista com o que o gerenciador de pacotes realmente retorna antes de confiar nela.
Os perfis de trabalho dividem o público novamente. Um túnel iniciado dentro do perfil cobre esse perfil e seus aplicativos, enquanto o lado pessoal mantém a própria conectividade. É uma decisão de escopo tomada uma vez, e ela pertence à interface, não a um ticket de suporte.
Mito 4: stopSelf encerra o túnel
Não encerra. A interface vive no descritor retornado por establish(), e o objeto de serviço pode morrer enquanto esse descritor permanece aberto. Feche o ParcelFileDescriptor primeiro, solte a referência, junte ou interrompa as threads de leitura e escrita, e então chame stopForeground(true) e stopSelf() nessa ordem.
Um descritor esquecido é a razão habitual pela qual um serviço parece sobreviver à própria solicitação de parada, e explica uma rota obsoleta que vive mais que o aplicativo que a adicionou.
O erro inverso é igualmente comum: fechar o descritor enquanto uma thread leitora está no meio da leitura, e depois deixar essa thread lidar com uma exceção para a qual ninguém escreveu nada. Mantenha a sequência de desligamento em um só lugar e faça tanto a ação do usuário quanto onRevoke() chamarem ali.
O que verificar quando o túnel parece morto
Proteja os sockets de vida longa com protect() antes que o túnel suba. Sem isso, o transporte do próprio túnel é roteado de volta para dentro do túnel e a conexão trava de um jeito que parece exatamente uma falha de servidor. Teste contra um endereço que só existe atrás da rede remota, não contra um site público que responde do cache ou do caminho subjacente.
Registre o resultado de establish(), as rotas, os pacotes excluídos e o motivo do desligamento. Essas quatro linhas separam um problema de consentimento de um problema de escopo e de um problema de ciclo de vida sem captura de pacotes.