OpenBSD Journal

Source and state limiters introduced in pf

Contributed by rueda on from the better-a-limited-state-than-a-failed-one dept.

David Gwynne (dlg@) has introduced source and state limiters, which provide a massive increase in the flexibily of pf traffic limiting:

CVSROOT:	/cvs
Module name:	src
Changes by:	dlg@cvs.openbsd.org	2025/11/10 21:06:20

Modified files:
	sbin/pfctl     : parse.y pfctl.8 pfctl.c pfctl_parser.c 
	                 pfctl_parser.h 
	share/man/man5 : pf.conf.5 
	sys/net        : pf.c pf_ioctl.c pf_table.c pfvar.h pfvar_priv.h 

Log message:
introduce source and state limiters in pf.

both source and state limiters can provide constraints on the number
of states that a set of rules can create, and optionally the rate
at which they are created. state limiters have a single limit, but
source limiters apply limits against a source address (or network).
the source address entries are dynamically created and destroyed,
and are also limited.
this started out because i was struggling to understand the source and
state tracking options in pf.conf, and looking at the code made it
worse. it looked like some functionality was missing, and the code also
did some things that surprised me. taking a step back from it, even it
if did work, what is described doesn't work well outside very simple
environments.

the functionality i'm talking about is most of the stuff in the
Stateful Tracking Options section of pf.conf(4).

some of the problems are illustrated one of the simplest options:
the "max number" option that limits the number of states that a
rule is allowed to create:

- wiring limits up to rules is a problem because when you load a
new ruleset the limit is reset, allowing more states to be created
than you intended.
- a single "rule" in pf.conf can expand to multiple rules in the
kernel thanks to things like macro expansion for multiple ports.
"max 1000" on a line in pf.conf could end up being many times
that in effect.
- when a state limit on a rule is reached, the packet is dropped.
this makes it difficult to do other things with the packet, such a
redirect it to a tarpit or another server that replies with an
outage notices or such.

a state limiter solves these problems. the example from the pf.conf.5
change demonstrates this:

An example use case for a state limiter is to restrict the number of
connections allowed to a service that is accessible via multiple
protocols, e.g. a DNS server that can be accessed by both TCP and UDP on
port 53, DNS-over-TLS on TCP port 853, and DNS-over-HTTPS on TCP port 443
can be limited to 1000 concurrent connections:

state limiter "dns-server" id 1 limit 1000

pass in proto { tcp udp } to port domain state limiter "dns-server"
pass in proto tcp to port { 853 443 } state limiter "dns-server"

a single limit across all these protocols can't be implemented with
per rule state limits, and any limits that were applied are reset
if the ruleset is reloaded.

the existing source-track implementation appears to be incomplete,
i could only see code for "source-track global", but not "source-track
rule". source-track global is too heavy and unweildy a hammer, and
source-track rule would suffer the same issues around rule lifetimes
and expansions that the "max number" state tracking config above has.

a slightly expanded example from the pf.conf.5 change for source limiters:

An example use for a source limiter is the mitigation of denial of
service caused by the exhaustion of firewall resources by network or port
scans from outside the network.  The states created by any one scanner
from any one source address can be limited to avoid impacting other
sources.  Below, up to 10000 IPv4 hosts and IPv6 /64 networks from the
external network are each limited to a maximum of 1000 connections, and
are rate limited to creating 100 states over a 10 second interval:

source limiter "internet" id 1 entries 10000 \
limit 1000 rate 100/10 \
inet6 mask 64

block in on egress
pass in quick on egress source limiter "internet"
pass in on egress proto tcp probability 20% rdr-to $tarpit

the extra bit is if the source limiter doesn't have "space" for the
state, the rule doesn't match and you can fall through to tarpitting
20% of the tcp connections for fun.

i've been using this in anger in production for over 3 years now.

sashan@ has been poking me along (slowly) to get it in a good enough
shape for the tree for a long time. it's been one of those years.

bluhm@ says this doesnt break the regress tests.
ok sashan@
This change has our resident packet manglers quite excited, and they think it will likely be a signature feature that will make the not-too-distant OpenBSD 7.9 release even more of an Internet favorite.

Credits

Copyright © - Daniel Hartmeier. All rights reserved. Articles and comments are copyright their respective authors, submission implies license to publish on this web site. Contents of the archive prior to as well as images and HTML templates were copied from the fabulous original deadly.org with Jose's and Jim's kind permission. This journal runs as CGI with httpd(8) on OpenBSD, the source code is BSD licensed. undeadly \Un*dead"ly\, a. Not subject to death; immortal. [Obs.]