OPNsenseIII · Stateful Firewalling and PFStateful firewalling and PF
Tables, anchors, and include in PF
What you'll learn
- Explain what a PF table is and why it is more efficient than listing addresses in rules
- Read a rule that references a table and identify the table source
- Describe what PF anchors are for and how OPNsense uses them internally
- Use include to organise a complex ruleset into maintainable files
Prerequisites
Verified against OPNsense 25.x · FreeBSD 14.x · PF (FreeBSD packet filter) FreeBSD 14.x · Unbound 1.20+ · Kea DHCP OPNsense 25.x plugin · WireGuard in-kernel + OPNsense plugin · strongSwan (IPsec plugin) OPNsense 25.x plugin · OpenVPN 2.6.x · Suricata 7.x · 2026-08-14
A firewall with ten rules is easy. A firewall with five hundred rules is a maintenance nightmare unless the ruleset is organised. PF provides three features for that organisation: tables (match a rule against a large list of addresses efficiently), anchors (load sub-rulesets at specific points), and include (split a ruleset across multiple files). OPNsense uses all three internally; understanding them is the difference between “the firewall works” and “I can maintain this firewall as it grows”.
Tables: matching large address lists
A PF table is a named collection of addresses or CIDR prefixes.
A rule can match against a table the same way it matches
against a single address — from <table_name>. The difference
is performance: a rule that references a table is a single
hash lookup per packet, not a linear walk over the addresses
in the rule.
OPNsense exposes tables under Firewall → Tables (aliases in
older OPNsense versions). The operator creates a table, adds
addresses (manually, by URL, or by feeding it a list), and
references the table in a firewall rule.
$ pfctl -t allowed_admin_ips -T show 203.0.113.10
203.0.113.20
198.51.100.0/24Illustrative output
Why use tables rather than inline addresses?
- Performance. A rule with
from { 203.0.113.10, 203.0.113.20, 198.51.100.0/24 }is parsed as a list of three matches; PF checks each one per packet. A rule withfrom <allowed_admin_ips>is a single table lookup. For a table with thousands of entries, the difference is significant. - Maintainability. Adding or removing an address from a table does not require recompiling the ruleset. Adding or removing an address from an inline list does (because the rule changes).
- Atomic updates. Tables can be updated with
pfctl -t <name> -T replace -f <file>to swap an entire table’s contents in one operation. Inline lists require editing the ruleset.
Built-in tables OPNsense exposes
OPNsense populates several tables automatically:
<wan_net>— the network address of the WAN interface.<lan_net>— the network address of the LAN interface.<bogons>— bogon networks (private, reserved, not-yet- allocated). Updated by OPNsense from a curated source.<sshlockout>— IPs that have been locked out by the SSH lockdown feature.
A rule can reference these tables the same way it references operator-created ones. For example, a rule that blocks inbound traffic from bogons:
block in quick on igb1 from <bogons> to any
The bogons table is updated automatically; the rule does not need to be edited when the table contents change.
Anchors: sub-rulesets
A PF anchor is a named point in the ruleset where additional
rules can be loaded. The main ruleset says anchor <name> at
a specific point; PF loads the anchor’s ruleset there. Anchors
are how OPNsense injects feature-specific rules (OpenVPN,
IPsec, captive portal) without coupling them to the main
ruleset.
For example, the compiled ruleset might include:
@200 anchor "openvpn/*" in on igb0
@210 anchor "ipsec/*" in on igb0
@220 pass in quick on igb0 inet proto tcp from 192.0.2.0/24 to any port = 22
The anchor "openvpn/*" in on igb0 line tells PF to load any
ruleset under openvpn/ at that point in the ruleset. The
OpenVPN subsystem can add its own rules without touching the
main ruleset.
$ pfctl -s Anchorsipsec/900
ipsec/910
openvpn/100
openvpn/110
captiveportal/igb0
captiveportal/igb1Illustrative output
To inspect the rules in an anchor:
pfctl -s Anchors
pfctl -a <anchor_name> -s rules
The first command lists active anchors; the second shows the rules loaded into a specific anchor. This is how the operator verifies that feature-specific rules compiled correctly.
Include: splitting a ruleset across files
For advanced rulesets, OPNsense allows include statements in
the compiled pf.conf. An include pulls in another file’s
contents at the specified point. This lets the operator split a
large ruleset across multiple files (one per interface, one
per feature, etc.).
In OPNsense this is exposed via System → Advanced → Firewall → Generated Rules. The operator can drop a .conf snippet
into a specific point in the ruleset, and OPNsense inserts it
via include at the next config apply.
The discipline: keep snippets small, comment them heavily, and document the intent. A snippet that does “something” with no explanation is an incident waiting to happen when the next operator inherits the firewall.
Production patterns
Three patterns show up repeatedly.
Alias-driven rules
The operator creates an alias (which becomes a table) for
“known good” or “known bad” IP sets, then references the alias
in a rule. Adding a new entry is an alias edit, not a rule
edit. This is the pattern OPNsense uses internally for
<bogons>, <sshlockout>, and the per-interface network
aliases.
Anchor-based feature isolation
When OpenVPN, IPsec, or Captive Portal is enabled, OPNsense loads rules into anchors. The operator does not edit those rules directly; the GUI manages them. This isolation means that editing a firewall rule under the LAN interface does not affect the VPN rules, and vice versa.
Snippet-driven customisation
For rules that the GUI cannot express (advanced routing matchers, custom NAT, conditional logic), the operator drops a snippet into the appropriate point. Snippets are advanced and should be reviewed by another operator before deployment.
Summary
- Tables match large address lists with a single hash lookup. OPNsense aliases map to PF tables.
- Anchors load sub-rulesets at specific points. OPNsense uses anchors for OpenVPN, IPsec, and Captive Portal.
- Include splits a ruleset across files. OPNsense exposes
include via custom snippets under
System → Advanced. - All three features are advanced. Use them deliberately and
verify with
pfctl -s rulesandpfctl -s Anchorsafter every change. - Custom snippets may break across OPNsense upgrades. Prefer GUI features when possible.
Knowledge check · 4 questions
Q1. You create a rule to block traffic from a list of 5000 IPs. You can express the rule as either an inline list `from { ip1, ip2, ..., ip5000 }` or as a table reference `from <blocklist>`. Which is the correct choice and why?
Q2. OPNsense aliases (under Firewall → Aliases) are stored as PF tables in the runtime ruleset.
Q3. Which of the following are valid use cases for PF anchors in OPNsense? Select all that apply.
Q4. You added a custom pf.conf snippet via System → Advanced, but the rules in the snippet do not appear in pfctl -s rules. What is the most likely cause?
Passing score: 75%. Answers are checked in this browser.