TTR Networks

Intermediate · FRR on Debian: a BGP and OSPF router · section 10 of 10

Build the lab yourself

✓ Lab-tested 4 November 2026 on Debian 13.7, FRR 10.3 (Debian package), FRR 10.2.1 (upstreams), nftables 1.1.3, systemd 257

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.

  1. 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 .network files and 99-forwarding.conf. Save FRR's container daemons file as frr-daemons.
  2. 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 on edge.
  3. 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.conf to /etc/sysctl.d/, and edge-frr.conf and nftables.conf to /root/.
  4. 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.

lab-compose.yaml
# 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.
lab-setup.sh
#!/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'
Dockerfile.edge
# 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.
Dockerfile.host
# 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"]
isp1.conf (FRR, ISP 1)
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.
isp2.conf (FRR, ISP 2)
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
!
core.conf (FRR, the office core)
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
!
frr-daemons (the FRR containers' /etc/frr/daemons)
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.