pf — FreeBSD, OpenBSD, and friends

Blocks via pf using a table inside a dedicated anchor. Banning an IP is one table add; the block rules referencing the table are loaded once at init.

[kur.sshd]
backend   = "pf"
ports     = [ "22" ]
protocols = [ "tcp" ]
ban_time  = 3600

[kur.sshd.options]
kill = 1

What it creates

block drop quick proto tcp from <kur_sshd> to any port 22

Rules take the form block drop quick proto <protocol> from <<table>> to any with port <port> appended once per port for tcp/udp/sctp. Protocols that cannot take ports get one portless rule.

Requirements

Ports, protocols, and names

Options

| option | default | what | |--------|---------|------------------------------------------| | kill | 0 | kill existing states for a banned IP |

kill

Banishing an IP to the table only stops new connections — established states keep talking (see security; for ban-on-abuse you almost certainly want kill = 1).

With it on, each ban also severs live states, scoped to what the kur blocks:

Kill commands are best effort; a failure (no matching states) is ignored.

What each operation runs

With A = pfctl -a <prefix>/<name> and T = <prefix>_<name>:

| operation | commands | |------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------| | init | cleanup (failures ok): A -t T -T flush, A -t T -T kill, A -F rules; then (fatal): echo 'table <T> persist counters' \| A -f-, echo '<rules>' \| A -f- | | ban | A -t T -T add <ip>, then the kill commands if enabled | | unban | A -t T -T delete <ip> | | list | no command — the kur's own ban book | | check | A -t T -T show must exit 0, and A -sr must produce output | | flush | A -t T -T flush | | re_init | teardown (best effort), init, then re-add every banned IP | | teardown | A -t T -T flush, A -t T -T kill, A -F rules |

Everything is scoped to the anchor — -F rules here flushes only the anchor's rules, never the main ruleset.

self_heal and reloads

check probes that the table exists and the anchor still holds rules. A pfctl -f /etc/pf.conf reload wipes anchor contents that are not in the file, so the next ban/unban with self_heal on notices and re-inits, re-banning everything from the kur's book. What check cannot notice is the missing anchor "kur/*" line — see Requirements.

Gotchas