Cómo comprobar en el código si hay una VPN conectada en Android
Android no tiene un getter de VPN conectada. Consulta las capacidades de red, vigila los cambios de transporte y mantén separados el consentimiento de la app, el estado del dispositivo y la prueba del servidor.
No puedes preguntar a Android si hay una VPN conectada. VpnService no expone ningún getter de conexión, y el icono de la llave en la barra de estado lo dibuja el sistema, no algo que tu app pueda leer. Lo que sí puedes hacer es hacer a la pila de conectividad una pregunta más concreta: qué red usa el tráfico de esta app y si esa red lleva el transporte VPN. Este manual recorre las comprobaciones en el orden que evita los falsos positivos, desde un comando shell en el dispositivo hasta el callback que dejas ejecutándose en la app.
Comprueba el dispositivo desde una shell antes de tocar el código de la app
Primero obtén una línea base. adb shell dumpsys connectivity imprime todas las redes que conoce el sistema, y cada bloque enumera sus transportes, así que una red VPN aparece con VPN en esa lista. Lee el bloque de la red predeterminada antes que nada y después busca otras redes que lleven el mismo transporte.
Dos salidas responden a preguntas distintas. Que exista una red VPN no es lo mismo que tu app la use, y en un dispositivo con perfil de trabajo o un segundo usuario ambas cosas pueden ser ciertas a la vez mientras tu proceso habla directamente con internet. La línea de la red activa es la que decide qué hacen tus sockets.
Pide la red que usa la app
En el código, la prueba de capacidades son cuatro líneas. Solicita ACCESS_NETWORK_STATE en el manifiesto y después compara las capacidades de la red activa con el 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);
La respuesta es deliberadamente estrecha: si la red predeterminada de la app es una VPN ahora mismo. No dice nada sobre qué VPN es, si es tuya o si el túnel llega a su extremo remoto. Trata un true como «el tráfico sale por algún túnel» y mantén tu propia contabilidad aparte. Un booleano que en silencio significa tres cosas a la vez es la razón por la que una pantalla de ajustes acaba informando de que la VPN está desactivada mientras el usuario navega a través de ella.
Cuando en cambio necesitas saber si hay alguna red VPN, itera cm.getAllNetworks() y prueba cada conjunto de capacidades. Las dos respuestas divergen en un dispositivo que ejecuta una VPN por app que cubre otras aplicaciones pero no la tuya, y esa divergencia es el origen de la mayoría de los informes de errores que dicen que la app no ve una VPN que claramente está activa.
Vigila los cambios en lugar de sondear
Sondear con un temporizador es la forma equivocada para esta pregunta, porque los cambios de estado llegan como eventos. Registra una solicitud que coincida con el transporte VPN y deja que el framework te avise cuando aparezca, desaparezca o cambie una red coincidente:
NetworkRequest request = new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build();
cm.registerNetworkCallback(request, callback);
Una red VPN nunca lleva NET_CAPABILITY_NOT_VPN, así que la eliminación explícita documenta la intención. Sin ella la solicitud sigue coincidiendo, pero la siguiente persona que lea el código tiene que deducirlo.
El registro pertenece al ámbito que necesita la observación, normalmente una pantalla o un servicio en primer plano. Cancela el registro en onStop o onDestroy, y no dejes un callback enganchado a una actividad que ya no existe. Una app sin componente visible es un mal anfitrión para un observador permanente, y los callbacks que olvidas son los que se caen tras un cambio de configuración.
Mantén tres preguntas separadas
La mayor parte de la confusión en esta área viene de tratar tres comprobaciones distintas como una sola. Usan API distintas, y cada una puede ser cierta mientras las otras son 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 |
La tercera fila es la que la gente intenta responder en el cliente, y ahí no se puede responder. Da a los clientes VPN su propio conjunto de direcciones y lee la dirección de origen en el servidor; es la única prueba que sobrevive cuando un usuario cambia de túnel a mitad de sesión.
Orden de verificación que descarta los falsos positivos
Todavía circula código de ejemplo antiguo que llama a getNetworkInfo(ConnectivityManager.TYPE_VPN). Esa llamada está obsoleta, y en varias versiones devolvía null incluso con una VPN conectada, por eso tantos fragmentos copiados parecen rotos en un dispositivo que funciona. Las comprobaciones de capacidades evitan la trampa y se comportan igual desde Android 5.0. Si mantienes una base de código que aún la llama, sustituye la llamada en lugar de envolverla en un fallback, porque el fallback fue lo que produjo la respuesta incorrecta.
Un atajo merece una advertencia. Comparar la dirección pública que informa un servicio externo con la dirección de salida de la VPN sí demuestra algo sobre la ruta, pero añade una solicitud a terceros a cada comprobación, depende de que ese servicio siga activo y puede ser cacheada o bloqueada. Mantenlo como diagnóstico manual, nunca como la comprobación de estado dentro de la app.
Ejecuta las comprobaciones en este orden y detente en la primera que no coincida con el informe del usuario: listado de transportes en la shell, capacidades de la red activa, eventos de callback mientras alternas el túnel y después la dirección de origen en el lado del servidor. Si las cuatro coinciden y la app sigue comportándose mal, el error está en tu propia máquina de estados y no en la visión que Android tiene de la red.