TTR Networks

Intermediate · VyOS firewall in depth · section 8 of 8

Build the lab yourself

✓ Lab-tested 21 October 2026 on VyOS 2026.10.16-0024-rolling, Debian 12 hosts

The lab is one VyOS VM and five small Debian containers. It needs about 2 GB of free memory for Docker.

  1. Get the VyOS image as in the tcpdump lesson (get-vyos.sh), into ./vyos.
  2. Put lab-compose.yaml, start-vyos.sh and hosts-setup.sh in the same directory, then docker compose up -d --build and ./hosts-setup.sh.
  3. Wait a few minutes for VyOS to boot, then attach to its console (the command is at the top of lab-compose.yaml) and log in as vyos / vyos.
  4. Paste the stages from the lesson in order, with configure before and commit after each: the interfaces, the input filter, the forward filter, then IPv6.
  5. Run the tests from each section.

start-vyos.sh caps QEMU's translation cache with tb-size=256. QEMU's default is 1 GB on top of the VM's own memory, and in this lab that was enough for Docker Desktop to kill the VM for running out of memory.

lab-compose.yaml
# Needs ./vyos (from the tcpdump-and-vyos lesson's get-vyos.sh). Then: docker compose up -d --build && ./hosts-setup.sh
# VyOS console: docker compose exec -it r1 socat -,raw,echo=0,escape=0x1d UNIX-CONNECT:/run/console.sock
name: vfw-lab
x-host: &host
  build:
    dockerfile_inline: |
      FROM debian:bookworm-slim
      RUN apt-get update && apt-get install -y --no-install-recommends \
        iproute2 iputils-ping curl tcpdump netcat-openbsd openssh-client python3 procps
      CMD ["sleep", "infinity"]
  image: ttr-vfw-host
  privileged: true
services:
  r1:
    build:
      context: .
      dockerfile_inline: |
        FROM debian:bookworm-slim
        RUN apt-get update && apt-get install -y --no-install-recommends qemu-system-x86 iproute2 socat
        COPY start-vyos.sh /usr/local/bin/start-vyos.sh
        CMD ["/usr/local/bin/start-vyos.sh"]
    cap_add: [NET_ADMIN]
    devices: ["/dev/net/tun:/dev/net/tun"]
    environment: { VYOS_MEM: "1536", VYOS_SMP: "1", VM_ID: "11" }
    volumes: ["./vyos:/vyos:ro"]
    networks: { wan: { interface_name: eth0 }, lan: { interface_name: eth1 }, mgmt: { interface_name: eth2 } }
  isp:      { <<: *host, hostname: isp,      networks: { wan: { interface_name: eth0, priority: 100 }, internet: { interface_name: eth1 } } }
  home:     { <<: *host, hostname: home,     networks: { internet: { interface_name: eth0 } } }
  scanner:  { <<: *host, hostname: scanner,  networks: { internet: { interface_name: eth0 } } }
  lanhost:  { <<: *host, hostname: lanhost,  networks: { lan: { interface_name: eth0 } } }
  admin:    { <<: *host, hostname: admin,    networks: { mgmt: { interface_name: eth0 } } }
# Layer-2-only networks: every address comes from VyOS's config and hosts-setup.sh.
networks:
  wan:      { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:b1::/64" }] } }
  lan:      { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:b2::/64" }] } }
  mgmt:     { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:b3::/64" }] } }
  internet: { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:b4::/64" }] } }
  • Lines 14–26: R1: a VyOS VM in QEMU, with one network per interface.
  • Lines 27–31: Five small Debian hosts.
start-vyos.sh
#!/bin/sh
# Bridge every container interface (eth0, eth1, …) to a tap, then boot VyOS with one virtio NIC per tap.
# VM NIC n gets MAC 52:54:00:<VM_ID>:0n:01, so VyOS's ethN matches the container's ethN. Give each VM its own VM_ID.
set -e
NETDEVS=""
n=0
for ifc in $(ls /sys/class/net | grep -E '^eth[0-9]+$' | sort -V); do
  ip link add "br$n" type bridge
  ip link set "$ifc" master "br$n"
  ip tuntap add "tap$n" mode tap
  ip link set "tap$n" master "br$n"
  ip link set "tap$n" up
  ip link set "br$n" up
  # Pin the VM's MAC to its tap and stop learning on the Docker side: Docker reflects the VM's
  # broadcasts back, which would otherwise teach the bridge the VM's MAC on the wrong port.
  bridge link set dev "$ifc" learning off
  bridge fdb replace "52:54:00:${VM_ID:-00}:0$n:01" dev "tap$n" master static
  NETDEVS="$NETDEVS -netdev tap,id=n$n,ifname=tap$n,script=no,downscript=no -device virtio-net-pci,netdev=n$n,mac=52:54:00:${VM_ID:-00}:0$n:01"
  n=$((n + 1))
done
# VYOS_TB_MB caps QEMU's TCG translation cache (QEMU's default is 1 GB on top of the guest RAM).
exec qemu-system-x86_64 -m "${VYOS_MEM:-2048}" -smp "${VYOS_SMP:-2}" -cpu max -accel tcg,thread=multi,tb-size=${VYOS_TB_MB:-256} \
  -kernel /vyos/vmlinuz -initrd /vyos/initrd.img \
  -append "boot=live components hostname=vyos username=live nopersistence noautologin nonetworking union=overlay console=ttyS0,115200 net.ifnames=0 biosdevname=0" \
  -cdrom /vyos/vyos.iso $NETDEVS \
  -display none -serial unix:/run/console.sock,server=on,wait=off -monitor none
  • Lines 21–22: Caps QEMU's translation cache, so the VM fits beside other containers.
hosts-setup.sh
#!/bin/sh
# Address the Debian hosts. IPv4 plus IPv6 from documentation ranges; Docker's own ULA addresses are removed.
cd "$(dirname "$0")"
x() { docker compose exec -T "$@"; }
for c in isp home scanner lanhost admin; do
  x $c sh -c 'for i in $(ls /sys/class/net | grep ^eth); do ip -6 addr flush dev $i scope global; done'
done
x isp     sh -c 'sysctl -qw net.ipv4.ip_forward=1 net.ipv6.conf.all.forwarding=1
  ip addr add 203.0.113.1/30 dev eth0; ip -6 addr add 2001:db8:ffff::1/64 dev eth0
  ip addr add 192.0.2.1/24 dev eth1;   ip -6 addr add 2001:db8:f000::1/64 dev eth1
  ip route add 198.51.100.0/24 via 203.0.113.2; ip -6 route add 2001:db8:10::/64 via 2001:db8:ffff::2; ip -6 route del default'
x home    sh -c 'ip addr add 192.0.2.10/24 dev eth0; ip route add default via 192.0.2.1
  ip -6 addr add 2001:db8:f000::10/64 dev eth0; ip -6 route replace default via 2001:db8:f000::1'
x scanner sh -c 'ip addr add 192.0.2.66/24 dev eth0; ip route add default via 192.0.2.1'
x lanhost sh -c 'ip addr add 198.51.100.10/24 dev eth0; ip route add default via 198.51.100.1
  ip -6 addr add 2001:db8:10::10/64 dev eth0; ip -6 route replace default via 2001:db8:10::1'
x admin   sh -c 'ip addr add 10.99.0.10/24 dev eth0; ip route add default via 10.99.0.1'
# A web server on the LAN host and on home, each logging who connected.
for c in lanhost home; do x -d $c sh -c "cd /tmp && echo 'hello from $c' > index.html && exec python3 -m http.server --bind :: 80 > /tmp/http.log 2>&1"; done
echo hosts ready

Where next

  • Setting up VyOS with NATPrerequisite · 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.

  • IPv6 on VyOS: setup and routingAlso covers firewall, IPv6 · 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 firewall, 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.

  • Zone-based firewall on VyOSAlso covers firewall · Intermediate · 35 min

    Rebuild the NAT lesson's router with a zone-based firewall: WAN, LAN, a DMZ and the router itself, with one ruleset per direction. Includes the missing-return-traffic mistake, post-NAT matching, and a full test matrix.