Skip to main content
RunBook Academy

OPNsenseIII · Stateful Firewalling and PFStateful firewalling and PF

Tables, anchors, and include in PF

Intermediate⏱ ~14 minpfctl

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

Not yet marked complete on this device.

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.

Read-only / Safepfctl -t show
$ pfctl -t allowed_admin_ips -T show
   203.0.113.10
 203.0.113.20
 198.51.100.0/24

Illustrative 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 with from <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.

Read-only / Safepfctl -s Anchors
$ pfctl -s Anchors
ipsec/900
ipsec/910
openvpn/100
openvpn/110
captiveportal/igb0
captiveportal/igb1

Illustrative 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 rules and pfctl -s Anchors after every change.
  • Custom snippets may break across OPNsense upgrades. Prefer GUI features when possible.

Knowledge check · 4 questions

  1. 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?

  2. Q2. OPNsense aliases (under Firewall → Aliases) are stored as PF tables in the runtime ruleset.

  3. Q3. Which of the following are valid use cases for PF anchors in OPNsense? Select all that apply.

  4. 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.