TL;DR

1
2
3
4
5
6
7
8
9
pfctl -sT                                    # loaded tables
pfctl -t <table> -T show                     # contents
pfctl -t <table> -T add 192.168.1.10         # add an IP or network (10.20.0.0/16 works too)
pfctl -t <table> -T delete 192.168.1.10
pfctl -t <table> -T test 192.168.1.10        # is it inside? (match / no match)
pfctl -t <table> -T flush                    # empty the table
pfctl -t <table> -T replace -f /path/list    # replace the whole contents from a file

pfctl -t sshguard -T flush                   # pfSense: clear the failed-login lockout

What a table is

A PF table is a set of addresses and networks that rules reference as <name>. It changes live, without reloading the ruleset, and holds thousands of entries with fast lookups; that is why PF uses tables for block lists, bogons, and anything that changes more often than the rules.

In pf.conf:

1
2
table <blocklist> persist file "/etc/pf/blocklist.txt"
block in quick from <blocklist>

persist keeps the table even when no rule uses it at the moment; file loads it from a file (one IP or network per line).

Commands

All in the form pfctl -t <table> -T <command>:

CommandWhat it does
showLists the entries. With -v, per-entry counters (packets/bytes in/out)
add <ip|net> …Adds one or more entries
delete <ip|net> …Removes entries
test <ip> …Says whether the address falls inside the table (without touching it)
flushEmpties the table; the table keeps existing
killDeletes the whole table (if no rule uses it)
replace -f <file>Swaps the entire contents for the file’s, in a single operation
expire <seconds>Deletes entries whose counters have been idle for more than N seconds
zeroZeroes the table’s counters (or one entry’s, if you name it)

And without -t:

1
2
3
4
pfctl -sT                    # names of all tables
pfctl -vvsT                  # global info per table: entries, evaluations, references
pfctl -T load -f /etc/pf.conf   # reload only the table definitions from pf.conf
pfctl -sr | grep '<name>'    # which rules use a table

Tables on pfSense

  • Aliases are tables. A Host, Network, or URL Table alias called Admins exists in PF as <Admins>; pfctl -t Admins -T show shows what pfSense resolved, including the FQDNs it already turned into IPs.
  • sshguard: where IPs blocked for failed SSH or webGUI logins land. If you locked yourself out, pfctl -t sshguard -T flush (documented by Netgate under Locked out of the WebGUI).
  • virusprot: hosts blocked for exceeding a rule’s Max. src. conn. rate or Max. states limits. pfctl -t virusprot -T show and -T delete <ip> to release one.
  • snort2c: Snort/Suricata blocks in legacy mode. -T flush to unblock everything.
  • bogons / bogonsv6: the lists that update themselves when you enable Block bogon networks on an interface.
  • pfBlockerNG: every list is a pfB_* alias. To know whether an IP is blocked by one: pfctl -t pfB_Top_v4 -T test 203.0.113.7.

On OPNsense the mechanics are the same (pfctl -sT to see the names); the login lockout table is called sshlockout.

What gets lost on reload

Any -T add, -T delete, or -T flush lives until the next filter reload: saving almost anything in the GUI, Filter Reload under Status → Filter Reload, or the alias update cron. At that point pfSense regenerates every table from its alias and your manual changes are gone. To make something permanent, edit the alias; for quick tests and unblocking, pfctl is perfect.

Beyond tables

What gets used most alongside the above:

1
2
3
4
5
6
7
pfctl -sr                    # loaded rules (with -vv, counters and rule numbers)
pfctl -ss                    # state table
pfctl -si                    # global statistics
pfctl -k 192.168.1.10        # kill a host's states
pfctl -F states              # kill all states (cuts every connection)
pfctl -d / pfctl -e          # disable / enable PF (no PF means no NAT: only to get out of a lockout)
pfctl -nf /etc/pf.conf       # syntax check without loading

Full reference: the OpenBSD Handbook’s pfctl cheat sheet and man pfctl on FreeBSD.