Build the lab yourself
This lesson continues on the lab from Debian as an office router (and DHCP and DNS on the Debian router, whose firewall rules it builds on). Build those first.
- Run
./lab-setup-ipv6.sh. It turns IPv6 back on in the other containers, removes the addresses and default routes Docker adds to them, addresses the ISP and the web server, and starts Kea on the ISP with the configuration and hook script below. - On the router, replace
10-wan.networkand the three VLAN.networkfiles,99-router.confandnftables.confwith this lesson's versions. Apply them withsysctl --system,networkctl reloadandsystemctl reload nftables, thennetworkctl reconfigure eth0to ask for the prefix. - On the office hosts there's nothing to do: they configure themselves from the router's advertisements.
Three things the lab needed that a real ISP takes care of:
- A route back. A real ISP routes your delegated prefix to you automatically. Kea doesn't install routes, so the lab uses Kea's
run_scripthook to call a small script that adds one for each delegation. - Hook paths. Kea 2.6.3 only loads hook libraries from its own directory, so the configuration names the library without a path.
- Timing. Kea has to start after the ISP's link-local address is ready. Started too early, it runs without listening anywhere, and only a warning in its log says so; the setup script waits three seconds.
To repeat the renumbering test, start Kea with a copy of its configuration that uses a different pool (the lab used 2001:db8:cd00::/48), delete its lease file, and reconfigure the router's WAN.
#!/bin/sh
# The IPv6 lesson: turn IPv6 back on everywhere except where the router's own config does it,
# remove the addresses and routes Docker adds, and start the ISP's DHCPv6 server.
dc() { docker compose -f lab-compose.yaml "$@"; }
x() { dc exec -T "$1" sh -c "$2"; }
on='for f in /proc/sys/net/ipv6/conf/*/disable_ipv6; do echo 0 > $f; done; ip -6 addr flush scope global; ip -6 route flush default 2>/dev/null; true'
for n in isp web staff guest server; do x $n "$on"; done
# The ISP: 2001:db8:f::1 towards customers, 2001:db8:e::1 towards the "internet", and Kea.
x isp 'ip -6 addr add 2001:db8:f::1/64 dev eth0; ip -6 addr add 2001:db8:e::1/64 dev eth1; sysctl -qw net.ipv6.conf.all.forwarding=1; mkdir -p /var/lib/kea /run/kea /var/log/kea'
dc cp isp/kea-dhcp6.conf isp:/etc/kea/kea-dhcp6.conf >/dev/null
dc cp isp/pd-route.sh isp:/etc/kea/pd-route.sh >/dev/null
sleep 3 # let the new link-local addresses finish duplicate address detection first
x isp 'nohup kea-dhcp6 -c /etc/kea/kea-dhcp6.conf > /dev/null 2>&1 &'
# The web server, now on both IPv4 and IPv6.
x web 'ip -6 addr add 2001:db8:e::10/64 dev eth0; ip -6 route add default via 2001:db8:e::1; pkill python3; cd /tmp && nohup python3 -m http.server 80 --bind :: > /tmp/web.log 2>&1 &'- Lines 6–7: IPv6 back on, and Docker's own addresses and default routes removed.
- Lines 12–13: Kea starts only after the link-local addresses are ready.
// The ISP's DHCPv6 server (Kea): delegates a /56 to each customer router.
{
"Dhcp6": {
"interfaces-config": { "interfaces": [ "eth0" ] },
"lease-database": { "type": "memfile", "persist": true, "name": "/var/lib/kea/kea-leases6.csv" },
"renew-timer": 900, "rebind-timer": 1800, "preferred-lifetime": 3600, "valid-lifetime": 7200,
"subnet6": [ {
"id": 1, "subnet": "2001:db8:f::/64", "interface": "eth0",
"pd-pools": [ { "prefix": "2001:db8:ab00::", "prefix-len": 48, "delegated-len": 56 } ]
} ],
// A real ISP's routers learn the route to each delegated prefix; in the lab, a script adds it.
"hooks-libraries": [ {
"library": "libdhcp_run_script.so",
"parameters": { "name": "/etc/kea/pd-route.sh", "sync": false }
} ],
"loggers": [ { "name": "kea-dhcp6", "output-options": [ { "output": "/var/log/kea/kea-dhcp6.log" } ], "severity": "INFO" } ]
}
}- Line 9: Delegate a /56 from 2001:db8:ab00::/48 to each customer.
- Lines 12–15: The run_script hook, named without a path, calls the route script for every lease event.
#!/bin/sh
# Kea's run_script hook: route each delegated prefix to the router that asked for it.
[ "$1" = "leases6_committed" ] || exit 0
i=0
while [ "$i" -lt "${LEASES6_SIZE:-0}" ]; do
eval type=\$LEASES6_AT${i}_TYPE prefix=\$LEASES6_AT${i}_ADDRESS len=\$LEASES6_AT${i}_PREFIX_LEN
[ "$type" = "IA_PD" ] && ip -6 route replace "$prefix/$len" via "$QUERY6_REMOTE_ADDR" dev eth0
i=$((i + 1))
done- Line 3: Only when leases are committed.
- Line 7: Route each delegated prefix to the address the request came from.
Where next
- Debian as an office router: interfaces, VLANs and a firewallPrerequisite · Intermediate · 45 min
Turn a fresh Debian 13 install into an office router: systemd-networkd for the WAN and three VLANs on one trunk, IP forwarding, an nftables policy between VLANs with NAT, protecting the router itself, and a configuration that survives a reboot.
- VyOS firewall in depthAlso covers IPv6, nftables · Intermediate · 45 min
Build a complete VyOS firewall step by step: the input, forward and output hooks, a global state policy, groups, custom chains, protecting the router with rate-limited SSH, egress filtering, rule order, the ICMPv6 that IPv6 can't live without, and the nftables underneath.
- IPv6 on VyOS: setup and routingAlso covers IPv6, SLAAC · Beginner · 55 min
Number a site from an ISP's /48 on VyOS: WAN and blackhole routes, SLAAC with Router Advertisements, handing a /56 to a downstream router statically and with DHCPv6 prefix delegation, and the forward firewall IPv6 needs because there's no NAT. Every output comes from a real two-router VyOS lab.
- nftables and iptables on Debian: host and router firewallsAlso covers debian, nftables · 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.