Build the lab, and make it permanent
Build the lab with Docker: three containers joined by two layer-2-only networks, so every address in the lesson is set by the commands below and nothing is assigned automatically.
Remember that everything set with ip and sysctl -w disappears on reboot. To make the client's configuration permanent on Ubuntu, describe it in netplan. Netplan doesn't configure the network itself: it generates configuration for systemd-networkd (or NetworkManager), which you can inspect after netplan generate.
Use netplan try rather than netplan apply on a remote machine: it rolls back automatically unless you confirm, so a typo can't lock you out.
# docker compose up -d --build, then: docker compose exec client bash
# Run the commands from lab-setup.sh in each container.
name: linux-lab
x-host: &host
image: ttr-linux-lab
privileged: true
services:
client:
<<: *host
build:
dockerfile_inline: |
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
iproute2 iputils-ping iputils-tracepath traceroute dnsutils curl \
tcpdump netcat-openbsd nftables dnsmasq python3 procps ethtool
CMD ["sleep", "infinity"]
hostname: client
networks: { office: { interface_name: eth0 } }
router:
<<: *host
hostname: router
networks: { office: { interface_name: eth0 }, dmz: { interface_name: eth1 } }
server:
<<: *host
hostname: server
networks: { dmz: { interface_name: eth0 } }
# Layer-2-only networks: Docker assigns no IPv4, so every address comes from lab-setup.sh.
networks:
office: { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:a::/64" }] } }
dmz: { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:b::/64" }] } }- Line 6: Privileged, because the lab changes kernel settings with
sysctl -w(forwarding), and Docker only makes /proc/sys writable for privileged containers. Fine for a throwaway lab; never do this for real services. - Lines 10–16: The toolbox image: iproute2, ping, tracepath, traceroute, dig, curl, tcpdump, nc, nftables, dnsmasq and python3.
- Line 18:
interface_namepins names, so eth0/eth1 always match the commands in the lesson. - Lines 27–30: Docker insists on one address family per network, so it gets IPv6 only; IPv4 is left entirely to you. In the lesson's lab, IPv6 was also disabled inside the containers to keep the output simple.
# router: two interfaces, forwarding on, and a tiny DNS server for lab.example
ip addr add 192.0.2.1/24 dev eth0
ip addr add 198.51.100.1/24 dev eth1
sysctl -w net.ipv4.ip_forward=1
dnsmasq --no-resolv --no-hosts --bind-interfaces --listen-address=192.0.2.1 \
--local=/lab.example/ --address=/server.lab.example/198.51.100.20
# server: address, default route, a web server on :80, and port 8443 silently dropped
ip addr add 198.51.100.20/24 dev eth0
ip route add default via 198.51.100.1
python3 -m http.server 80 &
nft add table inet filter
nft add chain inet filter input '{ type filter hook input priority 0; }'
nft add rule inet filter input tcp dport 8443 drop
# client: address, default route and resolver
ip addr add 192.0.2.10/24 dev eth0
ip route add default via 192.0.2.1
echo "nameserver 192.0.2.1" > /etc/resolv.conf- Lines 2–4: A router is just a host with two connected subnets and forwarding enabled.
- Lines 5–6: dnsmasq answers for lab.example and returns NXDOMAIN for other names in it.
- Line 10: The server's route back. Delete it to reproduce fault 3.
- Lines 12–14: An nftables rule that drops port 8443, so connections time out rather than being refused.
- Lines 17–19: The client: address, default gateway and resolver. These three lines are what DHCP normally does for you.
# /etc/netplan/01-eth0.yaml — the client's settings, made permanent (Ubuntu).
# Apply with: netplan try (rolls back automatically if you lose access)
network:
version: 2
ethernets:
eth0:
addresses:
- 192.0.2.10/24
routes:
- to: default
via: 192.0.2.1
nameservers:
addresses:
- 192.0.2.1- Lines 7–8: Static address with prefix, the equivalent of
ip addr add. - Lines 9–11: The default route. (
gateway4:is deprecated; userouteswithto: default.) - Lines 12–14: DNS servers, which end up in systemd-resolved rather than written directly to /etc/resolv.conf.
root@ubuntu:~# netplan generate
root@ubuntu:~# cat /run/systemd/network/10-netplan-eth0.network
[Match]
Name=eth0
[Network]
LinkLocalAddressing=ipv6
Address=192.0.2.10/24
DNS=192.0.2.1
[Route]
Destination=0.0.0.0/0
Gateway=192.0.2.1- Line 1: Renders the YAML into backend configuration. Silent on success.
- Lines 3–13: A systemd-networkd unit. This, not the YAML, is what actually configures the interface at boot.
Where next
- Packets That Never Come BackBuilds on this lesson · Intermediate · 75 min
Advanced packet capture with tcpdump, and how to find where packets die on a VyOS network: missing NAT and return routes, asymmetric routing through a stateful firewall, and MTU black holes, all diagnosed on a real VyOS router.
- Setting up VyOS with NATBuilds on this lesson · Beginner · 45 min
A VyOS router with a NAT interface for a private LAN and a public interface for a routed public subnet: plain routing, source NAT, port forwarding and the forward firewall that NAT isn't. Every output comes from a real VyOS lab.
- Office gateway: DHCP, DNS and VRRP on VyOSAlso covers DNS · Beginner · 40 min
Two VyOS routers serving one office LAN: VRRP for a gateway that survives a router failure, DHCP with split pools and a printer reservation, and DNS forwarding. Then a real failover, the DNS outage it exposed, and the fix.
- Debugging BGP: every session state, reproducedAlso covers troubleshooting · Intermediate · 60 min
Break a working BGP lab a dozen ways and learn to read each failure: Idle, Connect, Active and OpenSent, max-prefix, firewalls, wrong AS, TTL, hold timers, missing policy and unreachable next hops, all with real FRR output and packet captures.