Build the lab yourself
The lab runs on any machine with Docker, including an Apple Silicon Mac, in about 300 MB of memory. The router is a Debian 13 container running systemd, udev and D-Bus, so systemctl, networkctl and journalctl behave as on a real install; the other machines are plain Debian containers.
- Put the files from this page in one directory, with the router's configuration files from earlier in the lesson in
etc/systemd/network/,etc/sysctl.d/andetc/nftables.conf. docker compose -f lab-compose.yaml up -d --build, then./lab-setup.sh. It addresses the ISP, the web server and the office hosts (each host creates its own VLAN interface on the trunk), and turns forwarding off on the router, like a fresh install.- Copy the networkd files in with
docker compose cp, and follow the lesson from Interfaces with systemd-networkd:docker compose -f lab-compose.yaml exec router bash.
The lab is also the starting point for the later lessons on the Build a router in a box with Debian path, which add to the same router.
Two things the lab needed that a real Debian install already has: udev (without it, networkd leaves every link pending) and D-Bus (without it, networkctl reload fails with Failed to connect to system bus). The router's Dockerfile installs both, and masks the console logins a container can't have.
# Debian as an office router: the lab. docker compose -f lab-compose.yaml up -d --build && ./lab-setup.sh
name: drt-lab
x-host: &host
build: { context: ., dockerfile: Dockerfile.host }
image: ttr-drt-host:13
privileged: true # ip, sysctl and VLAN interfaces
services:
router:
build: { context: ., dockerfile: Dockerfile.router }
image: ttr-drt-router:13
hostname: router
privileged: true
cgroup: host # let systemd run as PID 1
volumes: ["/sys/fs/cgroup:/sys/fs/cgroup:rw"]
tmpfs: ["/run", "/run/lock"]
# Fixed MACs, like real network cards: a restart is then like a reboot, not a new machine.
networks: { wan: { interface_name: eth0, priority: 100, mac_address: "02:00:00:00:01:00" }, trunk: { interface_name: eth1, mac_address: "02:00:00:00:01:01" } }
isp:
build: { context: ., dockerfile: Dockerfile.isp }
image: ttr-drt-isp:13
hostname: isp
privileged: true
networks: { wan: { interface_name: eth0, priority: 100 }, internet: { interface_name: eth1 } }
web: { <<: *host, hostname: web, networks: { internet: { interface_name: eth0 } } }
# Fixed MACs, so a DHCP reservation can name the server's.
staff: { <<: *host, hostname: staff, networks: { trunk: { interface_name: eth0, mac_address: "02:00:00:00:10:20" } } }
guest: { <<: *host, hostname: guest, networks: { trunk: { interface_name: eth0, mac_address: "02:00:00:00:20:20" } } }
server: { <<: *host, hostname: server, networks: { trunk: { interface_name: eth0, mac_address: "02:00:00:00:30:10" } } }
# Layer-2-only networks: every address and route comes from the router's
# configuration or lab-setup.sh. "trunk" is the office switch: the hosts tag
# their own frames (eth0.10, eth0.20, eth0.30), like ports on a VLAN switch.
networks:
wan: { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:c1::/64" }] } }
trunk: { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:c2::/64" }] } }
internet: { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:c3::/64" }] } }- Lines 8–17: The router: systemd as PID 1, privileged, with fixed MAC addresses so a restart behaves like a reboot.
- Lines 25–28: The office hosts all share the trunk network and tag their own frames. Their fixed MACs matter in the DHCP lesson, where the server's is reserved.
- Lines 33–35: Layer-2-only networks: no Docker addresses or routes to get in the way.
#!/bin/sh
# Address everything except the router (its configuration is the lesson). Run after `docker compose up`.
x() { docker compose -f lab-compose.yaml exec -T "$1" sh -c "$2"; }
# IPv4 only, for clean output. (The IPv6 lesson turns it back on where it needs it.)
for n in router isp web staff guest server; do x $n 'sysctl -qw net.ipv6.conf.all.disable_ipv6=1'; done
# Docker Desktop turns forwarding on in every container. A fresh Debian install has it off,
# so put the router back to that: the lesson turns it on with a sysctl.d file.
x router 'sysctl -qw net.ipv4.ip_forward=0'
# The ISP: the router's upstream (203.0.113.1) and the way to the "internet".
x isp 'ip addr add 203.0.113.1/24 dev eth0; ip addr add 198.51.100.1/24 dev eth1; sysctl -qw net.ipv4.ip_forward=1'
x web 'ip addr add 198.51.100.10/24 dev eth0; ip route add default via 198.51.100.1'
x web 'cd /tmp && nohup python3 -m http.server 80 > /tmp/web.log 2>&1 &'
# The internet's DNS, for the lab: the ISP's resolver at 198.51.100.53 knows www.example.com.
x isp 'ip addr add 198.51.100.53/24 dev eth1; dnsmasq --no-resolv --no-hosts --listen-address=198.51.100.53 --bind-interfaces --address=/www.example.com/198.51.100.10 --address=/ads.example.com/198.51.100.66 --local-ttl=300 --log-queries --log-facility=/tmp/dns.log'
# Office machines on the trunk: each tags its own frames, like a switch port in that VLAN.
x staff 'ip link add link eth0 name eth0.10 type vlan id 10 && ip link set eth0.10 up && ip addr add 10.10.10.20/24 dev eth0.10 && ip route add default via 10.10.10.1'
x guest 'ip link add link eth0 name eth0.20 type vlan id 20 && ip link set eth0.20 up && ip addr add 10.10.20.20/24 dev eth0.20 && ip route add default via 10.10.20.1'
x server 'ip link add link eth0 name eth0.30 type vlan id 30 && ip link set eth0.30 up && ip addr add 10.10.30.10/24 dev eth0.30 && ip route add default via 10.10.30.1'
x server 'cd /tmp && nohup python3 -m http.server 80 > /tmp/web.log 2>&1 &'- Line 8: Forwarding off on the router, like a fresh Debian install (Docker Desktop turns it on).
- Lines 13–14: A resolver standing in for the internet's DNS. The router uses it from the DHCP and DNS lesson onwards.
- Lines 16–18: Each office host creates its VLAN interface and a fixed address. The next lesson replaces the addresses with DHCP.
# The router: Debian 13 with systemd, udev and D-Bus, so systemd-networkd, nftables and dnsmasq run as on
# real hardware (networkd waits for udev before it configures a link).
FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
systemd systemd-sysv udev dbus nftables dnsmasq openssh-server iproute2 iputils-ping \
tcpdump procps less curl netcat-openbsd conntrack \
&& rm -rf /var/lib/apt/lists/* \
&& systemctl mask systemd-modules-load.service getty@.service autovt@.service getty-static.service \
&& systemctl disable dnsmasq
CMD ["/sbin/init"]# Office PCs, servers and the internet: plain Debian 13 with network tools and a DHCP client.
FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
iproute2 iputils-ping tcpdump curl python3 netcat-openbsd procps dnsutils dhcpcd-base \
&& rm -rf /var/lib/apt/lists/*
CMD ["sleep", "infinity"]# The ISP's router: Debian 13 with Kea (IPv6 prefixes by DHCPv6 prefix delegation) and a
# small DNS resolver for the lab's "internet".
FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
iproute2 iputils-ping tcpdump procps kea-dhcp6-server dnsmasq-base \
&& rm -rf /var/lib/apt/lists/*
CMD ["sleep", "infinity"]Where next
- IPv6 on the Debian routerBuilds on this lesson · Intermediate · 40 min
Add IPv6 to a Debian router with systemd-networkd: ask the ISP for a /56 by DHCPv6 prefix delegation, give each VLAN a /64 and announce it with router advertisements, firewall it with nftables (including the ICMPv6 IPv6 can't work without), and see what happens when the ISP changes the prefix.
- 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.
- DHCP and DNS on the Debian routerBuilds on this lesson · Intermediate · 35 min
Add DHCP and DNS to a Debian router with dnsmasq: a range and lease time per VLAN, a reservation, local names from DHCP, a caching forwarder, blocking a domain, the firewall rules it needs, and reading its log when a VLAN gets no address.
- Two Debian routers: VRRP and connection syncBuilds on this lesson · Intermediate · 45 min
Pair two Debian 13 routers so the office survives one failing: keepalived VRRP for the gateway and public addresses, conntrackd so open connections survive, NAT to a shared address, health checks, and split brain, all measured in a lab.