FreeBSD.software
Home/Blog/How to Set Up a FreeBSD Router and Gateway
tutorial2026-03-29

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](/blog/pf-firewall-freebsd/).

**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](/blog/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:

+-----------+

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](/blog/nat-freebsd-pf/).


# /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 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 flush global)

# Block brute-force offenders

block drop in quick from

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](/blog/dhcp-server-freebsd/).

sh

pkg install isc-dhcp44-server

Complete dhcpd.conf

Create /usr/local/etc/dhcpd.conf:


# /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):


# --- 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:


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](/blog/wireguard-freebsd-setup/).

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 =

ListenPort = 51820

Address = 10.0.0.1/24

[Peer]

PublicKey =

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:


# 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:


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:


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](/blog/freebsd-vlans/) 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.