A verificação que a maioria das apps Android usa para detetar uma VPN é um nome de interface, e nunca foi uma verificação de VPN. Relata que existe alguma interface com a string que o programador esperava, o que num dispositivo significa OpenVPN e noutro significa algo sem qualquer relação. Um ping é o mesmo tipo de prova: prova que um destino respondeu, não que um túnel transportou o pacote. O que a plataforma expõe realmente é um conjunto de redes e os transportes que cada uma declara, e isso é outra pergunta com outra resposta.

Mito: se tun0 existe, há uma VPN ativa

As apps de VPN para Android são implementações de VpnService que entregam ao sistema um descritor de ficheiro tun. O nome da interface vem de quem a criou: o OpenVPN costuma produzir tun0, o PPTP e o L2TP produzem ppp0, o WireGuard dá à interface o nome do túnel, e um túnel IPsec pode não apresentar qualquer nome convencional. Ler /proc/net/dev ou enumerar objetos NetworkInterface procura, portanto, uma convenção e não um facto.

Ambas as direções do erro estão vivas. Um dispositivo pode ter tun0 para um túnel local que nada tem a ver com a VPN do utilizador, e um túnel WireGuard perfeitamente saudável pode ser invisível para uma verificação que só conhece a string tun0. Falsos positivos e falsos negativos na mesma função tornam-na inútil como diagnóstico, porque um pedido de suporte que diz «nenhuma VPN detetada» não é fiável em nenhuma direção.

O acesso também se estreitou. As versões mais recentes do Android limitam o que uma app comum pode ler de /proc, pelo que uma verificação baseada no nome que funcionava no Android 8 pode devolver uma lista vazia num dispositivo posterior sem qualquer alteração de código. Quando a verificação falha silenciosamente para o lado aberto, a app conclui que não há VPN num hardware onde o túnel está bem.

Mito: um ping bem-sucedido significa que o túnel transporta tráfego

Um ping a um host interno tem sucesso sempre que exista uma rota para esse host. Com túnel dividido, o host pode estar acessível por Wi-Fi enquanto o túnel está ativo e faz algo completamente diferente. Com uma VPN por app, a app que faz o ping pode estar excluída do túnel, pelo que o eco ICMP sai fora dele e volta, o que prova o contrário do que a verificação assume.

O ICMP e o TCP também são filtrados separadamente. Um túnel que permite pedidos de eco mas descarta a porta TCP de que um serviço precisa passará o teste de ping e falhará na aplicação, e isso é uma regra de firewall do outro lado, não uma propriedade do dispositivo. A única leitura honesta de um ping bem-sucedido é que este destino respondeu a este protocolo neste momento.

O que a plataforma relata realmente

Desde a API 21, o ConnectivityManager.getAllNetworks() devolve todas as redes que o sistema conhece, e o getNetworkCapabilities(network) relata o que essa rede declara. Uma rede VPN transporta TRANSPORT_VPN. Percorrer todas as redes responde a uma pergunta: há alguma VPN ativa neste dispositivo agora?

Uma segunda pergunta, mais estreita, exige outra chamada. O getActiveNetwork() devolve a rede que o tráfego da app usaria agora, e verificar a mesma capacidade nela responde: o meu tráfego passaria por uma VPN? As duas podem discordar, e essa discordância é informativa. Uma VPN ativa mas não atual aponta para encaminhamento por app ou para uma configuração de túnel dividido, não para um túnel em falta.

Network active = cm.getActiveNetwork();
NetworkCapabilities caps = cm.getNetworkCapabilities(active);
boolean viaVpn = caps != null
    && caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN);

Proteja ambas as chamadas. As capacidades podem ser null para uma rede que desapareceu entre a listagem e a consulta, e a rede ativa pode ser null durante uma transferência. O NET_CAPABILITY_NOT_VPN é útil na outra direção, para confirmar que uma rede não é de todo um túnel em vez de apenas lhe faltar o indicador de transporte VPN.

O NetworkInfo era o mesmo mito com uma API

O ConnectivityManager.TYPE_VPN com NetworkInfo é a segunda geração desta verificação, e está obsoleto por uma razão. Nas versões modernas o objeto devolvido para uma VPN pode ser null enquanto o túnel está ligado, pelo que o código que trata null como «sem VPN» relata silenciosamente um falso negativo nos dispositivos onde a resposta mais importa.

A transmissão que normalmente a acompanha também não é melhor. O CONNECTIVITY_CHANGE tem um histórico documentado de transições de VPN perdidas, incluindo compilações em que só disparava quando uma VPN se ligava ou desligava em vez de em cada mudança de rede. Um recetor construído sobre ela produz uma variável de estado que se afasta da visão da plataforma até à próxima reconexão completa.

Reaja aos callbacks em vez de sondar

Registe um NetworkRequest que exija TRANSPORT_VPN e remova NET_CAPABILITY_NOT_VPN, e depois chame registerNetworkCallback com esse pedido. O onAvailable e o onLost dizem-lhe então quando a declaração muda, e é esse o evento que realmente lhe interessa. Cancele o registo do callback quando o componente parar, ou a app mantém uma referência a um ecrã morto e o callback continua a disparar.

NetworkRequest req = new NetworkRequest.Builder()
    .addTransportType(NetworkCapabilities.TRANSPORT_VPN)
    .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
    .build();
cm.registerNetworkCallback(req, callback);

Isto exige ACCESS_NETWORK_STATE, que é uma permissão normal: sem root, sem diálogo de consentimento do utilizador, sem permissão de VPN. Sondar com um temporizador é a alternativa a que as pessoas recorrem, e consome bateria enquanto continua a perder uma transição mais curta do que o intervalo, que é exatamente a transição que um utilizador relata.

O que a resposta ainda não lhe pode dizer

Nenhuma destas API relata se o túnel está saudável, em que servidor termina ou se o tráfego de uma determinada app está dentro dele. Uma rede VPN pode existir sem rotas utilizáveis, e um túnel por app pode excluir a app que faz a pergunta.

Por isso registe o que a verificação viu, não apenas o booleano. Registe o identificador de rede, os transportes declarados e se essa rede estava ativa. Um relatório que diz «nenhuma VPN detetada» é inutilizável, enquanto um que diz que uma rede declarou TRANSPORT_VPN mas o getActiveNetwork() devolveu Wi-Fi aponta diretamente para o encaminhamento ou para uma lista de exclusões, e isso é uma correção sobre a qual alguém pode trabalhar.

Artigos relacionados