Le tunnel monte, l'icône de clé apparaît, et chaque requête du navigateur expire encore. Dans une application VpnService, cette combinaison signifie généralement que la boucle de lecture est vivante et que la moitié de sortie est cassée, pas que le serveur a rejeté la poignée de main. Android a déjà créé l'interface, y a déplacé le trafic de l'application et remis à votre processus un descripteur de fichier. Tout ce qui suit ce descripteur est votre code, et un appel manquant suffit à avaler tout le flux.

Ce que le système vous donne, et ce qu'il ne donne pas

establish() renvoie un ParcelFileDescriptor pour une interface tun. Le système y écrit les paquets IP sortants et attend que votre processus les lise, décide où ils vont et les envoie via un socket ordinaire. Il n'y a aucun relais noyau en dessous, donc quand personne ne lit, les paquets sont abandonnés et les compteurs de l'interface ne bougent jamais.

Commencez par compter au lieu de deviner. Incrémentez un compteur à chaque lecture réussie et journalisez la destination et le protocole des premiers paquets. Si le compteur reste à zéro alors que du trafic est généré, la panne se situe en amont de votre code de relais, ce qui réduit la recherche à la configuration du builder et au mode de lecture. Si le compteur bouge et que le navigateur échoue encore, la panne est dans le chemin d'envoi ou au retour.

La lecture bloquante décide si la boucle avance

setBlocking(true) place le descripteur en mode bloquant et existe depuis API 21. Sur les anciennes versions, la solution est un petit assistant JNI qui efface O_NONBLOCK avec fcntl sur le descripteur brut. Le mode compte parce qu'il décide de ce qu'une lecture sans paquet en attente est autorisée à faire.

En mode non bloquant, cette lecture revient immédiatement avec une valeur négative, et une boucle qui ignore le résultat fait tourner un cœur sans déplacer le moindre trafic. En mode bloquant, la lecture attend, ce que veut un thread de relais, mais le thread ne peut alors plus être interrompu. Fermer le descripteur est le moyen pris en charge de faire échouer la lecture pour que la boucle puisse sortir. Tuer le processus ou interrompre le thread peut laisser le démarrage suivant avec un descripteur qui ne correspond plus à l'interface.

Lisez un paquet par appel dans un tampon dimensionné pour la MTU de l'interface, et traitez zéro ou un retour négatif comme une erreur plutôt que comme un paquet vide.

protect() avant le premier envoi, pas après

Chaque socket qui transporte un paquet encapsulé doit d'abord être protégé. Un socket non protégé obéit aux mêmes règles de routage que vous venez d'installer, donc ses paquets retournent dans le tunnel et votre propre boucle de lecture les reprend. Le tunnel relaie alors sa propre sortie sans fin, et de l'extérieur cela ressemble à une connexion qui s'ouvre et ne répond jamais.

L'ordre est protect, puis connect ou bind, puis send. Appeler protect après connect peut laisser la décision de route antérieure en place, le socket continue donc d'utiliser le mauvais chemin même si l'appel a renvoyé true. Vérifiez le booléen renvoyé et échouez bruyamment quand il est false au lieu d'entrer dans un envoi qui ne peut pas fonctionner.

Une erreur de permission de sendto sur un socket qui n'a jamais touché protect() est la signature habituelle de cette erreur.

L'exemple UDP ne transporte pas HTTPS

L'exemple de relais que copient la plupart des tutoriels gère UDP et rien d'autre. TCP a besoin d'une poignée de main, de numéros de séquence, d'accusés de réception et de retransmission, et une boucle qui copie des charges entre un descripteur tun et un socket n'en fournit aucun. Voilà pourquoi une requête DNS en UDP peut passer alors que chaque requête HTTPS se bloque. Exécutez une pile TCP en espace utilisateur ou confiez les connexions à un proxy local qui termine TCP sur l'appareil et relaie le flux.

Testez la séparation avant de réécrire quoi que ce soit : envoyez une requête UDP et ouvrez une connexion TCP vers le même hôte. Si le chemin UDP fonctionne et que le chemin TCP bloque au premier SYN, le problème vient du code de relais et non du serveur. Cette seule comparaison fait gagner beaucoup de temps passé à lire des journaux serveur qui ne contiennent rien d'utile.

La liste des routes et les filtres par application bornent ce que vous voyez

addAddress, addRoute et addDns définissent ce que le système envoie dans le tunnel. Une destination hors de la liste des routes n'atteint jamais votre descripteur, donc aucune quantité de logique de relais ne la corrigera. Journalisez les appels au builder et comparez-les aux adresses que l'application tente réellement de joindre.

La sélection par application a son propre plancher : addAllowedApplication et addDisallowedApplication nécessitent API 21, et sur les anciennes versions ces méthodes sont simplement absentes. Une conception qui filtre par paquet ne peut pas y fonctionner du tout.

Le rétablissement laisse des descripteurs obsolètes

Un nouvel appel à establish() renvoie un nouveau descripteur et ferme l'interface précédente. L'ancien descripteur est désormais invalide, donc un thread encore bloqué dessus reçoit une erreur, et les sockets créés pour l'ancien chemin doivent peut-être être remplacés. onRevoke est le rappel pour un tunnel que l'utilisateur ou le système a retiré. Garder le cycle de vie chez un seul propriétaire, qui arrête le lecteur avant de rétablir, évite la plupart des états connecté-mais-mort.

Lisez chaque erreur selon l'étape à laquelle elle appartient. Une réinitialisation de connexion venue de recvfrom signifie que l'extrémité distante s'est fermée pendant que votre code lisait encore. Une erreur de mauvaise adresse pointe vers l'analyse d'en-tête ou un bug d'offset, pas vers le réseau.

Ordre du travail : mode bloquant activé, un paquet par lecture, en-tête analysé, socket protégé, charge envoyée, fermeture propre lors de la passation et liste de routes couvrant la destination.

Articles similaires