Le contrôle que la plupart des applications Android utilisent pour détecter un VPN est un nom d'interface, et ce n'a jamais été un contrôle de VPN. Il signale qu'une interface existe avec une chaîne que le développeur attendait, ce qui sur un appareil signifie OpenVPN et sur un autre signifie quelque chose de sans rapport. Un ping est le même type de preuve : il prouve qu'une destination a répondu, pas qu'un tunnel a transporté le paquet. Ce que la plateforme expose réellement, c'est un ensemble de réseaux et les transports que chacun déclare, et c'est une autre question avec une autre réponse.

Mythe : si tun0 existe, un VPN est actif

Les applications VPN Android sont des implémentations de VpnService qui remettent au système un descripteur de fichier tun. Le nom de l'interface vient de celui qui l'a créée : OpenVPN produit généralement tun0, PPTP et L2TP produisent ppp0, WireGuard nomme son interface d'après le tunnel, et un tunnel IPsec peut ne présenter aucun nom conventionnel. Lire /proc/net/dev ou énumérer des objets NetworkInterface cherche donc une convention, pas un fait.

Les deux directions d'erreur sont vivantes. Un appareil peut avoir tun0 pour un tunnel local sans rapport avec le VPN de l'utilisateur, et un tunnel WireGuard parfaitement sain peut être invisible pour un contrôle qui ne connaît que la chaîne tun0. Les faux positifs et les faux négatifs dans la même fonction la rendent inutilisable comme diagnostic, car un ticket de support qui dit « aucun VPN détecté » n'est fiable dans aucune direction.

L'accès s'est aussi resserré. Les versions récentes d'Android limitent ce qu'une application ordinaire peut lire dans /proc, si bien qu'un contrôle fondé sur le nom qui fonctionnait sous Android 8 peut renvoyer une liste vide sur un appareil plus récent sans aucun changement de code. Quand le contrôle échoue silencieusement du côté ouvert, l'application conclut qu'il n'y a pas de VPN sur un matériel où le tunnel fonctionne bien.

Mythe : un ping réussi signifie que le tunnel transporte du trafic

Un ping vers un hôte interne réussit dès qu'une route vers cet hôte existe. Avec un tunnel divisé, l'hôte peut être joignable par Wi-Fi alors que le tunnel est actif et fait tout autre chose. Avec un VPN par application, l'application qui fait le ping peut être exclue du tunnel, si bien que l'écho ICMP sort à l'extérieur et revient, ce qui prouve le contraire de ce que le contrôle suppose.

ICMP et TCP sont aussi filtrés séparément. Un tunnel qui autorise les demandes d'écho mais abandonne le port TCP dont un service a besoin passera le test de ping et échouera dans l'application, et c'est une règle de pare-feu de l'autre côté, non une propriété de l'appareil. La seule lecture honnête d'un ping réussi est que cette destination a répondu à ce protocole à cet instant.

Ce que la plateforme rapporte réellement

À partir de l'API 21, ConnectivityManager.getAllNetworks() renvoie tous les réseaux que le système connaît, et getNetworkCapabilities(network) indique ce que ce réseau déclare. Un réseau VPN porte TRANSPORT_VPN. Parcourir tous les réseaux répond à une question : un VPN est-il actuellement actif sur cet appareil ?

Une seconde question, plus étroite, exige un autre appel. getActiveNetwork() renvoie le réseau que le trafic de l'application utiliserait maintenant, et vérifier la même capacité dessus répond : mon trafic passerait-il par un VPN? Les deux peuvent diverger, et la divergence est instructive. Un VPN actif mais non courant pointe vers un routage par application ou une configuration de tunnel divisé, pas vers un tunnel manquant.

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

Protégez les deux appels. Les capacités peuvent être null pour un réseau disparu entre le listage et l'interrogation, et le réseau actif peut être null pendant un transfert. NET_CAPABILITY_NOT_VPN est utile dans l'autre sens, pour confirmer qu'un réseau n'est certainement pas un tunnel plutôt que de simplement manquer de l'indicateur de transport VPN.

NetworkInfo était le même mythe avec une API

ConnectivityManager.TYPE_VPN avec NetworkInfo est la deuxième génération de ce contrôle, et il est obsolète pour une raison. Dans les versions modernes, l'objet renvoyé pour un VPN peut être null alors que le tunnel est connecté, si bien que le code qui traite null comme « pas de VPN » signale silencieusement un faux négatif sur les appareils où la réponse compte le plus.

La diffusion qui l'accompagne habituellement n'est pas meilleure. CONNECTIVITY_CHANGE a un historique documenté de transitions VPN manquées, y compris des versions où il ne se déclenchait que lorsqu'un VPN se connectait ou se déconnectait plutôt qu'à chaque changement de réseau. Un écouteur construit dessus produit une variable d'état qui s'écarte de la vue de la plateforme jusqu'à la prochaine reconnexion complète.

Réagissez aux callbacks plutôt que d'interroger périodiquement

Enregistrez une NetworkRequest qui exige TRANSPORT_VPN et retire NET_CAPABILITY_NOT_VPN, puis appelez registerNetworkCallback avec cette requête. onAvailable et onLost vous indiquent alors quand la déclaration change, et c'est l'événement qui vous intéresse vraiment. Annulez l'enregistrement du callback quand le composant s'arrête, sinon l'application garde une référence à un écran mort et le callback continue de se déclencher.

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

Cela nécessite ACCESS_NETWORK_STATE, qui est une autorisation normale : pas de root, pas de dialogue de consentement utilisateur, pas d'autorisation VPN. L'interrogation par minuterie est l'alternative vers laquelle les gens se rabattent, et elle consomme la batterie tout en manquant une transition plus courte que l'intervalle, ce qui est exactement la transition qu'un utilisateur signale.

Ce que la réponse ne peut toujours pas vous dire

Aucune de ces API n'indique si le tunnel est sain, sur quel serveur il se termine ou si le trafic d'une application donnée est à l'intérieur. Un réseau VPN peut exister sans routes utilisables, et un tunnel par application peut exclure l'application qui pose la question.

Enregistrez donc ce que le contrôle a vu, pas seulement le booléen. Consignez l'identifiant du réseau, les transports déclarés et si ce réseau était actif. Un rapport qui dit « aucun VPN détecté » est inutilisable, tandis qu'un rapport qui dit qu'un réseau a déclaré TRANSPORT_VPN mais que getActiveNetwork() a renvoyé Wi-Fi pointe directement vers le routage ou vers une liste d'exclusions, et c'est une correction sur laquelle quelqu'un peut travailler.

Articles similaires