Build the lab yourself
The lab runs on any machine with Docker, including an Apple Silicon Mac, in well under a gigabyte. edge is Debian 13 with systemd, so the services behave as on a real server; the two ISPs and the core router are FRR 10.2.1 containers, and the web server and office server are plain Debian.
- Put the files from this page in one directory, with the edge router's files from earlier in the lesson:
edge-frr.conf,nftables.conf, the four.networkfiles and99-forwarding.conf. Save FRR's containerdaemonsfile asfrr-daemons. docker compose -f lab-compose.yaml up -d --build, then./lab-setup.sh. It addresses the hosts and, like a fresh Debian install, leaves forwarding off onedge.- Copy the edge files in:
docker compose -f lab-compose.yaml cp 10-isp1.network edge:/etc/systemd/network/(and the other three),99-forwarding.confto/etc/sysctl.d/, andedge-frr.confandnftables.confto/root/. - Open a shell on
edge(docker compose -f lab-compose.yaml exec edge bash) and follow the lesson: networkd, forwarding,daemons,frr.conf, then the firewall.
The ISPs' configurations are deliberately loose (no bgp ebgp-requires-policy) so they accept whatever edge sends; edge is the router that has to be careful. Docker Desktop's kernel has no TCP-MD5, so the lab can't show BGP session passwords; on real hardware, add neighbor <ip> password … on both ends.
# FRR on Debian: the lab. docker compose -f lab-compose.yaml up -d --build && ./lab-setup.sh
name: frrdeb-lab
x-frr: &frr
image: quay.io/frrouting/frr:10.2.1
privileged: true
volumes: ["./frr-daemons:/etc/frr/daemons:ro"]
x-host: &host
build: { context: ., dockerfile: Dockerfile.host }
image: ttr-frrdeb-host:13
privileged: true
services:
edge:
build: { context: ., dockerfile: Dockerfile.edge }
image: ttr-frrdeb-edge:13
hostname: edge
privileged: true
cgroup: host # let systemd run as PID 1
volumes: ["/sys/fs/cgroup:/sys/fs/cgroup:rw"]
tmpfs: ["/run", "/run/lock"]
networks: { up1: { interface_name: eth0, priority: 100 }, up2: { interface_name: eth1, priority: 90 }, inside: { interface_name: eth2 } }
isp1: { <<: *frr, hostname: isp1, volumes: ["./frr-daemons:/etc/frr/daemons:ro", "./isp1.conf:/etc/frr/frr.conf"], networks: { up1: { interface_name: eth0, priority: 100 }, net: { interface_name: eth1 } } }
isp2: { <<: *frr, hostname: isp2, volumes: ["./frr-daemons:/etc/frr/daemons:ro", "./isp2.conf:/etc/frr/frr.conf"], networks: { up2: { interface_name: eth0, priority: 100 }, net: { interface_name: eth1 } } }
core: { <<: *frr, hostname: core, volumes: ["./frr-daemons:/etc/frr/daemons:ro", "./core.conf:/etc/frr/frr.conf"], networks: { inside: { interface_name: eth0, priority: 100 }, servers: { interface_name: eth1 } } }
web: { <<: *host, hostname: web, networks: { net: { interface_name: eth0 } } }
server: { <<: *host, hostname: server, networks: { servers: { interface_name: eth0 } } }
# Layer-2-only networks: every address comes from the routers' own configuration.
networks:
up1: { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:d1::/64" }] } }
up2: { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:d2::/64" }] } }
inside: { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:d3::/64" }] } }
net: { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:d4::/64" }] } }
servers: { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:d5::/64" }] } }- Lines 3–6: The ISPs and the core router run FRR 10.2.1 from FRR's own image.
- Lines 12–20: edge runs systemd as PID 1, so its services behave as on a real server.
#!/bin/sh
# Address the hosts and turn IPv6 off everywhere. Run after `docker compose up`.
# The routers' addresses come from their own configuration (FRR, or systemd-networkd on edge).
x() { docker compose -f lab-compose.yaml exec -T "$1" sh -c "$2"; }
for n in edge isp1 isp2 core web server; do x $n 'sysctl -qw net.ipv6.conf.all.disable_ipv6=1'; done
# The FRR image ships without vtysh.conf, and vtysh complains on every command.
for n in isp1 isp2 core; do x $n 'touch /etc/frr/vtysh.conf'; done
# Docker Desktop starts every container with forwarding on; a fresh Debian server has it off.
x edge 'sysctl -qw net.ipv4.ip_forward=0'
x web 'ip addr add 198.51.100.80/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 &'
x server 'ip addr add 203.0.113.10/26 dev eth0; ip route add default via 203.0.113.1'# The edge router: Debian 13 with systemd, FRR from Debian's own packages, nftables.
FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
systemd systemd-sysv udev frr nftables iproute2 iputils-ping traceroute tcpdump procps less \
&& rm -rf /var/lib/apt/lists/* \
&& systemctl mask systemd-modules-load.service
CMD ["/sbin/init"]- Line 4: udev is there for networkd, as on any real Debian install.
# The web server and the office server: plain Debian 13 with network tools.
FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
iproute2 iputils-ping traceroute curl python3 procps \
&& rm -rf /var/lib/apt/lists/*
CMD ["sleep", "infinity"]frr defaults traditional
hostname isp1
!
ip route 100.64.0.0/10 blackhole
ip route 203.0.113.128/25 blackhole
!
interface eth0
ip address 192.0.2.0/31
!
interface eth1
ip address 198.51.100.1/24
!
router bgp 64501
bgp router-id 198.51.100.1
no bgp ebgp-requires-policy
neighbor 192.0.2.1 remote-as 64500
neighbor 192.0.2.1 description office (edge)
neighbor 198.51.100.2 remote-as 64502
!
address-family ipv4 unicast
network 198.51.100.0/24
! Two routes the office must refuse: shared address space, and a piece of its own prefix.
network 100.64.0.0/10
network 203.0.113.128/25
neighbor 192.0.2.1 default-originate
exit-address-family
!- Lines 4–5: Two routes ISP 1 shouldn't announce, so edge has something to filter.
- Line 15: The ISPs accept anything: edge must be the careful one.
- Lines 22–25: The leaks, and a default route for the office.
frr defaults traditional
hostname isp2
!
interface eth0
ip address 192.0.2.2/31
!
interface eth1
ip address 198.51.100.2/24
!
router bgp 64502
bgp router-id 198.51.100.2
no bgp ebgp-requires-policy
neighbor 192.0.2.3 remote-as 64500
neighbor 192.0.2.3 description office (edge)
neighbor 198.51.100.1 remote-as 64501
!
address-family ipv4 unicast
network 198.51.100.0/24
neighbor 192.0.2.3 default-originate
exit-address-family
!frr defaults traditional
hostname core
!
interface eth0
ip address 10.0.0.2/30
ip ospf area 0
ip ospf network point-to-point
!
interface eth1
ip address 203.0.113.1/26
ip ospf area 0
ip ospf passive
!
interface lo
ip address 10.255.0.2/32
ip ospf area 0
!
router ospf
ospf router-id 10.255.0.2
!bgpd=yes
ospfd=yes
ospf6d=no
ripd=no
ripngd=no
isisd=no
pimd=no
ldpd=no
nhrpd=no
eigrpd=no
babeld=no
sharpd=no
pbrd=no
bfdd=no
fabricd=no
vrrpd=no
pathd=no
staticd=yes
vtysh_enable=yes
zebra_options=" -A 127.0.0.1 -s 90000000"
bgpd_options=" -A 127.0.0.1"
staticd_options="-A 127.0.0.1"
bfdd_options=" -A 127.0.0.1"Where next
- BGP FundamentalsPrerequisite · Intermediate · 75 min
How the Border Gateway Protocol glues the internet together: autonomous systems, sessions, path attributes, best-path selection, iBGP scaling and a hands-on eBGP lab with real FRR configuration.
- 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.
- OSPF FundamentalsPrerequisite · Beginner · 70 min
The link-state IGP that runs inside most networks: neighbour adjacencies, LSA flooding, SPF, costs, DR/BDR, areas and LSA types, a hands-on FRR lab, and how OSPF underpins BGP.
- Edge and core: building out a VyOS networkAlso covers BGP, OSPF · Intermediate · 60 min
Split one BGP router into edge routers that face the outside world and a core that carries traffic inside: loopbacks, OSPF for infrastructure, iBGP between loopbacks, next-hop-self, and what happens when a link fails. Built and broken on three real VyOS routers.