Homelab — FreeBSD pf Router
A custom edge router running FreeBSD, pf, unbound, and dhcpd
What This Is
At the edge of my homelab is a small, fanless FreeBSD box 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.
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
- Chassis: Fanless 4-port mini PC (Intel N5105/N100 class)
- NICs: 4× Intel i225/i226 2.5GbE (the
igc(4)driver) - Memory: 16 GB DDR4 SODIMM
- Storage: 256 GB NVMe with ZFS root and a boot environment
- Power: ~10 W idle, no moving parts
Intel NICs matter. Realtek and other budget NICs technically work, but they regularly cost more in debugging time than the BOM saved.
Network Topology
┌─────────────────────┐
│ Internet │
└──────────┬──────────┘
│ igc0 (WAN)
┌──────────┴──────────┐
│ FreeBSD Router │
│ pf · unbound │
│ dhcpd · ntpd │
└──┬─────┬─────┬──────┘
igc1 │ │ │ igc3
(LAN trust)│igc2 │ │ (IoT / quarantine)
│ (servers)│
┌──────────┴───┐ ┌────┴────────┐ ┌──────────┐
│ Workstations │ │ Homelab │ │ IoT VLAN │
│ Phones / TV │ │ Proxmox/K8s │ │ Cameras │
└──────────────┘ └─────────────┘ └──────────┘
Four physical interfaces, four broadcast domains. No VLAN trunking required for the common case — physical separation is the simplest defense against misconfiguration. IoT and trusted LAN traffic never share a wire.
Software Stack
- FreeBSD 14.x — base OS, ZFS root, GENERIC kernel
- pf — stateful firewall, NAT, anti-spoofing, queueing
- unbound — local recursive DNS, DNSSEC validation, blocklists
- dhcpd — leases per LAN, static reservations for servers
- ntpd — local NTP for the network
- WireGuard — road-warrior VPN back into the LAN
- iocage / bastille — jails for service isolation
Everything is in base or in pkg. No vendor binaries, no upstream-only
repos, no kernel modules I didn't build from source.
The Configuration, in Spirit
# /etc/rc.conf (excerpt)
hostname="gw.lan"
zfs_enable="YES"
# Enable forwarding for both IP versions
gateway_enable="YES"
ipv6_gateway_enable="YES"
# Interfaces
ifconfig_igc0="DHCP"
ifconfig_igc1="inet 10.10.10.1/24"
ifconfig_igc2="inet 10.10.20.1/24"
ifconfig_igc3="inet 10.10.30.1/24"
# Services
pf_enable="YES"
pflog_enable="YES"
local_unbound_enable="YES"
dhcpd_enable="YES"
ntpd_enable="YES"
sshd_enable="YES"
That's the core. 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
- git-tracked config.
/etc,/usr/local/etc, and the jail manifests live in a private git repo. Every change is a commit with a message. - ZFS boot environments. Before any meaningful change I clone
the boot environment with
bectl create. If something breaks, I reboot to the previous BE in seconds. - pfctl -nf before pfctl -f. Always validate the ruleset before loading it. Never edit pf.conf in place over SSH without a safety net.
- Console access. Serial console wired up. The day pf locks me out is the day I'm grateful for it.
- ZFS snapshots + offsite send. Hourly snapshots, daily replication to a remote ZFS host. Belt and suspenders.
Articles in This Series
- Building a FreeBSD pf Router — hardware, install, and first-boot configuration
- pf.conf: Writing Rules That Survive a Power Outage — rule design, NAT, anti-spoofing, and a real config
- FreeBSD Jails for Network Services — using VNET jails to isolate DNS, monitoring, and VPN
- IPv6 for Home Networks: A FreeBSD Walkthrough — DHCPv6-PD, rtadvd, and v6-aware pf rules
- IPv6 Prefix Delegation: A Troubleshooting Cookbook — debugging dhcp6c, RAs, RDNSS, and PMTU
- WireGuard on FreeBSD: A 30-Minute Setup — server, clients, pf, and DNS through the tunnel
- ZFS Send/Recv: Replicating Your Homelab — snapshots, incremental streams, and a real pull-based cron script
- FreeBSD vs Linux: An SRE's Take — when to reach for each, and why I run both
Related Reading
- FreeBSD Handbook — the canonical reference, written like an actual book
- pf.conf(5) — the manual page that does most of the teaching
- jail(8) — the original lightweight container, still going strong
Have questions about the build, or notice something I'd benefit from changing? Drop me a line.