Build the lab yourself
The lab runs on any machine with Docker, including an Apple Silicon Mac, in a few hundred megabytes. The VPN server runs real systemd, so systemctl enable --now wg-quick@wg0 works exactly as on a Debian server; the users run wg-quick up wg0 directly.
- Put the files from this page in one directory, with
wg0-server.conf, the client configs,nftables.confand99-wireguard.conffrom earlier in the lesson. docker compose -f lab-compose.yaml up -d --build, then./lab-setup.shto address every node.- Generate a key pair on the server and on each user, as in The server. Your keys will be different from the lesson's, so put your public keys into each
[Peer], and the server's public key into each client's[Peer]. - Copy each config to
/etc/wireguard/wg0.confon its machine (docker compose cp),chmod 600it, and bring the tunnels up. - Add the return routes on
lanrtr, and work through the scenarios.
WireGuard works in Docker Desktop's containers because Docker Desktop's Linux kernel includes it (the versions output at the start of the lesson creates a test interface to prove it); on a Linux host it's in every kernel since 5.6.
One deliberate difference from a plain Docker container: Docker Desktop starts every container with IP forwarding already on, which would hide a missing 99-wireguard.conf. lab-setup.sh turns it off on the VPN server, so it behaves like a fresh Debian install and the lesson's sysctl file is what turns it on.
# WireGuard on Debian: the lab. docker compose -f lab-compose.yaml up -d --build && ./lab-setup.sh
name: wg-lab
x-host: &host
build: { context: ., dockerfile: Dockerfile.host }
image: ttr-wg-host:13
privileged: true # ip, sysctl, WireGuard interfaces
services:
vpn:
build: { context: ., dockerfile: Dockerfile.vpn }
image: ttr-wg-vpn:13
hostname: vpn
privileged: true
cgroup: host # let systemd run as PID 1
volumes: ["/sys/fs/cgroup:/sys/fs/cgroup:rw"]
tmpfs: ["/run", "/run/lock"]
networks: { server: { interface_name: eth0, priority: 100 }, office: { interface_name: eth1 } }
isp: { <<: *host, hostname: isp, networks: { users: { interface_name: eth0, priority: 100 }, server: { interface_name: eth1 } } }
web: { <<: *host, hostname: web, networks: { server: { interface_name: eth0 } } }
lanrtr: { <<: *host, hostname: lanrtr, networks: { office: { interface_name: eth0 } } }
office: { <<: *host, hostname: office, networks: { office: { interface_name: eth0 } } }
alice: { <<: *host, hostname: alice, networks: { users: { interface_name: eth0 } } }
bob: { <<: *host, hostname: bob, networks: { users: { interface_name: eth0 } } }
carol: { <<: *host, hostname: carol, networks: { users: { interface_name: eth0 } } }
branch: { <<: *host, hostname: branch, networks: { users: { interface_name: eth0, priority: 100 }, branchlan: { interface_name: eth1 } } }
branchpc: { <<: *host, hostname: branchpc, networks: { branchlan: { interface_name: eth0 } } }
# Layer-2-only networks: every address and route comes from lab-setup.sh.
# Docker names the first network it attaches eth0 (alphabetically), so each
# two-network node gives its eth0 network the highest priority.
networks:
users: { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:81::/64" }] } }
server: { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:82::/64" }] } }
office: { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:83::/64" }] } }
branchlan: { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:84::/64" }] } }- Lines 8–16: The VPN server runs systemd as PID 1: privileged, with the host's cgroups and tmpfs mounts for /run.
- Line 6: Every node is privileged, so it can create WireGuard interfaces and set sysctls.
#!/bin/sh
# Address every node and give it its routes. Run after `docker compose up`.
x() { docker compose -f lab-compose.yaml exec -T "$1" sh -c "$2"; }
# IPv4 only, for clean output. (Disabling IPv6 at container creation instead makes
# Docker attach an extra bridge, because the lab networks themselves are IPv6-only.)
for n in vpn isp web lanrtr office alice bob carol branch branchpc; do x $n 'sysctl -qw net.ipv6.conf.all.disable_ipv6=1'; done
# Docker Desktop turns forwarding on in every container. A fresh Debian server has it off,
# so put the VPN server back to that: the lesson turns it on with a sysctl.d file.
x vpn 'sysctl -qw net.ipv4.ip_forward=0'
# The "internet": the users' side (198.51.100.0/24) and the server's side (203.0.113.0/24), joined by isp.
x isp 'ip addr add 198.51.100.1/24 dev eth0; ip addr add 203.0.113.1/24 dev eth1; sysctl -qw net.ipv4.ip_forward=1'
x web 'ip addr add 203.0.113.80/24 dev eth0; ip route add default via 203.0.113.1'
x vpn 'ip addr add 203.0.113.10/24 dev eth0; ip addr add 10.10.0.1/24 dev eth1; ip route add default via 203.0.113.1'
x alice 'ip addr add 198.51.100.21/24 dev eth0; ip route add default via 198.51.100.1'
x bob 'ip addr add 198.51.100.22/24 dev eth0; ip route add default via 198.51.100.1'
x carol 'ip addr add 198.51.100.24/24 dev eth0; ip route add default via 198.51.100.1'
x branch 'ip addr add 198.51.100.23/24 dev eth0; ip route add default via 198.51.100.1; ip addr add 10.20.0.1/24 dev eth1; sysctl -qw net.ipv4.ip_forward=1'
x branchpc 'ip addr add 10.20.0.10/24 dev eth0; ip route add default via 10.20.0.1'
# The office: its own router (lanrtr) is every office machine's default gateway.
x lanrtr 'ip addr add 10.10.0.254/24 dev eth0; sysctl -qw net.ipv4.ip_forward=1'
x office 'ip addr add 10.10.0.20/24 dev eth0; ip route add default via 10.10.0.254'
# A tiny web server on the internet that logs who connects.
x web 'cd /tmp && nohup python3 -m http.server 80 > /tmp/web.log 2>&1 &'# The WireGuard server: Debian 13 with systemd, so the tunnel runs as wg-quick@wg0.
FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
systemd systemd-sysv wireguard-tools nftables iproute2 iputils-ping tcpdump procps less \
&& rm -rf /var/lib/apt/lists/* \
&& systemctl mask systemd-modules-load.service
CMD ["/sbin/init"]# Users, routers and servers: plain Debian 13 with WireGuard's tools and network tools.
FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
wireguard-tools iproute2 iputils-ping tcpdump nftables curl python3 procps \
&& rm -rf /var/lib/apt/lists/*
CMD ["sleep", "infinity"]# /etc/wireguard/wg0.conf on carol's computer (wg-quick up wg0)
[Interface]
Address = 10.9.0.12/24
PostUp = wg set %i private-key /etc/wireguard/carol.key
[Peer]
# The VPN server
PublicKey = /S03YCIRbPGjlhE53GYhxFEh8LVo/Joel3BkEgWeeXk=
Endpoint = 203.0.113.10:51820
# Split tunnel: only the VPN, the office LAN and the branch LAN go through it.
AllowedIPs = 10.9.0.0/24, 10.10.0.0/24, 10.20.0.0/24
# Send a packet every 25 s, so NAT and firewalls on the way keep the path open.
PersistentKeepalive = 25# /etc/wireguard/wg0.conf on bob's computer (wg-quick up wg0)
[Interface]
Address = 10.9.0.11/24
PostUp = wg set %i private-key /etc/wireguard/bob.key
[Peer]
# The VPN server
PublicKey = /S03YCIRbPGjlhE53GYhxFEh8LVo/Joel3BkEgWeeXk=
Endpoint = 203.0.113.10:51820
# Split tunnel: only the VPN, the office LAN and the branch LAN go through it.
AllowedIPs = 10.9.0.0/24, 10.10.0.0/24, 10.20.0.0/24
# Send a packet every 25 s, so NAT and firewalls on the way keep the path open.
PersistentKeepalive = 25Where next
- OpenVPN on Debian: users, routing and revocationAlso covers VPN, debian, nftables · Intermediate · 60 min
Install and run an OpenVPN server on Debian with your own certificate authority: one certificate per user, fixed addresses, routing to an office and a branch LAN, split and full tunnels, tls-crypt, revoking users, and the mistakes that lock everyone out.
- Site-to-site VPN with WireGuard on VyOSAlso covers VPN, wireguard · Intermediate · 35 min
Join two sites' private LANs across the internet with WireGuard on VyOS: keys and peers, the AllowedIPs mistake and what it teaches, what the internet actually sees, and the tunnel's smaller MTU.
- Linux Network ConnectivityPrerequisite · Beginner · 80 min
Find out why a Linux machine can't reach something, layer by layer: interfaces and addresses, routes and ip route get, ARP and the neighbour table, ping and traceroute, DNS, and listening sockets — with real output from a three-host lab.
- PowerDNS on Debian: authoritative, secondary, DNSSEC and recursorAlso covers debian · Intermediate · 60 min
Build a complete DNS service on Debian with PowerDNS: authoritative primary and secondary servers with NOTIFY and AXFR, zones by pdnsutil and the HTTP API, DNSSEC signed all the way from a lab root, and a validating recursor for the office.