FreeBSD IPFW Lookup Tables: Build and Update Large Match Sets Safely
Use IPFW lookup tables for large address and key sets, stage changes, inspect matches, and avoid partial updates or remote lockouts.
IPFW lookup tables let a ruleset refer to a changing set of addresses, interfaces, ports, numeric identifiers, or flow keys without creating one firewall rule per element. The rule expresses policy once; an operator updates table data as the set changes. That division is useful for block lists, partner networks, service cohorts, or dynamic routing policy, but it creates a second stateful object that must be inventoried and tested along with the rules.
This guide is about lookup-table lifecycle, not a complete firewall design. A table match is only one condition in the ordered IPFW ruleset. A mistaken earlier allow rule, an unexpected default rule, an unreviewed rule set, or a table in the wrong set can defeat the intended policy. Treat table contents and the referencing rule as one change, and keep console or out-of-band access available during remote firewall work.
Choose a table type from the lookup key
IPFW requires explicit table creation. The addr type accepts IPv4 and IPv6 addresses and prefixes, choosing the most specific matching entry. Other documented types include iface, number, flow, and mac. Choose a type based on what the rule actually needs to test; an address table is not a generic string map, and table values are separate from keys.
Create an address table and populate a small initial set:
ipfw table block_v4 create type addr
ipfw table block_v4 add 198.51.100.0/24
ipfw table block_v4 add 203.0.113.17
ipfw table block_v4 list
ipfw table block_v4 info
The addresses above are reserved for documentation and are examples only. Do not paste example network ranges into a production deny list. Build the actual list from an approved source, preserve its provenance and generation time, and normalize duplicates before loading it.
A rule can use a table reference where an address match is accepted:
ipfw add 120 deny ip from 'table(block_v4)' to any
Rule order is significant. Before inserting a rule, inspect the complete active ruleset and verify the default rule action:
ipfw -a -S list
ipfw table all info
The -S option displays rule-set membership, which matters when sets are used. Table references normally resolve to tables in set 0 even if the rule is in another set; the net.inet.ip.fw.tables_sets setting changes that behavior so the rule’s set is used. Do not assume that placing a table in a disabled set automatically makes a set of rules resolve to it. Confirm the setting, table set, and rule set on the target host.
Understand table keys, values, and matching
An address table can contain networks as well as individual hosts. A query for an address uses the most-specific matching prefix, so a narrow entry can take precedence over a broader network. This is powerful but makes overlapping entries worth reviewing. For instance, a broad allow-related table and a more specific deny table do not by themselves establish which policy wins; the rule sequence and each rule’s action still decide that.
Some table types can hold values. A rule may match an address and a value or use the value through tablearg. This supports policy maps such as selecting a rule number, pipe, or other documented action parameter from an entry. Values have a type and mask. Keep the key and value schemas explicit in your source file and test the rule action with representative records. A lookup that finds a key but supplies an unexpected value is not equivalent to a simple membership test.
Use the kernel’s own table listing to verify what was installed:
ipfw -i table block_v4 list
ipfw table block_v4 lookup 203.0.113.17
ipfw table block_v4 detail
The lookup operation is optional for some algorithms and may be unsupported; a failed diagnostic lookup does not prove packet matching is broken. Listing and info provide different evidence: entries show stored keys and values, while info reports generic table details. Choose a table algorithm only after measuring realistic data and traffic; do not assume a particular implementation wins for every set size or lookup pattern.
Load a large set without confusing partial and atomic updates
The ordinary add command accepts one or more entries. If one entry in a multi-entry request is invalid, the documented default behavior can add other valid entries and still return a non-zero status. Scripts that merely check “some output appeared” can therefore leave a partial table. The atomic add form requests all-or-none behavior for that operation:
ipfw table block_v4 atomic add 198.51.100.0/24 203.0.113.17
status=$?
if [ "$status" -ne 0 ]; then
echo "table update rejected; inspect source data and current table" >&2
exit "$status"
fi
For a full replacement, use two tables of the same type. Create a staging table under a distinct name, load the complete candidate set, inspect its contents and count, and then swap it with the live table. IPFW documents a table swap as atomic, but the swap can fail when configured limits would be exceeded by the exchange. A swap is a visibility boundary, not a guarantee that candidate data are correct or that a change cannot lock out an administrator.
ipfw table block_next create type addr
ipfw table block_next atomic add 198.51.100.0/24 203.0.113.17
ipfw table block_next info
ipfw -i table block_next list
# Review the complete candidate before this live change:
ipfw table block_v4 swap block_next
ipfw -i table block_v4 list
That example demonstrates the command shape, not a universally safe production script. A real loader must handle an already-existing staging table, clear or recreate it in a controlled way, check every exit status, compare the candidate with the approved source, and avoid a race between validation and swap. Do not add -q just to hide output: for several operations -q implies -f, which suppresses confirmation for potentially dangerous commands. Also note that a batch of ordinary additions may have partial success; use the documented atomic form when all-or-none insertion is required.
Persist intent separately from live state
Runtime table entries are not a substitute for a reproducible configuration. Record the source dataset, its format, approval path, generation timestamp, expected entry count, table type, and the exact rules that consume it. Store the generator and input under configuration management, and make startup behavior deterministic. The IPFW rc configuration can load firewall rules, but validate how the chosen release and local scripts populate tables; do not assume a table is automatically reconstructed simply because its rule is persisted.
For a change window, stage and inspect the data before changing live policy. If using a rules file, IPFW’s -n mode checks command syntax without passing commands to the kernel, but it does not validate external data files or prove packet behavior. Syntax checking is one gate, not a safety proof. A complete review includes current rule order, set membership, table contents, counters, routes, and a way to restore management access.
Define a rollback artifact before the update: a prior table dump, prior generator input, and a tested command sequence for restoring it. Table swap can make switching between two prepared sets fast, but it does not preserve an unlimited history. After the change, verify the active table by listing it, check relevant rule counters, and test traffic from both a matching and a nonmatching source. Use packet capture only as corroboration; a single successful ping does not demonstrate that all production paths or protocols behave as intended.
Diagnose common failure modes
An empty table can make a correct-looking rule match nothing. A populated table can still be ignored if the rule references the wrong table name or set. A table value can be mistyped or masked unexpectedly. A prefix can be broader than intended. A failed add can leave some entries installed if the operation was not explicitly atomic. A candidate swap can be rejected by table limits. A restart can restore rules but not the runtime data your service expects. These failures have different remedies, so compare active state with a known-good candidate instead of flushing everything.
When a remote update interrupts the session, use the console to inspect the active ruleset and restore management reachability. IPFW’s quiet option is not a transactional wrapper around a whole file, and applying commands over SSH can terminate the connection before later commands execute. Prefer console access, a scheduled rollback mechanism that is independently verified, or a prepared ruleset/set transition. Avoid changing the default rule or flushing tables in an interactive remote session without an explicit recovery plan.
Acceptance checks
Accept a table update only when the source dataset is approved and reproducible; the key/value type and table set are correct; all candidate entries are visible before activation; an atomic operation or reviewed staging-and-swap sequence is used where needed; and the active table matches the candidate afterward. Confirm the referencing rule is active in the expected set, counters move for a controlled test, permitted traffic remains permitted, and management access survives. Save pre-change and post-change rule and table listings with the change record.
Tables separate rule logic from changing data; they are not an authorization source on their own. Their operational value comes from disciplined data ownership, explicit failure handling, and verification of the rule-table relationship after every change.
Related:
- FreeBSD IPFW and Dummynet: Firewall Rules, Queues, and Traffic Emulation
- Configuring pf on FreeBSD: A Practical Guide to Packet Filtering
Sources: