O túnel dividido em um app VpnService quebra em alguns pontos concretos, e a correção é diferente para cada um. O cliente mostra apenas um destino que falha, então o primeiro trabalho é descobrir qual fronteira você está atingindo: as rotas que o builder recebeu, o proxy que você não consegue autenticar, o resolver que só anunciou ou a lista de apps que a plataforma aplica por usuário. É por isso que mantenho uma verificação em quatro passos. Ela roda na ordem abaixo e para na primeira falha.

Confirme o que o builder entregou à plataforma

VpnService.Builder é uma lista de declarações. establish() retorna um ParcelFileDescriptor para uma interface virtual, e a plataforma só envia para esse descritor o tráfego que corresponde aos prefixos de addAddress e addRoute; addDnsServer diz aos clientes qual resolver consultar. Um destino fora de todos os prefixos que você adicionou nunca chega ao seu app, por mais que a interface prometa. Esse único fato explica a maioria dos relatos de túnel dividido quebrado antes de qualquer mudança de código.

Registre as listas de endereços, rotas e DNS que o builder montou, e compare com os destinos que falham. Um /32 ausente para um host de gerenciamento é muito mais comum que um túnel quebrado. Se a lista estiver certa, siga adiante; se estiver errada, o resto da lista de verificação é esforço perdido.

excludeRoute só existe a partir da API 33

A exclusão por destino é a solicitação que mais vira tópico de suporte. builder.excludeRoute(IpPrefix) chegou na API 33; antes disso, a única aproximação suportada é adicionar rotas para tudo menos o prefixo excluído, o que significa calcular você mesmo os prefixos complementares. As duas chamadas não são imagens espelhadas: uma subtrai do conjunto de rotas da interface, a outra constrói o conjunto do zero, e um aparelho mais antigo lançará ou ignorará em silêncio uma chamada que não reconhece. Ramifique por Build.VERSION.SDK_INT e teste os dois caminhos em hardware real.

Ao calcular complementos, observe o alinhamento dos prefixos. Uma exclusão /24 dentro de uma rota /16 precisa dos prefixos de cobertura que realmente descrevem o restante, não de um /8 malformado. Registre a lista final e confirme que o host excluído ainda resolve por um caminho fora do túnel.

As credenciais do proxy não são expressáveis

setHttpProxy(ProxyInfo) aponta a interface para um proxy HTTP ou SOCKS, e uma URL PAC pode escolher entre eles por destino. Nem ProxyInfo nem o formato PAC carregam usuário e senha, então um proxy autenticado fica fora da API da plataforma. Apps que precisam dele costumam embutir uma pilha de espaço de usuário, tun2socks ou um motor semelhante, e respondem elas mesmas ao desafio 407.

Essa escolha tem consequências. Uma pilha de espaço de usuário só vê o tráfego que você roteia para ela, então precisa viver dentro da mesma fronteira do descritor, e herda o mesmo requisito de protect() para o próprio socket de upload. Se você pular protect(), o túnel captura o próprio transporte, o que aparece como um handshake que trava após o primeiro pacote. Teste contra um proxy que exija credenciais, porque um proxy aberto passa pelo caminho da plataforma e não prova nada.

Listas por app e DNS também são declarações

Builder tem addAllowedApplication e addDisallowedApplication, e eles são mutuamente exclusivos: chamar os dois lança exceção. De qualquer forma a lista é aplicada por usuário na configuração da interface, então um app do perfil de trabalho é regido pelo túnel do perfil de trabalho e não pelo seu. O que nenhuma API dá é controle sobre a decisão de outro app: você não pode empurrar um pacote para o seu túnel de fora, nem fechar ou reconfigurar uma VPN já ativa.

O DNS segue o mesmo padrão. addDnsServer anuncia um endereço de resolver; não permite que o seu app responda consultas. Se quiser resolver nomes sozinho, execute um resolver e roteie o endereço dele para o túnel. Apps que usam DoT ou DoH para um nome de host fixo podem contornar o endereço anunciado, então trate a resolução de nomes como melhor esforço, a menos que também intercepte o caminho da consulta.

Manifesto, always-on e o teste de quatro passos

A elegibilidade é declarada no manifesto: BIND_VPN_SERVICE, o filtro de intent android.net.VpnService e um serviço em primeiro plano se o túnel precisar sobreviver em segundo plano. Always-on VPN exige ainda um onRevoke limpo, porque o sistema o chama quando o usuário muda configurações ou outro app assume o controle.

Execute as verificações em ordem e pare na primeira falha. Primeiro, extraia as listas de endereços, rotas e DNS entregues ao builder. Segundo, confirme que establish() retornou um descritor e que o socket de transporte está protegido. Terceiro, peça um destino dentro de um prefixo incluído e outro dentro de um prefixo excluído. Quarto, verifique qual resolver respondeu de fato consultando um nome de host que só o seu resolver consegue resolver.

Um handshake concluído não prova nenhum dos passos seguintes. Cada verificação isola uma camada, então um sucesso no segundo passo não diz nada sobre o quarto.

A referência do VpnService.Builder lista toda a superfície do builder. Tudo o que você precisar além disso é outra API, outro app ou a sua própria pilha de espaço de usuário, e isso é uma decisão de projeto, não um bug do túnel dividido.

Artigos relacionados