Routing a docker network via wireguard

← Back

Exposing a home lab to the internet sounds easy, right?

The easiest option is to expose LAN directly from the router, which does the job but imposes a huge security risk. The second option is to set up a VPN on the router and share the client configuration, so LAN would only be accessible via VPN. While secure, it makes it harder to share with other people. The third option is Tailscale or Cloudflare Tunnel. They do the heavy lifting by providing servers and software for remote connections. The latter allows global public access, so anyone can connect. And the fourth option is to rent a VPS, set up a firewall, reverse proxy, separate Docker network, harden the system, and deploy a host-based intrusion detection system (HIDS)...

In the previous articles I've already covered firewall, reverse proxy setups, and system hardening. I'll be using WireGuard, but any other VPN solution would mostly have the same setup.

Install iproute2

sudo pacman -S iproute2
sudo mkdir -p /etc/iproute2
sudo touch /etc/iproute2/rt_tables
sudo chmod 644 /etc/iproute2/rt_tables

Create a new route table

echo "200 vpn_docker" | sudo tee -a /etc/iproute2/rt_tables

Next we need to pick an IP range for the network.
In my case I went for 172.30.0.0/16.

Create a new Docker network in that range

docker network create \
  --driver bridge \
  --opt com.docker.network.driver.mtu=1420 \
  --subnet=172.30.0.0/16 \
  vpn_docker_net

Add a routing rule

sudo ip rule add from 172.30.0.0/16 table vpn_docker

Add a NAT rule

sudo iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE
sudo ip route add 10.66.66.0/24 dev wg0

WireGuard config

[Interface]
PrivateKey =
Address = 10.66.66.2/32
DNS = 9.9.9.11,9.9.9.9

# Uncomment the next line to set a custom MTU
# This might impact performance, so use it only if you know what you are doing
# See https://github.com/nitred/nr-wg-mtu-finder to find your optimal MTU
MTU = 1320

Table = vpn_docker

[Peer]
PublicKey =
PresharedKey =
Endpoint =
# Since the table has been altered it won't proxy the whole system's traffic
AllowedIPs = 0.0.0.0/0, ::/0

Make it persistent

tee -a /usr/local/bin/vpn-docker-setup.sh << EOF
#!/bin/bash

# 1. Create the routing table if it doesn't exist
if ! grep -q "vpn_docker" /etc/iproute2/rt_tables; then
    echo "200 vpn_docker" | tee -a /etc/iproute2/rt_tables
fi

# 2. Add the IP rule if it doesn't exist
if ! ip rule show | grep -q "from 172.30.0.0/16 table vpn_docker"; then
    ip rule add from 172.30.0.0/16 table vpn_docker
fi

# 3. Add the NAT masquerade if it doesn't exist
if ! iptables -t nat -C POSTROUTING -o wg0 -j MASQUERADE >/dev/null 2>&1; then
    iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE
fi

# 4. Manage the routes for the vpn_docker table
# We delete the old default in that table to prevent conflicts, then add the new one
ip route del default dev wg0 table vpn_docker 2>/dev/null
ip route add default via 10.66.66.1 dev wg0 table vpn_docker

# 5. Ensure the local subnet is reachable via wg0
if ! ip route show | grep -q "10.66.66.0/24 dev wg0"; then
    ip route add 10.66.66.0/24 dev wg0
fi
EOF
chmod +x /usr/local/bin/vpn-docker-setup.sh

Systemd

tee -a /etc/systemd/system/docker-wg-routing.service << EOF
[Unit]
Description=Configure Docker VPN Routing Table
# Wait for the network and the WireGuard interface to be up
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/vpn-docker-setup.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable vpn-docker-setup.service

Update the Docker service

/usr/lib/systemd/system/docker.service
After=network-online.target nss-lookup.target docker.socket firewalld.service containerd.service time-set.target docker-wg-routing.service

Also add net.ipv4.ip_forward=1 to sysctl to allow traffic forwarding.

echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.conf
sysctl --system

Add this network to the containers and recreate them:

networks:
  vpn_docker_net:
    external: true