VpnService protect() ou exclusion de l'app : ce qui répare le routage
Protégez le socket qui porte le tunnel, ou excluez toute l'application : les trois conditions qui décident de la portée nécessaire à votre VPN Android.
Vous avez deux façons de garder les paquets du tunnel hors du tunnel : protéger le socket de transport, ou sortir toute l'application du VPN. Elles règlent des problèmes différents, et choisir la plus large parce qu'elle est plus facile à câbler est exactement ainsi que les applications perdent l'accès au réseau local et le DNS scindé sans le remarquer. Partez de ce qui doit rester hors du tunnel, puis choisissez la portée la plus petite qui y parvient.
Ce que protect() retire du tunnel
VpnService.protect() est un appel par socket. Vous lui passez le socket ou le descripteur qui porte le tunnel, et Android marque ce socket pour que son trafic utilise le réseau sous-jacent au lieu de l'interface du VPN. Tout le reste de ce que l'application envoie suit toujours la route VPN normale.
Cette portée étroite est justement l'intérêt. Un socket protégé ne change pas la façon dont votre client HTTP résout les noms, l'interface qu'utilisent vos appels d'analytique, ni ce qui arrive à une seconde connexion ouverte plus tard.
Trois conditions qui décident de la portée
La première condition est de savoir si seul le transport du tunnel doit s'échapper. Si le reste du trafic de l'application doit continuer à passer par le VPN, la protection du socket est la bonne portée. La deuxième condition est de savoir si vous maîtrisez la création du socket. Une bibliothèque qui ouvre son propre socket en profondeur ne vous laisse aucun endroit pour appeler protect(), à moins qu'elle n'expose un hook ou n'accepte un socket déjà construit. La troisième condition est de savoir si l'application doit atteindre directement le réseau local ou un portail captif, ce qui est une exigence différente du maintien du tunnel en vie.
Exclure l'application avec addDisallowedApplication() est l'outil brutal. Il retire du VPN chaque connexion que fait l'application, y compris celles que vous vouliez inspecter, et sur certains appareils il change aussi le comportement de la découverte locale.
Une troisième option consiste à lier le transport à un réseau choisi avec bindProcessToNetwork() ou un handle réseau au niveau du socket. C'est utile quand l'appareil a du Wi-Fi et du cellulaire en même temps et que vous voulez que le tunnel passe par une liaison montante précise.
Appelez-le pendant la création du transport
Appelez protect() dans le même bloc qui crée le transport, avant connect() ou send(). Si l'appel renvoie false, fermez le socket et faites échouer la configuration ; continuer vous laisse un transport dont les paquets peuvent revenir dans votre propre interface.
La forme est assez petite pour tenir à un seul endroit :
DatagramSocket transport = new DatagramSocket();
if (!protect(transport)) {
transport.close();
throw new IOException("transport not protected");
}
transport.connect(serverAddress);Trois détails mordent ici. D'abord,le booléen est le seul signal que vous obtenez, alors journalisez l'échec avec assez de contexte pour distinguer un consentement révoqué d'un appel refusé. Ensuite,protect() couvre le socket, pas le numéro du descripteur : si vous fermez le socket et réutilisez plus tard le même entier pour un autre socket, le nouveau socket n'est pas protégé. Enfin, quand une bibliothèque native ouvre le transport, protégez le descripteur avant que le code natif ne se connecte, sinon la route est peut-être déjà choisie.La référence VpnService est l'endroit où vérifier quelles surcharges votre niveau d'API cible fournit réellement.
Testez le chemin bloqué, pas seulement le chemin heureux
Vérifiez avec le tunnel actif, car c'est le seul état où les deux chemins peuvent différer. Envoyez une requête par le transport protégé et une depuis un chemin de code qui n'a jamais appelé protect(), puis comparez ce que voit l'extrémité distante : la connexion protégée doit arriver avec le réseau sous-jacent comme source, tandis que la non protégée montre l'adresse propre du tunnel et fait bouger ses compteurs d'octets. Sur l'appareil,ss ou netstat montre la même séparation avant que quoi que ce soit ne quitte la radio.
Testez la révocation séparément, car un changement de consentement démonte l'interface sans toucher à votre code de socket. Révoquez le consentement VPN ou arrêtez le service, reconstruisez le transport et confirmez que protect() renvoie toujours true et que le transport se reconnecte dès que le consentement est de nouveau accordé.
Si la requête protégée suit quand même le tunnel, l'appel de protection a probablement lieu après que le socket a déjà été routé. Avancez-le et retestez.
Gardez la décision écrite à côté de la configuration du transport : quels sockets sont protégés, quel trafic de l'application est exclu et ce que fait l'application quand la protection échoue. C'est cette ligne qui empêche un refactor ultérieur d'élargir l'exclusion à toute l'application parce qu'un seul test échouait. Elle donne aussi à la personne suivante un moyen de vérifier l'intention sans rétro-ingénierie du manifeste.