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_tcp en TCP vers $dmz_ip à travers le tunnel
  • nater le traffic Internet entrant sur le port $fwd_port_tcp en 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_KEY est la clé privée Wireguard du VPS
  • WIREGUARD_SRV_PUB_KEY est la clé publique Wireguard du serveur docker
  • WIREGUARD_SHARED_KEY est 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_KEY est la clé privée Wireguard du serveur Docker
  • IP_VPS est l’IP publique du serveur VPS
  • WIREGUARD_VPS_PUB_KEY est la clé publique Wireguard du VPS
  • WIREGUARD_SHARED_KEY est la même clé privée partagée