Votre conteneur tunnel fonctionne, docker compose exec vpn curl -s https://ifconfig.me répond avec l’adresse du fournisseur, et pourtant le navigateur de votre bureau sort toujours par votre ISP. Cet écart est attendu. Un conteneur qui exécute un client VPN détient un espace de noms réseau, des routes et une interface de tunnel, et rien qui parle HTTP ou SOCKS ; publier un port dessus ne peut pas créer un écouteur qui n’a jamais existé.

L’endroit où le client s’exécute décide de la conception. Un conteneur voisin peut entrer dans l’espace de noms du tunnel, une application de bureau ne le peut pas, et un routeur a besoin de routes plutôt que d’un champ proxy. Si vous vous trompez dans la correspondance, le symptôme est le même : connexion refusée, ou trafic qui emprunte discrètement l’ancien chemin.

Adaptez la disposition à l’endroit où le client s’exécute

Posez une question avant de modifier Compose : le client peut-il démarrer dans l’espace de noms réseau du conteneur VPN ? Tout ce qui tourne déjà comme conteneur Linux le peut. Un navigateur, un IDE ou une CLI sur votre bureau ne le peut pas, car son réglage de proxy attend un service de protocole, pas un espace de noms.

ClientArrangementWhat must already exist
Another Compose serviceShares the vpn namespace via network_mode: service:vpnA working tunnel and correct start order
Host browser, IDE or CLIProxy listener inside the namespace, published to host loopbackA configured SOCKS or HTTP proxy and its access rules
Router, VM or phoneThe container as a layer-3 gatewayForwarding, NAT and a return route

Ces trois ne se mélangent pas. Un champ proxy HTTP pointé vers l’adresse de pont du conteneur n’atteint rien si aucun proxy n’y écoute, et un écouteur proxy ne route pas le trafic d’un routeur. Décidez d’abord, puis suivez la section correspondante.

Partagez l’espace de noms du tunnel avec un conteneur voisin

C’est l’option la moins chère, car aucun protocole proxy n’intervient. L’application rejoint l’espace de noms du service vpn et hérite de ses interfaces, de ses routes et de son espace de ports :

services:
  vpn:
    image: your-vpn-client
    cap_add:
      - NET_ADMIN
    healthcheck:
      test: ["CMD-SHELL", "ip link show tun0 >/dev/null"]
      interval: 10s
      retries: 5
  app:
    image: your-app
    network_mode: service:vpn
    depends_on:
      vpn:
        condition: service_healthy

Trois restrictions découlent du fait que l’espace de noms appartient à un autre service. L’application n’obtient pas sa propre clé networks, car elle n’a pas d’espace de noms auquel s’attacher. La publication de ports revient à vpn, puisque c’est là que vit l’espace de noms. Et recréer vpn détruit l’espace de noms auquel l’application s’est jointe, donc docker compose up -d --force-recreate vpn vous laisse recréer app aussi ; un simple redémarrage ne suffit pas toujours. La référence des services Compose couvre cette disposition et sa restriction de publication de ports.

Ne supposez pas que le résolveur suit. /etc/resolv.conf est un fichier par conteneur, et non une partie de l’espace de noms réseau, donc l’application peut encore interroger le résolveur de l’hôte pendant que ses paquets sortent par le tunnel. Lisez-le depuis l’intérieur du conteneur et définissez dns: sur l’application lorsque des noms privés doivent être résolus par le tunnel.

docker compose exec app ip route
docker compose exec app cat /etc/resolv.conf
docker compose exec app curl -s https://ifconfig.me

Si la liste des routes contient l’interface du tunnel et que l’adresse est celle du fournisseur, l’application est dans le tunnel. Si l’interface manque, app a démarré avant que vpn n’ait un tunnel, donc recréez app dès que vpn signale être en bonne santé.

Placez un véritable écouteur proxy dans l’espace de noms pour les clients de l’hôte

Un navigateur de bureau ne peut pas rejoindre un espace de noms, donc le tunnel doit être exposé comme service. Exécutez un vrai proxy SOCKS ou HTTP dans l’espace de noms vpn et ne publiez que son port :

services:
  vpn:
    ports:
      - "127.0.0.1:1080:1080"
  proxy:
    image: your-proxy
    network_mode: service:vpn
    command: ["--socks", "0.0.0.0:1080"]

L’adresse côté hôte est la moitié importante de cette correspondance. 127.0.0.1:1080 à gauche garde l’écouteur hors de votre LAN ; un "1080:1080" nu publie un proxy ouvert à tout appareil qui peut atteindre votre machine.

Le côté conteneur a son propre piège. Les ports publiés sont livrés à l’adresse du conteneur sur le pont, pas à son loopback, donc un proxy démarré avec --socks 127.0.0.1:1080 dans l’espace de noms accepte les connexions des voisins et refuse tout ce qui arrive par le port publié. Liez-le à 0.0.0.0 et comptez sur la liaison loopback côté hôte pour l’isolation. La même règle s’applique à l’adresse d’écoute dans un fichier de configuration proxy.

Décidez ensuite où les noms sont résolus. Dans curl, --socks5 résout le nom d’hôte localement et --socks5-hostname envoie le nom au proxy ; les navigateurs exposent le même commutateur sous un autre libellé. La résolution locale divulgue la requête même lorsque la page se charge par le tunnel.

Ne traitez le conteneur comme passerelle qu’avec une route de retour

Les routeurs, les VM et les téléphones n’utiliseront pas un champ proxy HTTP, et certaines applications ignorent totalement les variables proxy. Router leur trafic signifie couche 3 : le conteneur transfère les paquets et traduit les adresses sur l’interface du tunnel.

sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -o tun0 -j MASQUERADE

Deux choses sont faciles à manquer. Le client a besoin d’une route qui pointe sa passerelle par défaut vers cet hôte, et les réponses doivent retrouver le chemin du retour, soit par une route statique sur le routeur amont, soit par NAT sur l’interface tournée vers les clients. Sans l’un des deux, le tunnel emporte la requête et la réponse disparaît.

Il a aussi le rayon d’impact le plus large. Un hôte servant de passerelle pour tout le LAN envoie chaque client mal configuré par votre compte VPN, et une chute du tunnel soit coupe le LAN, soit retombe sur l’ISP selon le kill switch.

Vérifiez le chemin de sortie, puis revenez en arrière dans l’ordre

Vérifiez dans cet ordre et arrêtez-vous au premier échec : adresse de sortie dans l’espace de noms, adresse de sortie depuis l’hôte via le proxy, puis résolution de noms pour un hôte privé.

docker compose exec vpn curl -s https://ifconfig.me
curl -s -x socks5h://127.0.0.1:1080 https://ifconfig.me
curl -s -x socks5h://127.0.0.1:1080 http://internal.example/health

Les deux premières adresses doivent correspondre au fournisseur, et la troisième doit se résoudre via le proxy au lieu d’échouer sur une erreur de nom. Une première ligne concordante et une deuxième en échec signifient que l’écouteur ou le port publié est faux, pas le tunnel.

Le retour en arrière est l’inverse de l’installation. Retirez les variables d’environnement proxy du profil du shell et de toute unité systemd, puis arrêtez la pile pour que l’espace de noms partagé disparaisse avec son propriétaire. Supprimez le port publié avant de supprimer le service proxy, sinon l’hôte garde un écouteur loopback que personne ne lit.

Avant de continuer, confirmez quatre choses : le client qui vous importe atteint réellement le tunnel, seul le propriétaire de l’espace de noms publie des ports, les noms privés se résolvent là où vous le vouliez, et rien n’écoute sur une adresse d’hôte autre que loopback. Chacune de ces vérifications échoue silencieusement en production, et chacune est un contrôle d’une ligne ci-dessus.

Articles similaires