Homelab: FreeBSD pf Router

FreeBSD 15 on a CWWK N100, behind an XGS-PON SFP+ module, with the AT&T gateway out of the path

FreeBSD 15 pf XGS-PON Networking Self-Hosted

What This Is

At the edge of my homelab is a small FreeBSD 15 box, hostname homefw, that handles every packet coming into and out of my network. It's the WAN router, the firewall, the DHCP and DNS server, the VPN endpoint, and the boundary that everything else sits behind. The AT&T fiber gateway is gone; an X-ONU-SFPP XGS-PON SFP+ module takes the fiber directly and slots into one of the box's 10G SFP+ ports, so the FreeBSD host gets the public IP itself.

I built it instead of buying a turnkey router for the same reason I run NixOS on servers: I want the configuration to be obvious, version-controlled, and learnable. FreeBSD's pf, rc.conf, and tightly integrated networking stack give me exactly that.

Why Not OPNsense or pfSense?

OPNsense and pfSense are both excellent, and both are FreeBSD-based. But they ship a heavyweight web UI on top of FreeBSD, and the moment you click into "Advanced", you discover the UI doesn't expose what you actually want to configure. You SSH in, edit a hidden file, and now your config is split between the GUI and a shadow config you have to remember.

Plain FreeBSD avoids that split. Every line of configuration is in /etc/rc.conf, /etc/pf.conf, /usr/local/etc/unbound/, readable, greppable, and trivial to put in git. The router is small enough to fit in my head, which is the entire point.

Hardware

Intel NICs matter. Realtek and other budget NICs technically work, but they regularly cost more in debugging time than the BOM saved.

Network Topology

        ┌────────────────────────────────┐
        │   AT&T XGS-PON Fiber            │
        │   (SC/APC, no ISP gateway)      │
        └───────────────┬────────────────┘
                        │
        ┌───────────────▼────────────────┐
        │  X-ONU-SFPP (XGS-PON SFP+ ONT) │
        │  8311 firmware · Azores boot   │
        └───────────────┬────────────────┘
                        │ ix0 (WAN, 10G SFP+)
        ┌───────────────▼────────────────┐
        │   FreeBSD 15 Router (homefw)   │
        │   pf · unbound · ISC dhcpd     │
        └───────────────┬────────────────┘
                        │ ix1 (LAN trunk, 10G SFP+)
        ┌───────────────▼────────────────┐
        │      Sodola Managed Switch     │
        └──┬─────────────────────────┬───┘
           │ untagged                │ VLAN 20 tagged
        ┌──▼──────────┐         ┌────▼─────────┐
        │ UniFi APs   │         │ UniFi APs    │
        │ SSID:       │         │ SSID:        │
        │ UniWorld    │         │ UniWork      │
        │ 10.0.0.0/24 │         │ 10.20.0.0/24 │
        │ (main LAN)  │         │ (ix1.20)     │
        └──┬──────────┘         └──────────────┘
           │
        ┌──▼──────────────────┐
        │ Wired LAN           │
        │ Workstations        │
        │ Servers / homelab   │
        └─────────────────────┘

One trunk down to the Sodola switch, two SSIDs out at the UniFi APs. UniWorld is the main LAN, untagged on ix1. UniWork is VLAN 20, terminated on ix1.20 with its own subnet, so devices on that SSID never share a broadcast domain with the rest of the house and have to traverse pf to talk to anything else.

Software Stack

Everything is in base or in pkg. Running pkgbase means base updates flow through the same package manager as everything else, which keeps the boot-environment story clean.

The Configuration, in Spirit

# /etc/rc.conf (excerpt)
hostname="homefw"
zfs_enable="YES"

# Forwarding for v4 and v6
gateway_enable="YES"
ipv6_gateway_enable="YES"

# WAN: SFP+ port hosting the X-ONU-SFPP XGS-PON ONT
ifconfig_ix0="DHCP"
ifconfig_ix0_ipv6="inet6 accept_rtadv"
background_dhclient_ix0="YES"   # XGS-PON DHCP can be slow at boot

# LAN trunk and VLAN 20 (UniWork)
ifconfig_ix1="inet 10.0.0.1 netmask 255.255.255.0"
vlans_ix1="20"
ifconfig_ix1_20="inet 10.20.0.1/24"

# Services
pf_enable="YES"
pflog_enable="YES"
unbound_enable="YES"
dhcpd_enable="YES"
dhcpd_ifaces="ix1 ix1.20"
sshd_enable="YES"

Two lines worth flagging. background_dhclient_ix0="YES" is the line that fixed a class of boot-time bugs: the XGS-PON ONT sometimes takes a few seconds to settle and hand out a DHCP lease, and without backgrounding the boot would hang waiting for it, dragging ntpd, unbound, and even sshd down with it. The VLAN pair (vlans_ix1 plus ifconfig_ix1_20) is what creates the ix1.20 interface; if that interface doesn't exist before pf loads, anything in pf that references it silently breaks.

Most of the rest is in /etc/pf.conf, see the pf rules deep dive for the full ruleset and the reasoning behind it.

Operational Habits

Articles in This Series

Related Reading

Have questions about the build, or notice something I'd benefit from changing? Drop me a line.