Le client VPN se connecte, le navigateur fonctionne, et la seule commande qui vous importe vraiment sort toujours par votre connexion domestique. Ou l’inverse se produit : le tunnel prend la route par défaut et le serveur de fichiers local disparaît du bureau. J’ai rencontré cela avec un agent de compilation qui devait atteindre un miroir d’artefacts interne pendant que le reste de l’hôte restait sur le LAN du bureau, et le client n’offrait que deux choix : tout le trafic ou rien. La demande est la même dans les deux sens, ce programme et pas toute la machine, et la plupart des clients n’ont pas de case à cocher pour cela, parce que la table de routage n’a jamais su ce qu’est un programme.

Pourquoi la table de routage ne voit pas votre programme

Quand un processus envoie un paquet, la pile cherche l’adresse de destination dans la table de routage. Ni le nom de l’exécutable ni le port n’entrent dans cette recherche. Les noms de domaine échouent pour la même raison : un tunnel transporte le trafic vers une adresse, et quand un nom s’est résolu en une, le routage est le seul levier qui reste au noyau. C’est pourquoi « VPN seulement pour ce site » finit par signifier un proxy, et « VPN seulement pour ce programme » signifie l’un d’un petit ensemble de mécanismes.

MechanismWhat selectsCoversOn tunnel drop
Socket bindsource addressa client you controlconnections fail
UID rule (Linux)owning userevery process of one accountconnections fail
Firewall profile (Windows)adapter profileone programfails closed
Network namespacenamespace membershipeverything started insideconnections fail
SOCKS hopapp proxy settingone app or one browser profileconnections fail

Aucun de ces mécanismes n’est une route par processus. Chacun déplace la décision vers une couche qui voit le programme, l’utilisateur ou la configuration propre à l’application.

Une destination unique reste une route, pas une règle de programme. Sur macOS, la recette habituelle consiste à décocher « Envoyer tout le trafic via le VPN » et à ajouter une route d’hôte : sudo route add -host internal-tuner.example -interface ppp0, retirée ensuite avec sudo route delete -host internal-tuner.example -interface ppp0. Lisez d’abord le nom de l’interface dans ifconfig, car ppp0 n’est qu’un exemple. Les jokers ne fonctionnent pas dans une route d’hôte, donc un service qui parle à plusieurs noms a besoin d’une entrée par nom, et la règle couvre chaque processus de la machine, pas celui que vous aviez en tête.

Liez la socket à l’adresse du tunnel

Quand vous contrôlez le client, la liaison est le sélecteur le moins coûteux. Une socket liée à l’adresse locale du tunnel sort par cette interface tant qu’une route vers la destination y existe, et tout le reste de la machine garde son chemin normal. La subtilité est que la liaison choisit l’adresse source tandis que la table de routage choisit encore la sortie. La liaison seule ne déplace pas le trafic qui n’a pas de route vers le tunnel, et c’est l’erreur derrière la plupart des signalements « je l’ai lié et il sort encore du mauvais côté ».

import socket
s = socket.socket()
s.bind(("10.8.0.2", 0))
s.connect(("build.internal.example", 443))

Les outils de plus haut niveau exposent la même idée : curl --interface 10.8.0.2, CURLOPT_INTERFACE en PHP et l’argument source_address dans les clients HTTP de Python. L’adresse doit appartenir au tunnel, et une faute de frappe ici échoue comme une erreur de connexion ordinaire, pas comme une plainte de routage. La liaison à un nom de périphérique plutôt qu’à une adresse est un privilège Linux et ne se transpose pas à Windows ni à macOS, c’est pourquoi les scripts portables passent l’adresse.

Linux : faites correspondre le propriétaire, pas le nom du programme

Le noyau peut faire correspondre l’utilisateur propriétaire d’une socket, donc un compte dédié devient le sélecteur. Placez le tunnel dans sa propre table de routage et n’y envoyez que le trafic de ce compte :

ip rule add uidrange 1001-1001 lookup 100
ip route add default dev tun0 table 100

Cela ne fonctionne que si le tunnel n’est pas déjà la route par défaut. Démarrez le client avec --route-nopull, ou filtrez la redirection poussée avec --pull-filter ignore "redirect-gateway" sur OpenVPN 2.4 et ultérieur, puis ajoutez vous-même l’entrée de table. Sinon, les deux routes par défaut existent et la moins prioritaire l’emporte. La version délibérée consiste à pointer la route par défaut vers la passerelle du tunnel, ou à le faire depuis un script de déconnexion : cela garde le trafic hors d’Internet quand le tunnel tombe, mais c’est un interrupteur d’arrêt, pas un sélecteur, et il reste jusqu’à ce que quelqu’un le retire.

La version plus forte est un espace de noms réseau. Créez-le, exécutez le client à l’intérieur et lancez l’application avec ip netns exec. Rien à l’extérieur de l’espace de noms ne voit l’interface du tunnel, donc l’application ne peut pas retomber par accident sur la route de l’hôte. Le coût est réel : l’espace de noms a besoin d’une paire veth pour atteindre le réseau physique et de son propre fichier de résolution, car l’espace de noms de montage ne partage pas les réglages de résolution de l’hôte.

Deux modes de défaillance expliquent la plupart des fuites. La résolution de noms se fait toujours dans le résolveur de l’hôte, donc la requête sort hors du tunnel même quand la connexion TCP entre. Et IPv6 a besoin de sa propre ip -6 rule, car une règle v4 ne la couvre pas.

Windows et macOS : délimitez le pare-feu ou cédez un proxy

Windows n’a pas non plus de sélecteur de route par application, mais ses règles de pare-feu peuvent être limitées à un profil, et un adaptateur VPN peut être classé « public » tandis que le LAN domestique reste « privé ». Créez des règles de blocage sortant pour le programme sur les profils « privé » et « domaine », laissez « public » ouvert, et l’application ne pourra atteindre le réseau que par le tunnel. Définissez d’abord la catégorie réseau de la connexion, puis confirmez que la règle a correspondu dans le panneau de surveillance du pare-feu. C’est un blocage, pas une route : il échoue fermé quand le tunnel tombe, et il ne peut pas arrêter une application qui ignore le pare-feu.

macOS suit la même logique avec d’autres outils. Un écouteur SOCKS atteint par SSH est l’option la moins invasive, car ssh -D 1080 user@host crée un seul port local et seules les applications configurées pour l’utiliser prennent ce chemin. Une règle pf peut bloquer un programme par utilisateur ou par groupe, mais elle ne peut toujours pas choisir le tunnel pour ce seul programme. Tunnelblick ne peut pas filtrer par domaine du tout, car les routes travaillent sur des adresses ; les utilisateurs d’OpenConnect se tournent vers vpn-slice quand le serveur pousse une liste de préfixes trop large.

Deux contournements couvrent ce que les outils de la plateforme manquent. Les extensions de navigateur qui changent de proxy par domaine sont la réponse pratique pour « ce site seulement », car elles agissent à la couche applicative où les noms existent encore ; elles couvrent un navigateur et rien d’autre. La solution brutale consiste à exécuter le client de l’éditeur dans une machine virtuelle pour que l’invité possède le changement de route pendant que l’hôte garde son chemin, au prix de deux piles réseau à maintenir.

Un saut SOCKS pour les outils sans option de liaison

La plupart des applications peuvent être pointées vers un proxy HTTP ou SOCKS même quand elles ne savent rien des interfaces. Un client SSH en fournit un sans aucun changement côté serveur :

ssh -N -D 127.0.0.1:9999 [email protected]

Tout ce qui est configuré pour utiliser ce port sort au bastion pendant que le reste du bureau garde sa connexion normale. Une instance locale de 3proxy liée à l’adresse du tunnel fait de même pour un seul port sans SSH, et stunnel couvre un seul port enveloppé dans TLS. Les limites sont pratiques : un proxy ne transporte que les protocoles qui savent l’utiliser, UDP a besoin d’une association UDP SOCKS5 que le réglage « SOCKS5 » de nombreux clients n’active pas, et le DNS doit être résolu par le proxy (socks5h dans curl), sinon le nom fuit vers le résolveur local.

Gardez l’écouteur sur la boucle locale sauf si une autre machine en a besoin.

Vérifiez le chemin par une capture, pas par un message d’état

Les fenêtres d’état des applications décrivent le réglage, pas le paquet. Capturez sur les deux interfaces et comparez :

tcpdump -i any -n port 443
ss -tnp | grep 443

Si la connexion apparaît sur l’interface physique sans entrée correspondante sur le tunnel, le sélecteur n’est pas en vigueur, quoi que rapporte le client. Vérifiez IPv6 et le résolveur séparément, et cherchez un second chemin comme un processus auxiliaire ou un proxy système hérité.

Choisissez ensuite la frontière qui correspond à l’exigence : un proxy suffit pour un site, une règle UID suffit pour un compte, un profil de pare-feu suffit quand la connexion doit échouer fermée, et un espace de noms est la seule option qui cache le tunnel à tout ce qui est dehors.

Articles similaires