VpnService protect() ou exclusão do app: o que corrige o roteamento
Proteja o socket que carrega o túnel ou exclua todo o app: as três condições que decidem de que escopo sua VPN Android precisa.
Há duas formas de manter os pacotes do próprio túnel fora do túnel: proteger o socket de transporte ou tirar o app inteiro da VPN. Elas resolvem problemas diferentes, e escolher a mais ampla porque é mais fácil de ligar é exatamente como os apps perdem acesso à rede local e o DNS dividido sem perceber. Comece pelo que precisa ficar fora do túnel e então escolha o menor escopo que alcança isso.
O que o protect() remove do túnel
VpnService.protect() é uma chamada por socket. Você entrega a ele o socket ou o descritor que carrega o túnel, e o Android marca esse socket para que seu tráfego use a rede subjacente em vez da interface da VPN. Todo o resto que o app envia continua seguindo a rota normal da VPN.
Esse escopo estreito é o ponto. Um socket protegido não muda como seu cliente HTTP resolve nomes, qual interface suas chamadas de análise usam ou o que acontece com uma segunda conexão aberta mais tarde.
Três condições que decidem o escopo
A primeira condição é se apenas o transporte do túnel precisa escapar. Se o restante do tráfego do app deve continuar passando pela VPN, a proteção do socket é o escopo correto. A segunda condição é se você controla a criação do socket. Uma biblioteca que abre o próprio socket lá no fundo não deixa onde chamar protect() a não ser que exponha um hook ou aceite um socket já construído. A terceira condição é se o app precisa alcançar diretamente a rede local ou um captive portal, o que é um requisito diferente de manter o túnel vivo.
Excluir o app com addDisallowedApplication() é a ferramenta bruta. Ela remove da VPN toda conexão que o app faz, inclusive as que você queria inspecionar, e em alguns aparelhos também muda o comportamento da descoberta local.
Uma terceira opção é vincular o transporte a uma rede escolhida com bindProcessToNetwork() ou com um identificador de rede no nível do socket. Isso é útil quando o aparelho tem Wi-Fi e celular ao mesmo tempo e você quer que o túnel passe por um enlace de subida específico.
Chame-o enquanto o transporte está sendo criado
Chame protect() no mesmo bloco que cria o transporte, antes de connect() ou send(). Se a chamada retornar false, feche o socket e faça a configuração falhar; continuar deixa você com um transporte cujos pacotes podem voltar para a sua própria interface.
A forma é pequena o bastante para caber num só lugar:
DatagramSocket transport = new DatagramSocket();
if (!protect(transport)) {
transport.close();
throw new IOException("transport not protected");
}
transport.connect(serverAddress);Três detalhes mordem aqui. Primeiro,o booleano é o único sinal que você recebe, então registre a falha com contexto suficiente para distinguir um consentimento revogado de uma chamada recusada. Segundo,protect() cobre o socket, não o número do descritor: se você fechar o socket e mais tarde reutilizar o mesmo inteiro para outro socket, o novo socket não fica protegido. Terceiro, quando uma biblioteca nativa abre o transporte, proteja o descritor antes de o código nativo se conectar, ou a rota pode já estar escolhida.A referência do VpnService é o lugar onde confirmar quais sobrecargas o seu nível de API de destino realmente oferece.
Teste o caminho bloqueado, não só o caminho feliz
Verifique com o túnel ativo, porque é o único estado em que os dois caminhos podem diferir. Envie uma requisição pelo transporte protegido e outra de um caminho de código que nunca chamou protect(), depois compare o que o lado remoto vê: a conexão protegida deve chegar com a rede subjacente como origem, enquanto a não protegida mostra o endereço do próprio túnel e move seus contadores de bytes. No aparelho,ss ou netstat mostra a mesma divisão antes de qualquer coisa sair do rádio.
Teste a revogação em separado, porque uma mudança de consentimento derruba a interface sem tocar no seu código de socket. Revogue o consentimento da VPN ou pare o serviço, reconstrua o transporte e confirme que protect() ainda retorna true e que o transporte se reconecta assim que o consentimento é concedido de novo.
Se a requisição protegida seguir o túnel mesmo assim, a chamada de proteção provavelmente acontece depois que o socket já foi roteado. Mova-a para antes e teste de novo.
Mantenha a decisão escrita ao lado da configuração do transporte: quais sockets estão protegidos, qual tráfego do app está excluído e o que o app faz quando a proteção falha. É essa linha que impede uma refatoração posterior de ampliar a exclusão para o app inteiro porque um único teste falhava. Ela também dá à próxima pessoa uma forma de conferir a intenção sem fazer engenharia reversa do manifesto.