Router le traffic d'un containeur ailleurs
Soit un serveur docker hébergeant plusieurs containeurs. J’aimerais bien qu’un de ces containeurs sorte sur Internet non pas via ma connexion Internet personelle mais via une IP différente. L’Internet à deux vitesses, ce n’est pas un mythe. Google ou LinkedIn pour ne citer qu’eux, réagissent différemment si vous y accéder depuis chez vous ou depuis une IP non marquée comme utilisée par les FAI. Nous allons donc prendre un petit VPS peuchère et empreinter son IP en configurant le routage de ce containeur pour sortir avec l’IP de ce VPS.
flowchart LR
subgraph Maison
pc@{ icon: "material-symbols-light:computer-outline", form: "square", label: "PC" }
docker@{ icon: "material-icon-theme:docker", form: "square", label: "Serveur docker" }
livebox@{ icon: "material-symbols-light:router-outline", form: "square", label: "Router" }
docker-->livebox
pc-->livebox
end
internet@{ icon: "material-symbols-light:cloud", form: "square", label: "Internet" }
vps@{ icon: "mdi:server-outline", form: "square", label: "VPS" }
livebox-->internet
vps---internet
docker-.tunnel Wireguard.->vps
Pré-requis
Si cette architecture peut être faite avec n’importe quel Linux, on se focalisera sur Ubuntu, pour netplan, qui permet de configurer le routage et le tunneling au même endroit avec une syntaxe commune.
Pour le VPS, le seul critère en terme de ressource sera la bande passante disponible. Le VPS le plus crappy avec une connexion 100MBps ou plus sera amplement suffisant.
Configuration du VPS
La configuration étant 100% réseau, le paramétrage se décomposera en :
- la configuration du pare-feu
- la configuration des interfaces réseaux
On commence par installer les paquets nécessaires :
apt install -y wireguard iproute2 nftables
Configuration du pare-feu
Puis on y installe un pare-feu minimal mais fonctionnel. Ce pare-feu va:
- autoriser le tunnel wireguard à s’établir (sans autoriser ce qu’il passe dans le tunnel)
- autoriser le traffic du tunnel wireguard vers internet s’il provient de
$dmz_ip - nater le traffic Internet entrant sur le port
$fwd_port_tcpen TCP vers$dmz_ipà travers le tunnel - nater le traffic Internet entrant sur le port
$fwd_port_tcpen UDP vers$dmz_ipà travers le tunnel - logger ce qui est bloqué pour faciliter le debug avec
dmesg
Pour cela, on crée le fichier /etc/nftables.conf avec le contenu suivant :
#!/usr/sbin/nft -f
table inet filter {}
flush table inet filter
# default interface: ip a l | grep ^default | head -1 | awk '{print $5}'
define wan = ens3
define wg_port = 51820
define dmz_ip = 192.18.0.3
define fwd_port_tcp = 6881
define fwd_port_udp = 6881
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
ct state { established, related } counter accept
ct state invalid counter drop
icmp type echo-request counter accept
icmpv6 type echo-request counter accept
icmp type timestamp-request counter drop
iifname lo counter accept
tcp dport 22 limit rate 20/second burst 5 packets counter accept
udp dport $wg_port counter accept # wireguard
meta pkttype host limit rate 20/second burst 5 packets log prefix "in: " counter # log
}
chain forward {
type filter hook forward priority filter; policy drop
ct state { established, related } counter accept
ct state invalid counter drop
icmp type echo-request counter accept
icmpv6 type echo-request counter accept
iifname wg0 oifname $wan ip saddr $dmz_ip counter accept
iifname $wan oifname wg0 tcp dport $fwd_port_tcp counter accept
iifname $wan oifname wg0 udp dport $fwd_port_udp counter accept
meta pkttype host limit rate 20/second burst 5 packets log prefix "fwd: " counter # log
}
chain prerouting {
type nat hook prerouting priority dstnat;
iifname $wan tcp dport $fwd_port_tcp dnat ip to $dmz_ip
iifname $wan udp dport $fwd_port_udp dnat ip to $dmz_ip
}
chain postrouting {
type nat hook postrouting priority srcnat;
ip saddr 172.18.253.0/24 oifname $wan masquerade
ip saddr $dmz_ip oifname $wan masquerade
}
# chain output {
# type filter hook output priority 0;
# }
}
wan désigne l’interface par défaut. On peut la récupérer simplement à partir de :
ip a l | grep ^default | head -1 | awk '{print $5}'
Le fichier nftables.conf doit être owné par root:root avec le mode 0755.
On n’a plus qu’à le tester en l’exécutant :
/etc/nftables.conf
nft list table inet filter
Si la première commande ne renvoit rien, ce qui est le résultat escompté, la second commande affichera la configuration du pare-feu, limité à la table inet filter.
Si le test est réussi, on va pouvoir activer le chargement des règles par Systemd :
systemctl enable --now nftables
On va pouvoir activer le routage des paquets au niveau du kernel Linux.
# effectif à chaque redémarrage
echo 'net.ipv4.ip_forward=1' > /etc/sysctl.d/20-network-ipv4-forward.conf
# pour l'activer tout de suite
sysctl net.ipv4.ip_forward=1
Configuration du tunnel Wireguard
echo -n PSK=; wg genpsk; echo -n PUB=; wg genkey | tee /tmp/wg_priv | wg pubkey; echo -n KEY=; cat /tmp/wg_priv; rm /tmp/wg_priv
Ce one-liner va générer, respectivement
- une psk (clé privée partagée)
- la clé publique associée à la clé privée ci-dessous
- une clé privée
Exécutez-le deux fois pour générer une paire de clé public/privée pour le serveur VPS et une autre pour le serveur Docker, qui seront les deux bouts du tunnel Wireguard. On prendre une seule PSK, qui sera un secret commun aux deux serveurs
On va pouvoir passer à la configuration réseau, avec netplan.
Créons un fichier /etc/netplan/60-wireguard.yaml
network:
version: 2
tunnels:
wg0:
mode: wireguard
port: 51820
key: WIREGUARD_VPS_PRIV_KEY
addresses:
- 198.18.0.1/29
peers:
- allowed-ips: [198.18.0.3/32]
keys:
public: WIREGUARD_SRV_PUB_KEY
shared: WIREGUARD_SHARED_KEY
où :
WIREGUARD_VPS_PRIV_KEYest la clé privée Wireguard du VPSWIREGUARD_SRV_PUB_KEYest la clé publique Wireguard du serveur dockerWIREGUARD_SHARED_KEYest la clé privée partagée
Le paramètre allowed-ips liste les réseaux source autorisés dans les paquets tunnelisés par ce peer. Tout paquet sera purement dropé avant même d’être traité par la stack réseau Linux.
Ce fichier doit être owné par root:root avec le mode 0600.
On teste que le fichier ne présente pas d’anomalie avec :
netplan apply
La configuration sur le VPS est terminée, on va passer au serveur docker.
Configuration du serveur Docker
La configuration va être similaire à celle du VPS, à la différence que nous allons maquer le containeur concerné pour le router différemment.
Pour chaque stack Docker, un réseau dédié à cette stack est créé. Le plus simple va être de définir un range d’IP non-utilisé et de configurer le routage en fonction de ce range. Ainsi tout containeur qui aura une IP dans ce range sera routé dans le tunnel Wireguard.
Pour la suite, le range 172.18.253.0/24 sera celui sur lequel le routage sera fait vers le tunnel Wireguard.
Isoler le containeur à router
Supposons que le containeur concerné soit dante, un serveur SOCKS5. Ce containeur tourne tout seul et nous pourrions le lancer simplement avec un docker run. Mais pour le côté pratique d’avoir l’intégralité de la configuration sous la forme d’un fichier de configuration, nous allons plutôt le déployer avec un fichier compose.yaml tel que ci-dessous :
networks:
default:
ipam:
driver: default
config:
- subnet: 172.18.253.16/28
gateway: 172.18.253.30
services:
dante:
image: ghcr.io/aeron/socks5-dante-proxy
container_name: dante
networks:
default:
ports:
- 1080:1080
environment:
WORKERS: 4
CONFIG: /etc/sockd.conf
volumes:
- ./conf/dante.conf:/etc/sockd.conf
Au lieu de laisser Docker générer un réseau pour cette stack à partir de son pool d’adresse, nous allons spécifier explicitement le réseau dans la plage 172.18.253.0/24.
Pour éviter tout conflit avec les réseaux générés automatiquement par Docker, il est préférable que le range soit en dehors du pool d’adresses Docker. Vous pouvez lister ces pools avec :
docker info -f json | jq '.DefaultAddressPools'
ce qui donne chez moi :
[
{
"Base": "172.30.0.0/16",
"Size": 26
}
]
Le range 172.18.253.0/24 n’ayant aucune intersection avec le pool d’adresses de Docker 172.30.0.0/16, on est tranquille.
On veillera également à ce que chaque stack utilisant le range 172.18.253.0/24 ait bien leur propre réseau distinct. La configuration étant manuelle, Docker s’exécutera bêtement et pètera une erreur pas très lisible le cas échéant.
Ici le subnet 172.18.253.16/28 a 4 bits de libres (de 29 à 32), soit 2^4-2=14 IPs disponibles. Si non spécifié, comme ici, Docker assignera une IP à chaque containeur connecté à se réseau dans l’ordre croissant, c’est pourquoi on assignera la dernière IP utilisable de ce subnet à la gateway, 172.18.253.30.
Configuration réseau
Nous allons maintenant configurer à la fois le tunnel Wireguard et le routage.
# ip l d wg0 && systemctl restart systemd-networkd && netplan apply
network:
version: 2
tunnels:
wg0:
mode: wireguard
key: WIREGUARD_SRV_PRIV_KEY
addresses: [198.18.0.3/29]
peers:
- endpoint: IP_VPS:51820
keys:
public: WIREGUARD_VPS_PUB_KEY
shared: WIREGUARD_SHARED_KEY
allowed-ips: [0.0.0.0/0]
keepalive: 25
routing-policy:
- from: 172.18.253.0/24
table: 45
- mark: 8
table: 45
from: 172.18.253.0/28
routes:
- to: 0.0.0.0/0
via: 192.18.0.1
table: 45
on-link: true
- to: 198.18.0.0/29
table: 45
scope: link
on-link: true
où :
WIREGUARD_SRV_PRIV_KEYest la clé privée Wireguard du serveur DockerIP_VPSest l’IP publique du serveur VPSWIREGUARD_VPS_PUB_KEYest la clé publique Wireguard du VPSWIREGUARD_SHARED_KEYest la même clé privée partagée