Como verificar no código se uma VPN está conectada no Android
O Android não tem getter de conexão VPN. Consulte as capacidades de rede, acompanhe mudanças de transporte e mantenha separados o consentimento do app, o estado do aparelho e a prova do servidor.
Você não pode perguntar ao Android se uma VPN está conectada. VpnService não expõe nenhum getter de estado de conexão, e o ícone de chave na barra de status é desenhado pelo sistema, não por algo que seu app possa ler. O que você pode fazer é fazer à pilha de conectividade uma pergunta mais estreita: qual rede o tráfego deste app está usando, e se essa rede carrega o transporte VPN. Este guia percorre as verificações na ordem que evita falsos positivos, de um comando shell no dispositivo até o callback que você deixa rodando no app.
Verifique o dispositivo a partir de um shell antes de tocar no código do app
Primeiro obtenha uma linha de base. adb shell dumpsys connectivity imprime toda rede que o sistema conhece, e cada bloco lista seus transportes, então uma rede VPN aparece com VPN nessa lista. Leia o bloco da rede padrão antes de qualquer outra coisa, depois procure outras redes que carreguem o mesmo transporte.
Duas saídas respondem a perguntas diferentes. Existir uma rede VPN não é o mesmo que seu app estar usando-a, e em um dispositivo com perfil de trabalho ou um segundo usuário ambos podem ser verdadeiros ao mesmo tempo enquanto seu processo fala diretamente com a internet. A linha da rede ativa é a que decide o que seus sockets fazem.
Pergunte qual rede o app está usando
No código, o teste de capacidade são quatro linhas. Solicite ACCESS_NETWORK_STATE no manifesto e depois compare as capacidades da rede ativa com o transporte VPN:
ConnectivityManager cm = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
Network active = cm.getActiveNetwork();
NetworkCapabilities caps = cm.getNetworkCapabilities(active);
boolean vpnInUse = caps != null
&& caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN);
A resposta é deliberadamente estreita: a rede padrão do app é uma VPN neste momento. Ela não diz qual VPN, se é sua, ou se o túnel chega ao outro extremo. Trate um true como “o tráfego sai por algum túnel” e mantenha sua própria contabilidade separada. Um booleano que silenciosamente significa três coisas ao mesmo tempo é como uma tela de configurações acaba relatando a VPN como desligada enquanto o usuário navega por ela.
Quando, ao contrário, você precisa saber se alguma rede VPN está presente, itere cm.getAllNetworks() e teste cada conjunto de capacidades. As duas respostas divergem em um dispositivo que executa uma VPN por app que cobre outros aplicativos mas não o seu, e essa divergência é a origem da maioria dos relatos de bug que dizem que o app não vê uma VPN claramente ligada.
Observe mudanças em vez de fazer polling
Polling com timer é a forma errada para esta pergunta, porque mudanças de estado chegam como eventos. Registre uma requisição que corresponda ao transporte VPN e deixe o framework avisar quando uma rede correspondente aparecer, desaparecer ou mudar:
NetworkRequest request = new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build();
cm.registerNetworkCallback(request, callback);
Uma rede VPN nunca carrega NET_CAPABILITY_NOT_VPN, então a remoção explícita documenta a intenção. Sem ela a requisição ainda corresponde, mas a próxima pessoa a ler o código tem de descobrir isso sozinha.
O registro pertence ao escopo que precisa da observação, geralmente uma tela ou um serviço em primeiro plano. Cancele o registro em onStop ou onDestroy, e não deixe um callback preso a uma activity que já sumiu. Um app sem componente visível é um mau anfitrião para um observador permanente, e os callbacks que você esquece são os que quebram depois de uma mudança de configuração.
Mantenha três perguntas separadas
A maior parte da confusão nesta área vem de tratar três verificações diferentes como uma. Elas usam APIs diferentes, e cada uma pode ser verdadeira enquanto as outras são falsas.
| Question | How you answer it | What a yes means |
|---|---|---|
| Did the user grant VPN consent to my app? | VpnService.prepare() returns null | Consent exists; the tunnel may still be down |
| Is the device sending default traffic through a VPN? | TRANSPORT_VPN on the active network | Something is tunneling, not necessarily your app |
| Did this request arrive through the VPN? | Source address seen by the server | Proof for that connection, nothing wider |
A terceira linha é a que as pessoas tentam responder no cliente, e não pode ser respondida ali. Dê aos clientes VPN a sua própria faixa de endereços e leia o endereço de origem no servidor; é a única prova que sobrevive a um usuário trocar de túnel no meio da sessão.
Ordem de verificação que elimina falsos positivos
Código de exemplo mais antigo ainda em circulação chama getNetworkInfo(ConnectivityManager.TYPE_VPN). Essa chamada está obsoleta e em várias versões retornava null mesmo com uma VPN conectada, e é por isso que tantos trechos copiados parecem quebrados em um dispositivo que funciona. As verificações de capacidade evitam a armadilha e comportam-se da mesma forma desde o Android 5.0. Se você mantém uma base de código que ainda a chama, substitua a chamada em vez de envolvê-la em um fallback, porque foi o fallback que produziu a resposta errada em primeiro lugar.
Um atalho merece um aviso. Comparar o endereço público relatado por um serviço externo com o endereço de saída da VPN prova algo sobre o caminho, mas adiciona uma requisição de terceiros a cada verificação, depende de esse serviço continuar no ar e pode ser armazenada em cache ou bloqueada. Mantenha-a como diagnóstico manual, nunca como a verificação de estado dentro do app.
Execute as verificações nesta ordem e pare na primeira que discordar do relato do usuário: listagem de transportes no shell, capacidades da rede ativa, eventos de callback enquanto você liga e desliga o túnel, e então o endereço de origem no lado do servidor. Se todos os quatro concordarem e o app ainda se comportar mal, o bug está na sua própria máquina de estados, não na visão que o Android tem da rede.