Deux chemins peuvent placer une interface IP devant une application Android, et tous deux aboutissent au même type de descripteur de fichier avec des propriétaires très différents. L’un passe par VpnService et son constructeur, l’autre par un shell root et /dev/net/tun. Chaque lecture sur ce descripteur est un paquet sortant et chaque écriture un paquet entrant. Ce qui sépare les chemins, c’est qui détient la politique, qui survit à une mise à jour du système et quelle part de la table de routage vous pouvez toucher.

Prenez le chemin de la plateforme, sauf si vous pouvez nommer la chose précise qu’elle refuse de faire. Ce refus est généralement TAP, la capture hors de votre propre ensemble d’applications, ou une règle de routage que le framework n’expose jamais.

Sur le chemin de la plateforme, vous déclarez une intention au lieu de commandes. Vous fournissez au moins une adresse, les routes voulues, des serveurs DNS et une liste d’applications facultative, puis le service système d’Android effectue la configuration privilégiée sous sa propre identité. Vous n’apprenez jamais le nom de l’interface à l’avance et vous n’émettez jamais l’ioctl qui la crée, et c’est tout l’intérêt.

Deux façons d’obtenir une interface IP

Le chemin root est la recette la plus ancienne. Vous ouvrez /dev/net/tun, appelez TUNSETIFF avec IFF_TUN, montez l’interface avec ip link et ajoutez adresses et routes à la main. Chaque étape est à réussir vous-même, y compris celles que le framework aurait réglées en silence. Une route manquante produit le même rapport utilisateur qu’un serveur mort.

Le débogage diffère autant que la configuration. Sur le chemin root, l’interface existe ou bien votre ioctl a renvoyé une erreur que vous pouvez lire directement. Sur le chemin de la plateforme, l’interface n’existe qu’après la réussite d’establish(), donc un échec arrive comme exception ou comme descripteur null plutôt que comme nœud de périphérique où fouiller.

Ce que establish() renvoie réellement

Builder.establish() a trois issues et elles demandent trois réponses différentes. Un descripteur signifie que l’interface est configurée et vous appartient, null que la demande n’est pas passée, et une exception levée que le service système l’a rejetée. Fusionner null et succès dans un seul booléen est la façon la plus courante pour une application de signaler un tunnel fonctionnel qui ne transporte jamais de paquet. La référence de Builder indique l’exigence d’adresse, et cela vaut la lecture avant de blâmer le serveur.

Les directions sont fixées par le descripteur, pas par votre protocole. Les lectures vous donnent des paquets sortants déjà sélectionnés pour le VPN, et les écritures injectent les paquets entrants dans l’interface. Votre propre transport chiffre, authentifie et encadre ces paquets, car la plateforme s’arrête à la frontière du paquet.

Binder est un saut, JNI est un autre axe

Une demande du constructeur traverse le système via Binder, et quelque part en dessous la plateforme appelle du code natif pour configurer le périphérique. Ce sont deux mécanismes différents empilés dans un seul appel. Binder est une communication entre processus entre votre application et un service système, tandis que JNI est le pont Java-vers-natif à l’intérieur d’un processus. Lire une arborescence AOSP d’Android 9 rend la pile littérale, et il faut se rappeler que les noms de classes internes et les chemins de fichiers bougent d’une version à l’autre.

Ce qui ne bouge pas, c’est le contrat sur lequel vous pouvez compter : le constructeur, le descripteur qu’il renvoie et le cycle de vie du service autour de lui. Aucune application ne devrait appeler une transaction de service interne ni recréer un nœud de périphérique pour obtenir une interface. Si votre code dépend d’un nom de classe AOSP, il cassera sur une version que vous n’avez pas testée.

TUN transporte IP, TAP transporte Ethernet

TUN est une interface IP. TAP est une interface Ethernet, et cette différence décide si le pontage est même possible. Un périphérique TUN n’a pas d’adresse MAC ni d’en-tête Ethernet à transmettre, donc un client TUN ne peut pas rejoindre un pont de couche 2, quelle que soit l’écriture des routes.

VpnService vous donne la variante IP, qui est ce dont un VPN au niveau applicatif a besoin et ce qu’il peut prendre en charge sans pile Ethernet brute. Si votre conception exige du pontage, le chemin de la plateforme ne vous y mènera pas, et le chemin root seulement si le noyau et les permissions de l’appareil coopèrent.

Là où le chemin root gagne encore

Trois travaux poussent les gens hors du chemin de la plateforme. Le pontage de couche 2 exige TAP. Capturer le trafic d’applications hors de votre propre périmètre VPN exige une visibilité que le framework n’accorde pas. Le routage par politique que le constructeur n’expose jamais, comme une table séparée pour une destination, exige des règles que vous installez vous-même.

Les coûts arrivent plus tard : un bootloader déverrouillé ou une image rootée, une politique SELinux qui bouge sous vous, et un appareil qui cesse de se comporter correctement après une OTA. Une recette rootée ne peut pas non plus être livrée par la distribution normale d’applications, donc la plupart des équipes la gardent comme chemin de diagnostic interne plutôt que comme produit.

Choisir entre les deux

Partez de l’interface dont vous avez besoin, pas du code que vous avez. Si la réponse est un périphérique TUN avec des routes limitées à l’application, le chemin de la plateforme est plus court et survit aux mises à jour. Si la réponse implique des trames Ethernet, la capture d’autres applications ou des règles de routage que le constructeur rejette, vous êtes en territoire root et devez le budgéter comme un coût d’exploitation.

Un contrôle qui règle la plupart des débats : construisez d’abord le chemin de la plateforme, enregistrez le résultat d’establish() séparément de l’état de la session distante, et confirmez un paquet dans chaque direction. Décidez seulement ensuite si la capacité manquante justifie de quitter le chemin pris en charge.

Articles similaires