Pourquoi votre tunnel Android VpnService cesse d'acheminer le trafic
Quatre croyances sur VpnService qui cassent en production : un tunnel à la fois, la portée des routes, les listes par application et l'ordre d'arrêt, avec une vérification pour chacune.
Quatre suppositions sur Android VpnService m'ont coûté une fois la plus grande partie d'une semaine, et aucune ne portait sur le protocole réseau. L'application disait connecté, l'icône de clé trônait dans la barre d'état, et le trafic sortait toujours par la route normale. Je continuais à supposer que le tunnel était le mien jusqu'à ce que je l'arrête, que les routes que j'ajoutais étaient celles que j'obtenais, et qu'un appel à stopSelf emporterait l'interface avec lui. Le service se comportait exactement comme documenté. La partie cassée, c'était mon modèle mental.
Mythe 1 : le tunnel reste en place jusqu'à ce que vous l'arrêtiez
Android ne tient qu'une interface VPN à la fois. Établissez-en une seconde et la plateforme ne la met pas en file : elle désactive la vôtre et appelle onRevoke() sur votre service. Si votre code ne démonte qu'à l'appui d'un bouton, une prise de contrôle laisse un descripteur et un thread de lecture derrière elle, et le démarrage suivant hérite des deux.
La référence de VpnService dit explicitement que prepare() renvoie null quand l'application a déjà le consentement et un Intent quand l'utilisateur doit encore approuver. Ce consentement n'arrive que si vous livrez l'intention via startActivityForResult(). Sautez la remise et establish() renvoie null, ce qui se lit comme un échec silencieux plutôt que comme un problème de permission.
Always-on VPN ajoute un propriétaire que vous ne contrôlez pas. Le système peut ramener le tunnel après un démarrage ou après un plantage, et il supplante une application VPN que l'utilisateur a lancée à la main. Construisez onRevoke() pour qu'il ferme les mêmes ressources que votre arrêt normal, et traitez-le comme une voie d'arrêt principale plutôt que comme un cas limite. Les utilisateurs changent d'application VPN ; votre code doit y survivre.
Mythe 2 : addRoute capture exactement ce que vous avez listé
Les routes décident quels paquets atteignent votre descripteur, et une erreur de portée produit le cas exaspérant où une partie du trafic passe par le tunnel et une autre non. addRoute("0.0.0.0", 0) prend tout l'IPv4, ce qui signifie que chaque paquet arrive désormais chez vous et doit être transmis ou abandonné. Un /32 pour un seul hôte ne prend presque rien, et tout le reste se comporte comme si votre VPN n'avait jamais été installé.
Les familles se comptent séparément. Une route IPv4 seule laisse IPv6 sur le réseau sous-jacent, si bien qu'un hôte double pile peut encore résoudre et se connecter hors du tunnel tout en paraissant parfaitement connecté à l'intérieur.
Le DNS suit la même logique. Les serveurs ajoutés avec addDnsServer() vont aux applications qui utilisent le résolveur du tunnel, tandis qu'une application dotée de son propre point de terminaison DNS chiffré ou d'un Private DNS réglé sur un nom d'hôte les ignore totalement. Quand les noms échouent pour une application et fonctionnent pour une autre, vérifiez le chemin du résolveur avant de toucher à une seule route.
Mythe 3 : les règles par application sont un interrupteur que vous pouvez basculer dans les deux sens
addDisallowedApplication() et addAllowedApplication() sont deux modes, pas deux filtres. Le constructeur accepte un modèle par tunnel, donc une application qui veut tout sauf trois paquets utilise la liste des interdits et laisse celle des autorisés vide. Les deux s'excluent, et les gens le découvrent à l'exécution après que l'écran de réglages a déjà proposé les deux bascules.
La visibilité des paquets mord aussi. Sur Android 11 et au-delà, vous ne pouvez nommer que des paquets que vous voyez, donc une liste d'exclusions construite à partir d'une requête que votre application n'a pas le droit d'exécuter manquera des entrées en silence. Comparez la liste à ce que le gestionnaire de paquets renvoie réellement avant de lui faire confiance.
Les profils professionnels divisent à nouveau le public. Un tunnel démarré dans le profil couvre ce profil et ses applications, tandis que le côté personnel conserve sa propre connectivité. C'est une décision de portée prise une fois, et elle appartient à l'interface, pas à un ticket de support.
Mythe 4 : stopSelf met fin au tunnel
Non. L'interface vit dans le descripteur renvoyé par establish(), et l'objet du service peut mourir pendant que ce descripteur reste ouvert. Fermez d'abord le ParcelFileDescriptor, lâchez la référence, joignez ou interrompez les threads de lecture et d'écriture, puis appelez stopForeground(true) et stopSelf() dans cet ordre.
Un descripteur oublié est la raison habituelle pour laquelle un service semble survivre à sa propre demande d'arrêt, et il explique une route obsolète qui survit à l'application qui l'a ajoutée.
L'erreur inverse est tout aussi courante : fermer le descripteur alors qu'un thread de lecture est en pleine lecture, puis laisser ce thread gérer une exception pour laquelle personne n'a rien écrit. Gardez la séquence d'arrêt au même endroit et faites-y appeler à la fois l'action de l'utilisateur et onRevoke().
Que vérifier quand le tunnel semble mort
Protégez les sockets de longue durée avec protect() avant que le tunnel ne monte. Sans cela, le transport du tunnel lui-même est renvoyé dans le tunnel et la connexion se bloque d'une manière qui ressemble exactement à une panne de serveur. Testez contre une adresse qui n'existe que derrière le réseau distant, pas contre un site public qui répond depuis le cache ou depuis le chemin sous-jacent.
Journalisez le résultat de establish(), les routes, les paquets exclus et la raison de l'arrêt. Ces quatre lignes séparent un problème de consentement d'un problème de portée et d'un problème de cycle de vie sans capture de paquets.