Build the lab yourself
The lab runs on any machine with Docker, including an Apple Silicon Mac, in a few hundred megabytes. The router runs real systemd, so the services and timer work as on a Debian server.
- Put the files from this page in one directory, with the router's files from earlier in the lesson.
docker compose -f lab-compose.yaml up -d --build, then./lab-setup.shto address every node and set up the ISPs' filters.- Copy the router's files into place on
gwwithdocker compose cp, then follow the lesson: forwarding and nftables, the first default route, the routing tables, the marks and the health check. - To fail ISP 1 the way the lesson does, run this on
isp1, and delete the table again to repair it:
nft add table ip outage
nft add chain ip outage upstream '{ type filter hook forward priority 0; policy drop; }'
As in the other Debian labs, lab-setup.sh turns IP forwarding off on the router, because Docker Desktop starts every container with it on.
# Two internet connections on Debian: the lab. docker compose -f lab-compose.yaml up -d --build && ./lab-setup.sh
name: dwan-lab
x-host: &host
build: { context: ., dockerfile: Dockerfile.host }
image: ttr-dwan-host:13
privileged: true # ip, sysctl, nft
services:
gw:
build: { context: ., dockerfile: Dockerfile.gw }
image: ttr-dwan-gw:13
hostname: gw
privileged: true
cgroup: host # let systemd run as PID 1
volumes: ["/sys/fs/cgroup:/sys/fs/cgroup:rw"]
tmpfs: ["/run", "/run/lock"]
networks:
wan1: { interface_name: eth0, priority: 300 }
wan2: { interface_name: eth1, priority: 200 }
lan: { interface_name: eth2, priority: 100 }
isp1: { <<: *host, hostname: isp1, networks: { wan1: { interface_name: eth0, priority: 200 }, up1: { interface_name: eth1, priority: 100 } } }
isp2: { <<: *host, hostname: isp2, networks: { wan2: { interface_name: eth0, priority: 200 }, up2: { interface_name: eth1, priority: 100 } } }
core:
<<: *host
hostname: core
networks:
up1: { interface_name: eth0, priority: 300 }
up2: { interface_name: eth1, priority: 200 }
net: { interface_name: eth2, priority: 100 }
web: { <<: *host, hostname: web, networks: { net: { interface_name: eth0 } } }
pc: { <<: *host, hostname: pc, networks: { lan: { interface_name: eth0 } } }
# Layer-2-only networks: every address and route comes from lab-setup.sh.
networks:
wan1: { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:a3::/64" }] } }
wan2: { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:a4::/64" }] } }
lan: { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:a5::/64" }] } }
up1: { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:a6::/64" }] } }
up2: { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:a7::/64" }] } }
net: { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:a8::/64" }] } }- Lines 8–19: The router runs systemd as PID 1, with three networks named in order.
- Lines 32–38: Layer-2-only networks; addresses come from lab-setup.sh.
#!/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.
for n in gw isp1 isp2 core web pc; do x $n 'sysctl -qw net.ipv6.conf.all.disable_ipv6=1'; done
# Docker Desktop turns forwarding on in every container; a fresh Debian router has it off.
x gw 'sysctl -qw net.ipv4.ip_forward=0'
# The office router: one address from each ISP, and the office LAN. No routes yet.
x gw 'ip addr add 198.51.100.2/28 dev eth0; ip addr add 203.0.113.2/28 dev eth1; ip addr add 10.20.0.1/24 dev eth2'
x pc 'ip addr add 10.20.0.10/24 dev eth0; ip route add default via 10.20.0.1'
# Two ISPs. Each accepts only its own addresses from the customer (BCP38 source
# filtering, as real ISPs should), and routes the rest of the internet via core.
x isp1 'ip addr add 198.51.100.1/28 dev eth0; ip addr add 198.51.100.242/28 dev eth1; ip route add default via 198.51.100.241; sysctl -qw net.ipv4.ip_forward=1
nft add table ip bcp38; nft add chain ip bcp38 customer "{ type filter hook forward priority 0; }"
nft add rule ip bcp38 customer iifname eth0 ip saddr != 198.51.100.0/28 counter drop'
x isp2 'ip addr add 203.0.113.1/28 dev eth0; ip addr add 203.0.113.242/28 dev eth1; ip route add default via 203.0.113.241; sysctl -qw net.ipv4.ip_forward=1
nft add table ip bcp38; nft add chain ip bcp38 customer "{ type filter hook forward priority 0; }"
nft add rule ip bcp38 customer iifname eth0 ip saddr != 203.0.113.0/28 counter drop'
# The internet: core routes each ISP's customer range to that ISP, and hosts the web.
x core 'ip addr add 198.51.100.241/28 dev eth0; ip addr add 203.0.113.241/28 dev eth1; ip addr add 192.0.2.1/24 dev eth2; sysctl -qw net.ipv4.ip_forward=1
ip route add 198.51.100.0/28 via 198.51.100.242; ip route add 203.0.113.0/28 via 203.0.113.242'
# A web server that logs visitors, with a second address used as a health-check target.
x web 'ip addr add 192.0.2.80/24 dev eth0; ip addr add 192.0.2.53/24 dev eth0; ip route add default via 192.0.2.1'
x web 'cd /tmp && nohup python3 -m http.server 80 > /tmp/web.log 2>&1 &'
# A web server in the office, to publish through both ISPs.
x pc 'cd /tmp && nohup python3 -m http.server 80 > /tmp/web.log 2>&1 &'- Lines 6–7: Forwarding off on the router, as on a fresh Debian install.
- Lines 11–18: Each ISP drops packets from the office that don't carry its own addresses.
- Lines 20–21: The internet core knows which ISP each office address belongs to.
# The office router: Debian 13 with systemd, so the health check runs as a real timer.
FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
systemd systemd-sysv nftables iproute2 iputils-ping tcpdump procps less curl conntrack \
&& rm -rf /var/lib/apt/lists/* \
&& systemctl mask systemd-modules-load.service
CMD ["/sbin/init"]# ISPs, the internet and the office PC: plain Debian 13 with network tools.
FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
iproute2 iputils-ping tcpdump curl python3 nftables procps \
&& rm -rf /var/lib/apt/lists/*
CMD ["sleep", "infinity"]Where next
- nftables and iptables on Debian: host and router firewallsPrerequisite · Intermediate · 60 min
Build a Debian firewall with nftables, from a host firewall to an office router with NAT and port forwards: safe changes with a timed rollback, sets and maps, logging and packet tracing, and what iptables really is on Debian 13.
- Two internet connections on VyOSAlso covers failover, policy routing · Intermediate · 45 min
Add a second ISP to a VyOS office router and make it actually work: why a backup default route misses most outages, health checks that test beyond the ISP, per-ISP NAT, port forwards through both ISPs, and what happens to open connections, all measured on a real lab.
- OpenVPN on Debian: users, routing and revocationAlso covers 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.
- WireGuard on Debian: users, routing and keysAlso covers debian, nftables · Intermediate · 50 min
Run a WireGuard server on Debian for several users, an office LAN and a branch: keys, wg-quick and systemd, AllowedIPs as routing and access control, split and full tunnels, roaming and keepalives, adding and removing users live, and the silent failures.