How to Set Up a FreeBSD Router and Gateway
A FreeBSD router can outperform most commercial off-the-shelf routers in throughput, stability, and security. FreeBSD's network stack has been refined for decades, its PF firewall is among the most capable packet filters available, and the operating system runs for years without reboots. If you want full control over your network -- NAT, DHCP, DNS, QoS, VPN, ad blocking, multi-WAN failover -- FreeBSD delivers all of it with no licensing fees and no vendor lock-in.
This guide walks through every step of building a production-quality FreeBSD router and gateway from hardware selection to monitoring. Every configuration file is complete and ready to deploy.
Why FreeBSD as a Router
Three things set FreeBSD apart from Linux-based router distributions:
PF firewall. Originally developed for OpenBSD and ported to FreeBSD, PF provides stateful packet filtering, NAT, traffic shaping (ALTQ), and logging in a single coherent configuration language. One file -- pf.conf -- defines your entire firewall and NAT policy. For a deep dive, see our PF firewall guide.
Network stack maturity. FreeBSD's TCP/IP stack powers Netflix (serving a significant portion of global internet traffic), WhatsApp, and countless ISP edge routers. It handles high packet rates, supports features like CARP for failover, and has rock-solid VLAN and bridging support. If you plan to segment your network, see FreeBSD VLANs.
Stability and uptime. FreeBSD systems routinely achieve multi-year uptimes. The base system is lean, the kernel is modular, and security patches rarely require reboots.
Hardware Considerations
NIC Selection
The single most important hardware decision for a FreeBSD router is the network interface. Intel NICs have the best FreeBSD driver support -- the igb(4) (1 GbE) and ix(4) (10 GbE) drivers are mature, stable, and fully featured. Avoid Realtek consumer NICs (re(4)) for router duty; they work but lack hardware offloads and perform poorly under high packet rates.
Recommended NICs:
- Intel I210/I211 -- Quad-port PCIe, excellent
igb(4)support - Intel I350-T4 -- Server-grade quad-port, ECC and SR-IOV support
- Intel X710 -- 10 GbE SFP+, for high-throughput gateways
Form Factor
A mini-ITX or thin-client system with dual or quad Intel NICs is ideal. Popular choices:
- Protectli Vault (FW4B, FW6D) -- Fanless, 4-6 Intel NICs, AES-NI
- PC Engines APU2 -- Low-power AMD, 3x Intel i211 NICs
- Supermicro A1SRi -- Atom C2758, server-grade, IPMI
Minimum specs for a home/small office router: dual-core x86-64, 4 GB RAM, 32 GB SSD. For traffic shaping above 500 Mbps, aim for a quad-core with AES-NI (needed for VPN throughput).
Network Topology
Here is the basic topology this guide implements:
shell+-----------+ ISP Modem ------->| WAN (igb0)| | | | FreeBSD | | Router | | | | LAN (igb1)|-------> Switch -----> LAN Clients +-----------+ | NAT / PF / DHCP / DNS
- WAN interface (igb0): Receives a public IP via DHCP from the ISP (or static).
- LAN interface (igb1): Serves the internal network on
192.168.1.0/24. - The FreeBSD box performs NAT, runs DHCP, resolves DNS, and applies firewall rules.
Enabling IP Forwarding
IP forwarding is the kernel feature that lets the FreeBSD machine route packets between interfaces. Without it, the system drops packets not destined for itself.
Permanent Configuration
Add these lines to /etc/rc.conf:
sh# /etc/rc.conf -- routing and interface configuration hostname="router.local" # WAN interface -- DHCP from ISP ifconfig_igb0="DHCP" # LAN interface -- static IP, this is the default gateway for LAN clients ifconfig_igb1="inet 192.168.1.1 netmask 255.255.255.0" # Enable packet forwarding gateway_enable="YES" # Enable PF firewall pf_enable="YES" pf_rules="/etc/pf.conf" pflog_enable="YES"
Apply Immediately Without Reboot
shsysctl net.inet.ip.forwarding=1
Verify:
shsysctl net.inet.ip.forwarding # net.inet.ip.forwarding: 1
PF NAT Configuration
PF handles both firewall rules and NAT in a single file. Below is a complete, production-ready /etc/pf.conf for a router. For more on NAT specifically, see our NAT guide.
shell# /etc/pf.conf -- FreeBSD Router Configuration # ============================================= # --- Macros --- wan_if = "igb0" lan_if = "igb1" lan_net = "192.168.1.0/24" # Services accessible on the router itself from LAN tcp_services_lan = "{ ssh }" # Port forwarding: forward WAN port 8080 to internal web server webserver = "192.168.1.50" # --- Tables --- table <bruteforce> persist # --- Options --- set skip on lo0 set block-policy drop set loginterface $wan_if set state-policy if-bound # --- Scrub --- match in all scrub (no-df max-mss 1460) # --- NAT --- # Source NAT: masquerade LAN traffic going out WAN nat on $wan_if from $lan_net to any -> ($wan_if) # Port forwarding: external port 8080 -> internal 192.168.1.50:80 rdr on $wan_if proto tcp from any to ($wan_if) port 8080 -> $webserver port 80 # --- Filter Rules --- # Default: block everything block log all # Allow all outbound from the router itself pass out quick on $wan_if proto { tcp udp icmp } from ($wan_if) to any modulate state pass out quick on $lan_if from any to $lan_net # Allow LAN to router (DNS, DHCP, SSH) pass in on $lan_if proto tcp from $lan_net to ($lan_if) port $tcp_services_lan pass in on $lan_if proto { tcp udp } from $lan_net to ($lan_if) port { domain } pass in on $lan_if proto udp from any to any port { bootpc bootps } # Allow LAN clients outbound (NAT will apply) pass in on $lan_if from $lan_net to any modulate state # Allow port-forwarded traffic pass in on $wan_if proto tcp from any to $webserver port 80 synproxy state # Allow ICMP ping (rate-limited) pass in on $wan_if inet proto icmp icmp-type echoreq max-pkt-rate 10/1 # --- Brute-force protection on SSH (if exposed on WAN) --- # pass in on $wan_if proto tcp from any to ($wan_if) port ssh \ # flags S/SA keep state (max-src-conn 5, max-src-conn-rate 3/30, \ # overload <bruteforce> flush global) # Block brute-force offenders block drop in quick from <bruteforce>
Load the ruleset:
shpfctl -f /etc/pf.conf pfctl -e # enable PF if not already running
Verify NAT state:
shpfctl -s nat pfctl -s rules pfctl -s state
DHCP Server for LAN
Install and configure ISC DHCP to assign addresses to LAN clients. For a more detailed walkthrough, see our DHCP server guide.
shpkg install isc-dhcp44-server
Complete dhcpd.conf
Create /usr/local/etc/dhcpd.conf:
shell# /usr/local/etc/dhcpd.conf -- LAN DHCP Configuration option domain-name "home.local"; option domain-name-servers 192.168.1.1; default-lease-time 3600; max-lease-time 86400; authoritative; log-facility local7; subnet 192.168.1.0 netmask 255.255.255.0 { range 192.168.1.100 192.168.1.200; option routers 192.168.1.1; option subnet-mask 255.255.255.0; option broadcast-address 192.168.1.255; option domain-name-servers 192.168.1.1; option ntp-servers 192.168.1.1; } # Static leases for known hosts host webserver { hardware ethernet aa:bb:cc:dd:ee:01; fixed-address 192.168.1.50; } host nas { hardware ethernet aa:bb:cc:dd:ee:02; fixed-address 192.168.1.10; }
Enable and Start
Add to /etc/rc.conf:
shdhcpd_enable="YES" dhcpd_ifaces="igb1"
Start the service:
shservice isc-dhcpd start
DNS Resolver with Unbound and DNSSEC
Unbound is included in the FreeBSD base system. It provides a caching, validating, recursive DNS resolver -- no additional packages needed.
Complete Unbound Configuration
Edit /var/unbound/unbound.conf:
yamlserver: interface: 192.168.1.1 interface: 127.0.0.1 access-control: 192.168.1.0/24 allow access-control: 127.0.0.0/8 allow access-control: 0.0.0.0/0 refuse port: 53 do-ip4: yes do-ip6: no do-udp: yes do-tcp: yes # Performance tuning num-threads: 2 msg-cache-slabs: 4 rrset-cache-slabs: 4 infra-cache-slabs: 4 key-cache-slabs: 4 msg-cache-size: 64m rrset-cache-size: 128m cache-min-ttl: 300 cache-max-ttl: 86400 prefetch: yes prefetch-key: yes serve-expired: yes # DNSSEC validation auto-trust-anchor-file: "/var/unbound/root.key" val-clean-additional: yes # Privacy hide-identity: yes hide-version: yes harden-glue: yes harden-dnssec-stripped: yes harden-referral-path: yes use-caps-for-id: yes # Logging (reduce for production) verbosity: 1 log-queries: no # Root hints root-hints: "/var/unbound/root.hints" # Ad blocking (see section below) include: "/var/unbound/blocklist.conf" remote-control: control-enable: yes control-interface: 127.0.0.1
Enable Unbound
In /etc/rc.conf:
shlocal_unbound_enable="YES"
Fetch root hints and start:
shfetch -o /var/unbound/root.hints https://www.internic.net/domain/named.cache service local_unbound start
Test resolution:
shdrill @192.168.1.1 freebsd.org
QoS and Traffic Shaping
FreeBSD offers two traffic shaping mechanisms: ALTQ (integrated with PF) and dummynet (ipfw-based, pipe/queue model). ALTQ is the more natural choice when PF is your firewall.
ALTQ with PF
ALTQ requires a kernel rebuilt with ALTQ support, or loading the appropriate modules. On FreeBSD 14+, ALTQ support is available as kernel modules:
shkldload altq_cbq kldload altq_hfsc
Add to /boot/loader.conf for persistence:
shaltq_cbq_load="YES" altq_hfsc_load="YES"
Add ALTQ rules to /etc/pf.conf (insert before filter rules):
shell# --- ALTQ Traffic Shaping --- # Shape outbound on WAN to 90% of upload speed (e.g., 90 Mbps of 100 Mbps) altq on $wan_if hfsc bandwidth 90Mb queue { q_default, q_bulk, q_priority } queue q_priority priority 7 hfsc (realtime 30Mb) { q_dns, q_ssh } queue q_dns priority 7 hfsc (realtime 5Mb) queue q_ssh priority 7 hfsc (realtime 5Mb) queue q_default priority 3 hfsc (default linkshare 50Mb) queue q_bulk priority 1 hfsc (linkshare 10Mb) # Assign traffic to queues pass out on $wan_if proto { tcp udp } from any to any port domain queue q_dns pass out on $wan_if proto tcp from any to any port ssh queue q_ssh pass out on $wan_if proto tcp from any to any port { 80 443 } queue q_default
This configuration prioritizes DNS and SSH, gives web traffic a fair share, and constrains bulk transfers.
Alternative: dummynet
If you prefer dummynet (no kernel rebuild required):
shkldload dummynet # Create a pipe limiting bandwidth to 50 Mbps ipfw pipe 1 config bw 50Mbit/s delay 0 queue 50 # Apply to traffic from a specific host ipfw add 100 pipe 1 ip from 192.168.1.150 to any out xmit igb0
Dummynet is useful for per-host bandwidth limiting and testing under constrained conditions.
Multi-WAN Failover
If you have two ISP connections, FreeBSD can fail over automatically using a monitoring script and routing table manipulation.
Setup
Assume:
- WAN1:
igb0-- primary, default gateway203.0.113.1 - WAN2:
igb2-- backup, gateway198.51.100.1
In /etc/rc.conf:
shifconfig_igb0="DHCP" ifconfig_igb2="inet 198.51.100.10 netmask 255.255.255.0" defaultrouter="203.0.113.1"
Failover Script
Create /usr/local/sbin/wan-failover.sh:
sh#!/bin/sh # Multi-WAN failover monitor PRIMARY_GW="203.0.113.1" BACKUP_GW="198.51.100.1" CHECK_HOST="1.1.1.1" PING_COUNT=3 FAIL_THRESHOLD=2 INTERVAL=10 fail_count=0 on_backup=0 while true; do if ping -c $PING_COUNT -t 5 -S $(ifconfig igb0 | awk '/inet /{print $2}') $CHECK_HOST > /dev/null 2>&1; then fail_count=0 if [ $on_backup -eq 1 ]; then logger -t wan-failover "Primary WAN restored, switching back" route delete default route add default $PRIMARY_GW on_backup=0 fi else fail_count=$((fail_count + 1)) if [ $fail_count -ge $FAIL_THRESHOLD ] && [ $on_backup -eq 0 ]; then logger -t wan-failover "Primary WAN down, failing over to backup" route delete default route add default $BACKUP_GW on_backup=1 fi fi sleep $INTERVAL done
shchmod +x /usr/local/sbin/wan-failover.sh
Add to /etc/rc.local or create a daemon wrapper to run at boot. For production environments, consider using ifstated or a cron-based health check with route(8).
PF NAT for Multi-WAN
Update pf.conf to NAT on both interfaces:
shellnat on igb0 from $lan_net to any -> (igb0) nat on igb2 from $lan_net to any -> (igb2)
PF will apply the correct NAT rule based on which interface the traffic exits.
VPN Gateway with WireGuard
Route all LAN traffic through a WireGuard tunnel. This is useful for privacy or connecting to a remote office. For a dedicated walkthrough, see our WireGuard setup guide.
Install and Configure
shpkg install wireguard-tools
Generate keys:
shwg genkey | tee /usr/local/etc/wireguard/privatekey | wg pubkey > /usr/local/etc/wireguard/publickey chmod 600 /usr/local/etc/wireguard/privatekey
Create /usr/local/etc/wireguard/wg0.conf:
ini[Interface] PrivateKey = <ROUTER_PRIVATE_KEY> ListenPort = 51820 Address = 10.0.0.1/24 [Peer] PublicKey = <REMOTE_PEER_PUBLIC_KEY> Endpoint = vpn.example.com:51820 AllowedIPs = 0.0.0.0/0 PersistentKeepalive = 25
Enable at Boot
In /etc/rc.conf:
shwireguard_enable="YES" wireguard_interfaces="wg0"
Start:
shservice wireguard start
Route LAN Traffic Through the Tunnel
To force all LAN client traffic through WireGuard, update PF:
shell# NAT LAN traffic out the WireGuard tunnel instead of WAN nat on wg0 from $lan_net to any -> (wg0) # Allow traffic on the tunnel pass in on $lan_if from $lan_net to any pass out on wg0 from any to any
And set the default route through the tunnel:
shroute add default 10.0.0.2
This approach encrypts all LAN traffic before it leaves the router.
DNS-Based Ad Blocking
Unbound can block ads and trackers at the DNS level by returning NXDOMAIN or 0.0.0.0 for known ad-serving domains. This works for every device on the network without installing any client-side software.
Generate the Blocklist
Create /usr/local/sbin/update-blocklist.sh:
sh#!/bin/sh # Fetch and convert ad blocklists for Unbound BLOCKLIST="/var/unbound/blocklist.conf" TMPFILE=$(mktemp) # Steven Black's unified hosts list fetch -qo - "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts" | \ awk '/^0\.0\.0\.0/ && !/0\.0\.0\.0$/ {print "local-zone: \""$2"\" redirect\nlocal-data: \""$2" A 0.0.0.0\""}' \ > "$TMPFILE" # Remove duplicates and install sort -u "$TMPFILE" > "$BLOCKLIST" rm -f "$TMPFILE" # Reload Unbound unbound-control reload logger -t blocklist "Updated Unbound blocklist: $(wc -l < $BLOCKLIST) entries"
shchmod +x /usr/local/sbin/update-blocklist.sh /usr/local/sbin/update-blocklist.sh
Automate Updates
Add a weekly cron job:
sh# /etc/crontab 0 3 * * 0 root /usr/local/sbin/update-blocklist.sh
Whitelist Domains
If a domain is incorrectly blocked, override it in unbound.conf:
yamlserver: local-zone: "example.com" transparent
This gives you network-wide ad blocking comparable to Pi-hole, without running additional software.
Monitoring
PF Statistics
sh# Real-time state table pfctl -s info # Connection states pfctl -s state | head -20 # Per-rule hit counters pfctl -vvs rules
Traffic Monitoring with pflog
PF logs blocked packets to the pflog0 interface. View them in real time:
shtcpdump -n -e -ttt -i pflog0
Or read stored logs:
shtcpdump -n -e -ttt -r /var/log/pflog
Bandwidth Monitoring
Install iftop for real-time per-connection bandwidth:
shpkg install iftop iftop -i igb0 # WAN traffic iftop -i igb1 # LAN traffic
For historical data, install vnstat:
shpkg install vnstat vnstatd -d vnstat -i igb0 --days
SNMP for External Monitoring
If you use a network monitoring system (Zabbix, LibreNMS, Nagios), enable SNMP:
shpkg install net-snmp
Configure /usr/local/etc/snmpd.conf:
shellrocommunity readonlycommunity 192.168.1.0/24 syslocation "Server Room" syscontact "admin@example.com"
Enable in /etc/rc.conf:
shsnmpd_enable="YES"
System Health
Monitor CPU, memory, and disk on the router itself:
shtop -b -d 1 | head -15 # Process overview vmstat 1 5 # Memory and CPU gstat # Disk I/O systat -ifstat 1 # Interface statistics
Security Hardening
A router is exposed to the internet by default. Harden it.
Disable Unnecessary Services
In /etc/rc.conf:
shsshd_enable="YES" # Keep SSH, but restrict access sendmail_enable="NONE" inetd_enable="NO"
Restrict SSH
In /etc/ssh/sshd_config:
shellPort 22 ListenAddress 192.168.1.1 PermitRootLogin no PasswordAuthentication no PubkeyAuthentication yes AllowUsers admin MaxAuthTries 3
This binds SSH to the LAN interface only. Root login is disabled. Password authentication is disabled -- keys only.
Sysctl Hardening
Add to /etc/sysctl.conf:
sh# Drop packets for non-routable addresses net.inet.ip.sourceroute=0 net.inet.ip.accept_sourceroute=0 # Ignore ICMP redirects net.inet.icmp.drop_redirect=1 net.inet.ip.redirect=0 # Prevent SYN floods net.inet.tcp.syncookies=1 # Randomize PID allocation kern.randompid=1 # Disable core dumps kern.coredump=0 # Black hole dropped TCP/UDP (silently drop instead of RST/ICMP unreachable) net.inet.tcp.blackhole=2 net.inet.udp.blackhole=1 # Limit ARP cache net.link.ether.inet.max_age=1200
Securelevel
Set the kernel securelevel to prevent modification of firewall rules even by root (useful in production):
sh# /etc/rc.conf kern_securelevel_enable="YES" kern_securelevel="2"
At securelevel 2, PF rules cannot be changed, immutable file flags cannot be removed, and raw disk access is prohibited. Only lower the securelevel from single-user mode.
Automatic Security Updates
Use freebsd-update in a cron job to fetch security patches:
sh# /etc/crontab 0 4 * * * root freebsd-update cron
Review and install manually:
shfreebsd-update fetch install
Putting It All Together
Here is the recommended order of operations for a fresh FreeBSD router build:
- Install FreeBSD (minimal, no ports/packages selected during install).
- Configure network interfaces and IP forwarding in
/etc/rc.conf. - Write
/etc/pf.confwith NAT and firewall rules. Enable and test PF. - Install and configure ISC DHCP. Verify clients get addresses.
- Configure Unbound for DNS with DNSSEC. Point DHCP at it.
- Set up ad blocking with the blocklist script.
- (Optional) Configure QoS/ALTQ if you need traffic shaping.
- (Optional) Set up WireGuard if you need VPN.
- (Optional) Configure multi-WAN if you have a backup ISP link.
- Harden SSH, sysctl, and enable securelevel.
- Set up monitoring (pflog, vnstat, SNMP).
- Test failover, reboot, and verify everything starts cleanly.
FAQ
Can FreeBSD handle gigabit NAT routing?
Yes. Even modest hardware (Intel Atom C3000, 4 GB RAM) can route and NAT at wire speed on gigabit links. FreeBSD's network stack processes millions of packets per second on multi-core hardware. For 10 Gbps, use Intel ix(4) NICs and ensure you have sufficient CPU cores.
Should I use FreeBSD or pfSense/OPNsense?
pfSense and OPNsense are FreeBSD-based distributions with a web GUI. If you want point-and-click management, use one of those. If you want full control, minimal overhead, and the ability to customize everything through configuration files and scripts, use plain FreeBSD. This guide gives you everything those GUIs configure, without the GUI overhead.
How do I update PF rules without dropping connections?
Run pfctl -f /etc/pf.conf. PF reloads rules atomically. Existing stateful connections continue as long as the new ruleset does not explicitly block them. There is no downtime during a rule reload.
Can I use VLANs to segment my LAN?
Absolutely. FreeBSD supports 802.1Q VLANs natively. Create VLAN interfaces in /etc/rc.conf, assign them separate subnets, and add PF rules to control traffic between VLANs. See our VLAN guide for step-by-step instructions.
How do I troubleshoot when LAN clients cannot reach the internet?
Work through this checklist:
- Verify IP forwarding is enabled:
sysctl net.inet.ip.forwardingshould return1. - Check PF is running:
pfctl -s infoshould show the PF status as enabled. - Confirm NAT rules are loaded:
pfctl -s natshould show thenat onrule. - Test from the router itself:
ping -c 3 8.8.8.8from the router. If this fails, WAN connectivity is the problem. - Check DHCP leases:
cat /var/db/dhcpd/dhcpd.leasesto verify clients received an address. - Check DNS:
drill @192.168.1.1 freebsd.orgfrom the router. If DNS fails, check Unbound. - Inspect PF logs:
tcpdump -n -e -ttt -i pflog0to see if traffic is being blocked.
What is the advantage of Unbound over forwarding to 8.8.8.8?
Unbound is a full recursive resolver. It queries authoritative DNS servers directly rather than relying on a third party. This gives you DNSSEC validation (cryptographic proof that DNS responses are authentic), privacy (your queries do not go to Google or Cloudflare), and lower latency for cached records. It also makes DNS-based ad blocking possible at the resolver level.
Conclusion
A FreeBSD router gives you a level of control and transparency that no commercial router or GUI-based distribution can match. Every component -- NAT, DHCP, DNS, VPN, QoS, monitoring -- is configured through plain text files that you can version-control, audit, and replicate. The system is stable, performant, and secure by default.
Start with the basic setup (forwarding, PF, DHCP, DNS) and add features as you need them. Once running, a FreeBSD router requires very little maintenance beyond applying security updates and reviewing logs.