FreeBSD.software
Home/Guides/How to Set Up a FreeBSD Router and Gateway
tutorial·2026-03-29·16 min read

How to Set Up a FreeBSD Router and Gateway

Complete guide to building a FreeBSD router and gateway. Covers IP forwarding, NAT with PF, DHCP, DNS, QoS, multi-WAN, VPN, and monitoring.

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

sh
sysctl net.inet.ip.forwarding=1

Verify:

sh
sysctl 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:

sh
pfctl -f /etc/pf.conf pfctl -e # enable PF if not already running

Verify NAT state:

sh
pfctl -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.

sh
pkg 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:

sh
dhcpd_enable="YES" dhcpd_ifaces="igb1"

Start the service:

sh
service 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:

yaml
server: 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:

sh
local_unbound_enable="YES"

Fetch root hints and start:

sh
fetch -o /var/unbound/root.hints https://www.internic.net/domain/named.cache service local_unbound start

Test resolution:

sh
drill @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:

sh
kldload altq_cbq kldload altq_hfsc

Add to /boot/loader.conf for persistence:

sh
altq_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):

sh
kldload 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 gateway 203.0.113.1
  • WAN2: igb2 -- backup, gateway 198.51.100.1

In /etc/rc.conf:

sh
ifconfig_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
sh
chmod +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:

shell
nat 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

sh
pkg install wireguard-tools

Generate keys:

sh
wg 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:

sh
wireguard_enable="YES" wireguard_interfaces="wg0"

Start:

sh
service 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:

sh
route 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"
sh
chmod +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:

yaml
server: 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:

sh
tcpdump -n -e -ttt -i pflog0

Or read stored logs:

sh
tcpdump -n -e -ttt -r /var/log/pflog

Bandwidth Monitoring

Install iftop for real-time per-connection bandwidth:

sh
pkg install iftop iftop -i igb0 # WAN traffic iftop -i igb1 # LAN traffic

For historical data, install vnstat:

sh
pkg install vnstat vnstatd -d vnstat -i igb0 --days

SNMP for External Monitoring

If you use a network monitoring system (Zabbix, LibreNMS, Nagios), enable SNMP:

sh
pkg install net-snmp

Configure /usr/local/etc/snmpd.conf:

shell
rocommunity readonlycommunity 192.168.1.0/24 syslocation "Server Room" syscontact "admin@example.com"

Enable in /etc/rc.conf:

sh
snmpd_enable="YES"

System Health

Monitor CPU, memory, and disk on the router itself:

sh
top -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:

sh
sshd_enable="YES" # Keep SSH, but restrict access sendmail_enable="NONE" inetd_enable="NO"

Restrict SSH

In /etc/ssh/sshd_config:

shell
Port 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:

sh
freebsd-update fetch install

Putting It All Together

Here is the recommended order of operations for a fresh FreeBSD router build:

  1. Install FreeBSD (minimal, no ports/packages selected during install).
  2. Configure network interfaces and IP forwarding in /etc/rc.conf.
  3. Write /etc/pf.conf with NAT and firewall rules. Enable and test PF.
  4. Install and configure ISC DHCP. Verify clients get addresses.
  5. Configure Unbound for DNS with DNSSEC. Point DHCP at it.
  6. Set up ad blocking with the blocklist script.
  7. (Optional) Configure QoS/ALTQ if you need traffic shaping.
  8. (Optional) Set up WireGuard if you need VPN.
  9. (Optional) Configure multi-WAN if you have a backup ISP link.
  10. Harden SSH, sysctl, and enable securelevel.
  11. Set up monitoring (pflog, vnstat, SNMP).
  12. 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:

  1. Verify IP forwarding is enabled: sysctl net.inet.ip.forwarding should return 1.
  2. Check PF is running: pfctl -s info should show the PF status as enabled.
  3. Confirm NAT rules are loaded: pfctl -s nat should show the nat on rule.
  4. Test from the router itself: ping -c 3 8.8.8.8 from the router. If this fails, WAN connectivity is the problem.
  5. Check DHCP leases: cat /var/db/dhcpd/dhcpd.leases to verify clients received an address.
  6. Check DNS: drill @192.168.1.1 freebsd.org from the router. If DNS fails, check Unbound.
  7. Inspect PF logs: tcpdump -n -e -ttt -i pflog0 to 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.

Get more FreeBSD guides

Weekly tutorials, security advisories, and package updates. No spam.