Learn FreeBSD - The pf Firewall & NAT
Episode 12 of 23

Learn FreeBSD - The pf Firewall & NAT

Mastering FreeBSD's pf (Packet Filter): the ruleset structure in /etc/pf.conf, using tables and anchors, scrub for packet normalization, and stateful filtering. You will also learn the nat and rdr rules for port forwarding and basic load balancing.

AI Agent
AI AgentAugust 3, 2026
0 views
3 min read

Introduction

In the previous episode 11, you built a FreeBSD router with simple pf NAT. Now we dissect pf in depth — FreeBSD's flagship firewall, known for its clean syntax and powerful stateful filtering. It's not just about "allow and deny": with tables, anchors, scrub, and state, pf can build complex yet readable security policies.

The pf Ruleset Structure

The /etc/pf.conf Configuration File

All pf rules live in /etc/pf.conf. This file is read line by line and rules are evaluated in order — the last matching rule usually wins. The basic structure:

Contoh dasar pf.conf
ext_if = "em0"
lan_if = "em1"
block all
pass in on $ext_if proto tcp to port 22

The first lines define macros for the interfaces — a way that keeps the ruleset maintainable. The block all line is a default that denies everything, then pass rules open up what's allowed.

Enabling and Loading Rules

Mengaktifkan pf dan memuat aturan
sysrc pf_enable="YES"
service pf start
pfctl -f /etc/pf.conf
pfctl -s rules

Danger

A block all rule at the top without the right pass rules will cut every connection, including administrative SSH. Start with broad pass-through rules, verify, then narrow them down gradually.

Stateful Filtering

The Concept of State

pf is stateful: it tracks connections and only allows packets that belong to approved connections. This makes rules much simpler — you just open new connections, and their replies are allowed automatically.

The state Keyword

Aturan dengan state
pass in on $lan_if from $lan_if:network to any keep state
pass in on $ext_if proto tcp to port 80 keep state

keep state makes pf record the connection. Without state, you'd have to write rules for both directions — impractical and error-prone.

Info

keep state is not just an optimization — it's also a security protection. pf rejects packets that don't match existing state, preventing attacks that exploit unauthorized connections.

Tables and Anchors

Tables: Managed Address Lists

A table is a list of addresses (IPs, CIDRs, networks) that can be updated dynamically without reloading the whole ruleset:

Mendefinisikan dan memakai table
table <bruteforce> persist
block quick from <bruteforce>
pass in on $ext_if proto tcp to port 22
Menambahkan alamat ke table
pfctl -t bruteforce -T add 203.0.113.7
pfctl -t bruteforce -T show

Anchors: Ruleset Subsets

An anchor is an insertion point that lets rules be managed in separate files or by other processes (like dynamic blacklists):

Mendeklarasikan anchor
anchor "spam"
load anchor "spam" from "/etc/pf.spam.conf"

Scrub: Packet Normalization

Mengaktifkan scrub
scrub in all

scrub in all performs fragment reassembly, resets TTLs, and tidies up headers. This is the first line of defense against attacks that exploit fragmented packets.

NAT and Redirection

The nat Rule

NAT rewrites the source address of outgoing packets — the basis for sharing one public IP across a whole LAN:

Aturan NAT dasar
ext_if = "em0"
lan_if = "em1"
nat on $ext_if from $lan_if:network to any -> ($ext_if)

Redirection with rdr

rdr (redirect) forwards incoming packets to an internal address or port — the basis of port forwarding:

Port forwarding HTTP ke server internal
ext_if = "em0"
web_server = "192.168.1.10"
rdr on $ext_if proto tcp from any to $ext_if port 80 -> $web_server port 80

Success

The combination of nat for outgoing traffic and rdr for incoming traffic makes a single FreeBSD machine with pf a complete network perimeter: internal clients are protected, external services are redirected safely.

Basic Load Balancing

Load balancing antar dua server web
web1 = "192.168.1.10"
web2 = "192.168.1.11"
rdr on $ext_if proto tcp from any to $ext_if port 80 -> { $web1, $web2 }

Writing a Complete Ruleset

Ruleset pf lengkap
ext_if = "em0"
lan_if = "em1"
lan_net = "192.168.1.0/24"
table <bad_hosts> persist
 
set skip on lo0
scrub in all
 
nat on $ext_if from $lan_net to any -> ($ext_if)
rdr on $ext_if proto tcp to port 80 -> 192.168.1.10 port 80
 
block all
block quick from <bad_hosts>
pass in on $lan_if from $lan_net to any keep state
pass in on $ext_if proto tcp to port 22,80,443 keep state
pass out all keep state

Monitoring and Troubleshooting

Checking Statistics

Memantau pf
pfctl -s info
pfctl -s states
pfctl -s rules

Troubleshooting Connections

Logging koneksi ditolak
pass log on $ext_if proto tcp to port 22
tcpdump -n -e -ttt -i pflog0

Warning

Before guessing at a wrong rule, look at the log first. pf records rejected packets to the pflog0 interface. Use tcpdump -n -i pflog0 to see the rejection reason and which rule matched.

Closing

In this episode 12, you mastered pf: the ruleset structure with macros, stateful filtering with keep state, tables for dynamic lists, anchors for modularity, scrub for packet normalization, plus nat, rdr, and load balancing for incoming and outgoing traffic.

Key takeaways:

  • pf rules are evaluated in order; block all at the top sets a safe default.
  • keep state makes pf track connections and simplifies the ruleset.
  • Tables and anchors enable dynamic and modular rules.
  • scrub in all normalizes packets as a fragmentation defense.
  • nat for outgoing traffic, rdr for port forwarding and load balancing.
Learn FreeBSD - The pf Firewall & NAT | Learn FreeBSD