Comment vérifier dans le code qu'un VPN est connecté sur Android
Android n'a pas de getter d'état de connexion VPN. Interrogez les capacités réseau, surveillez les changements de transport et séparez le consentement de l'app, l'état de l'appareil et la preuve côté serveur.
Vous ne pouvez pas demander à Android si un VPN est connecté. VpnService n'expose aucun getter de connexion, et l'icône de clé dans la barre d'état est dessinée par le système, pas par quoi que ce soit que votre app puisse lire. Ce que vous pouvez faire, c'est poser à la pile de connectivité une question plus étroite : quel réseau porte le trafic de cette app, et ce réseau transporte-t-il le transport VPN. Ce guide passe les vérifications dans l'ordre qui évite les faux positifs, d'une commande shell sur l'appareil jusqu'au callback que vous laissez tourner dans l'app.
Vérifiez l'appareil depuis un shell avant de toucher au code de l'app
Obtenez d'abord une ligne de base. adb shell dumpsys connectivity affiche chaque réseau connu du système, et chaque bloc liste ses transports, donc un réseau VPN apparaît avec VPN dans cette liste. Lisez le bloc du réseau par défaut avant tout le reste, puis cherchez d'autres réseaux qui portent le même transport.
Deux sorties répondent à deux questions différentes. L'existence d'un réseau VPN n'est pas la même chose que son utilisation par votre app, et sur un appareil avec profil professionnel ou second utilisateur, les deux peuvent être vrais en même temps alors que votre processus parle directement à internet. La ligne du réseau actif est celle qui décide de ce que font vos sockets.
Demandez le réseau que l'app utilise
Dans le code, le test de capacités tient en quatre lignes. Demandez ACCESS_NETWORK_STATE dans le manifeste, puis comparez les capacités du réseau actif au transport 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 réponse est volontairement étroite : le réseau par défaut de l'app est-il un VPN en ce moment. Elle ne dit rien sur quel VPN c'est, s'il vous appartient ou si le tunnel atteint son extrémité distante. Traitez true comme « le trafic sort par un tunnel » et gardez votre propre comptabilité à part. Un booléen qui signifie silencieusement trois choses à la fois, c'est ainsi qu'un écran de réglages finit par annoncer le VPN comme désactivé pendant que l'utilisateur navigue à travers lui.
Quand vous avez plutôt besoin de savoir si un réseau VPN existe, itérez sur cm.getAllNetworks() et testez chaque jeu de capacités. Les deux réponses divergent sur un appareil qui exécute un VPN par app couvrant d'autres applications mais pas la vôtre, et cette divergence est la source de la plupart des rapports de bug qui disent que l'app ne voit pas un VPN pourtant bien actif.
Surveillez les changements au lieu d'interroger
Interroger avec un minuteur est la mauvaise forme pour cette question, car les changements d'état arrivent comme des événements. Enregistrez une requête qui correspond au transport VPN et laissez le framework vous dire quand un réseau correspondant apparaît, disparaît ou change :
NetworkRequest request = new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build();
cm.registerNetworkCallback(request, callback);
Un réseau VPN ne transporte jamais NET_CAPABILITY_NOT_VPN, donc le retrait explicite documente l'intention. Sans lui, la requête correspond toujours, mais la personne suivante qui lit le code doit le déduire elle-même.
L'enregistrement appartient à la portée qui a besoin de l'observation, généralement un écran ou un service de premier plan. Désenregistrez dans onStop ou onDestroy, et ne laissez pas un callback attaché à une activité qui n'existe plus. Une app sans composant visible est un mauvais hôte pour un observateur permanent, et les callbacks que vous oubliez sont ceux qui plantent après un changement de configuration.
Gardez trois questions distinctes
L'essentiel de la confusion dans ce domaine vient de traiter trois vérifications différentes comme une seule. Elles utilisent des API différentes, et chacune peut être vraie alors que les autres sont fausses.
| 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 troisième ligne est celle que les gens tentent de répondre côté client, et elle ne peut pas y être répondue. Donnez aux clients VPN leur propre pool d'adresses et lisez l'adresse source sur le serveur ; c'est la seule preuve qui survit à un utilisateur qui change de tunnel en cours de session.
Ordre de vérification qui écarte les faux positifs
Du code d'exemple plus ancien encore en circulation appelle getNetworkInfo(ConnectivityManager.TYPE_VPN). Cet appel est obsolète, et sur plusieurs versions il renvoyait null alors même qu'un VPN était connecté, ce qui explique pourquoi tant d'extraits copiés semblent cassés sur un appareil qui fonctionne. Les tests de capacités évitent le piège et se comportent de la même façon depuis Android 5.0. Si vous maintenez une base de code qui l'appelle encore, remplacez l'appel au lieu de l'envelopper dans un fallback, car c'est le fallback qui a produit la mauvaise réponse à l'origine.
Un raccourci mérite un avertissement. Comparer l'adresse publique rapportée par un service externe à l'adresse de sortie du VPN prouve bien quelque chose sur le chemin, mais ajoute une requête tierce à chaque vérification, dépend de la disponibilité de ce service et peut être mise en cache ou bloquée. Gardez-le comme diagnostic manuel, jamais comme vérification d'état dans l'app.
Exécutez les vérifications dans cet ordre et arrêtez-vous à la première qui contredit le rapport de l'utilisateur : liste des transports dans le shell, capacités du réseau actif, événements de callback pendant que vous basculez le tunnel, puis l'adresse source côté serveur. Si les quatre concordent et que l'app se comporte toujours mal, le bug est dans votre propre machine à états et non dans la vision qu'Android a du réseau.