Skip to content
daniel@cosenza:~/blog
FreeBSDHow-To July 7, 2026 2 min read

How to Configure FreeBSD as a Router with pf NAT

A complete walkthrough turning a FreeBSD box with two network interfaces into a working NAT router/firewall — gateway forwarding, NAT rules, and a sane default-deny ruleset.

FreeBSD’s combination of a solid network stack and pf makes it a genuinely capable router platform — this walks through the complete setup from two network interfaces to a working NAT gateway.

Step 1: identify your two interfaces

ifconfig -l

You need one interface facing your upstream/internet connection (em0, say) and one facing your internal network (em1).

Step 2: enable IP forwarding

sysrc gateway_enable=YES

Without this, the kernel simply won’t forward packets between interfaces regardless of firewall configuration — it’s the single most common thing missed when “the NAT rules look right but nothing routes.”

Step 3: configure the internal interface with a static address

sysrc ifconfig_em1="inet 192.168.1.1/24"

Step 4: write the pf configuration

# /etc/pf.conf
ext_if = "em0"
int_if = "em1"

set skip on lo

nat on $ext_if from $int_if:network to any -> ($ext_if)

block in all
pass out all keep state
pass in on $int_if all
pass in on $ext_if proto tcp to ($ext_if) port { 22 }

The nat line is what actually performs address translation, rewriting internal clients’ source addresses to the router’s own external address; the block in all establishes a default-deny baseline, with explicit pass rules opening only what’s actually needed.

Step 5: enable pf and load the ruleset

sysrc pf_enable=YES
service pf start

Step 6: verify NAT is actually working

pfctl -s nat
pfctl -ss

pfctl -s nat confirms the NAT rule loaded correctly; pfctl -ss (state table) shows active translated connections once internal clients start generating traffic.

Step 7: set up DHCP for the internal network, if needed

pkg install isc-dhcp44-server
sysrc dhcpd_enable=YES
sysrc dhcpd_ifaces="em1"

Step 8: test from an internal client

Confirm a client on the internal network can reach the internet, and check pfctl -ss on the router to see the corresponding translated state entries.

Step 9: harden the ruleset further as needed

pass in on $ext_if proto tcp to ($ext_if) port { 22 } \
    from <trusted_ips>

Restricting management access (SSH) to specific trusted source addresses, rather than leaving it open to the whole internet, is a sensible next step once basic routing is confirmed working.

Why gateway_enable is the step people forget

A completely correct pf ruleset accomplishes nothing if the kernel itself isn’t configured to forward packets between interfaces — this is a kernel-level setting entirely separate from the firewall’s own rules, which is exactly why “my NAT rule looks right but traffic still doesn’t route” so often traces back to this one missing sysrc line rather than anything wrong in /etc/pf.conf itself.