How to Configure FreeBSD as a Router with pf NAT
A console-safe FreeBSD IPv4 router and PF NAT workflow covering interface plans, forwarding, default-deny rules, management access, validation, and recovery.
FreeBSD and PF can provide routing, stateful filtering, and IPv4 network address translation. Turning on forwarding and adding one NAT rule is not a complete firewall design. Interface identity, upstream addressing, LAN services, management access, IPv6, state behavior, logging, recovery, and client tests all need explicit decisions.
Perform the first ruleset load from a physical or out-of-band console. A syntactically valid rule can still block SSH or route replies through the wrong link.
Map interfaces and preserve the current path
Inventory addresses, routes, listeners, and active PF state:
freebsd-version -kru
ifconfig -a
netstat -rn
sockstat -4 -6 -l
service pf status
pfctl -sr 2>/dev/null
Identify interfaces by verified MAC address and cabling, not by copying em0 and em1. In this example, em0 is upstream and em1 is LAN. The LAN will be 192.168.50.0/24, with the router at 192.168.50.1. Confirm that this subnet does not overlap the upstream network, a VPN, or another routed site.
Document how current administration reaches the host and which rule must preserve it. Keep a known-good /etc/pf.conf, console access, and a tested rollback command available before changing networking.
Configure LAN addressing and forwarding
Persist the internal address using FreeBSD’s rc configuration:
sysrc ifconfig_em1="inet 192.168.50.1 netmask 255.255.255.0"
sysrc gateway_enable=YES
Replace the interface and subnet. gateway_enable persists IPv4 forwarding; to enable it immediately after the surrounding network is ready:
sysctl net.inet.ip.forwarding=1
Do not restart all interfaces over the only SSH session. Apply the LAN interface from console or during a controlled window, then verify:
ifconfig em1
netstat -rn -f inet
sysctl net.inet.ip.forwarding
Configure the external interface according to the provider: DHCP, PPP, or a static allocation. NAT can track a changing external address with PF’s parenthesized interface syntax, but routing, DNS, and inbound management still need to survive renewal.
Build a minimal reviewed PF ruleset
Start with explicit macros and a default block policy in /etc/pf.conf:
ext_if = "em0"
int_if = "em1"
lan_net = "192.168.50.0/24"
table <management> const { 203.0.113.10 }
set skip on lo0
nat on $ext_if inet from $lan_net to any -> ($ext_if)
block all
pass out on $ext_if inet from ($ext_if) to any keep state
pass in on $int_if inet from $lan_net to ! ($int_if) keep state
pass in on $ext_if inet proto tcp from <management> \
to ($ext_if) port 22 flags S/SA keep state
Replace all examples. The correct loopback interface is lo0, not lo. The NAT rule applies only to IPv4 traffic from the declared LAN and rewrites it to the current external address. The LAN pass rule permits forwarding but excludes traffic addressed to the router’s LAN address; add separate, narrow rules for DNS, DHCP, NTP, or administration if the router provides those services.
The external SSH rule is optional. Do not add it unless remote management is required and 203.0.113.10 has been replaced by a trustworthy, stable source. Prefer a management VPN or dedicated interface. Verify that another existing rule does not broaden port 22 afterward, because PF normally uses the last matching filter rule unless quick changes evaluation.
This is an IPv4 baseline, not an IPv6 firewall. NAT44 does not protect globally routed IPv6 clients. If the upstream or LAN uses IPv6, design forwarding, router advertisements, ICMPv6, address policy, and PF rules explicitly. Do not advertise IPv6 and then ignore its filter path.
Validate syntax before enabling PF
Parse the complete file without loading it:
pfctl -nf /etc/pf.conf
Warnings and errors must be resolved. Then enable boot persistence and load from console:
sysrc pf_enable=YES
sysrc pf_rules="/etc/pf.conf"
service pf start
If PF is already running, use pfctl -f /etc/pf.conf only after the syntax check and rollback preparation. Starting or reloading flushes or changes policy in ways that can sever active access. Do not treat an open existing SSH state as proof that a new connection will work.
Inspect loaded translation, filtering, and state:
pfctl -sn
pfctl -sr
pfctl -ss
pfctl -si
Use pfctl -vvsr when rule counters are needed. Logging every blocked packet on an Internet-facing interface can overwhelm storage; log selected diagnostic or policy events and configure pflog0 retention intentionally.
Provide client configuration separately
LAN clients need an address in 192.168.50.0/24, default gateway 192.168.50.1, and reachable DNS servers. Configure one client statically for initial testing before deploying DHCP.
The old instruction installed ISC DHCP 4.4 without noting that upstream ISC DHCP is end-of-life. DHCP is a separate security-sensitive service. Choose a maintained implementation available for the deployed FreeBSD release, bind it only to the LAN, validate its configuration, and authorize UDP 67/68 deliberately. Do not let a second DHCP server appear on an existing LAN.
If the router provides a local resolver, allow only the required TCP and UDP DNS traffic from the LAN to the router and prevent public recursion. If clients use external resolvers, their traffic follows the forwarding rule and NAT state.
Test layer by layer from an internal client
Check the client address and route, then test the router’s LAN address, an external IP, DNS, and an application protocol. ICMP alone is insufficient because some networks filter it:
ping -c 3 192.168.50.1
traceroute -n 1.1.1.1
drill www.freebsd.org
fetch -o /dev/null https://www.freebsd.org/
While generating traffic, observe the router:
tcpdump -ni em1 host 192.168.50.10
tcpdump -ni em0 host 1.1.1.1
pfctl -ss
Replace the client address. Seeing packets enter but not leave narrows the issue to forwarding, PF, NAT, or routing. Seeing outbound traffic without replies points toward upstream routing, provider filtering, or an incorrect source. Replies arriving without reaching the client point toward state, LAN routing, or client firewalling.
Test negative policy too: an untrusted external host must not reach LAN services or SSH. Verify management from the approved path with a new connection, not the session used during configuration.
Test reboot, failure, and recovery
Reboot during a maintenance window and confirm interface naming, upstream acquisition, LAN addressing, forwarding, PF, DNS/DHCP services, and monitoring start in the correct order. Exercise upstream renewal or link loss and verify new NAT sessions recover. Existing translated sessions commonly reset when the public address changes.
Back up /etc/rc.conf, /etc/pf.conf, service configuration, and the network diagram through a protected channel. Alert on PF disabled, forwarding disabled, no default route, address changes, state-table pressure, packet errors, DHCP exhaustion, resolver failures, and loss of an external probe.
A router is ready when permitted client traffic works, forbidden traffic fails, management remains recoverable, state survives expected conditions, and a reboot produces the same reviewed policy.
Related:
- Configuring pf on FreeBSD: A Practical Guide to Packet Filtering
- How to Configure Static Networking on FreeBSD from Scratch
Sources: