pf: introduce source and state limiters

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@

Obtained from:	OpenBSD, dlg <dlg@openbsd.org>, 8463cae72e
Sponsored by:	Rubicon Communications, LLC ("Netgate")
This commit is contained in:
Kristof Provost
2025-12-30 20:06:48 +01:00
parent c498eaa2f9
commit 4616481212
14 changed files with 3456 additions and 103 deletions
+159 -1
View File
@@ -27,7 +27,7 @@
.\" ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
.\" POSSIBILITY OF SUCH DAMAGE.
.\"
.Dd November 3, 2025
.Dd December 30, 2025
.Dt PF.CONF 5
.Os
.Sh NAME
@@ -2365,6 +2365,24 @@ For example, the following rule will drop 20% of incoming ICMP packets:
.Bd -literal -offset indent
block in proto icmp probability 20%
.Ed
.It Cm state limiter Ar name
Use the specified state limiter to restrict the creation of states
by this rule.
If capacity is not availabe, the rule does not match and evaluation
of the ruleset continues.
See the
.Sx State Limiters
section for more information.
.Pp
.It Cm source limiter Ar name
Use the specified source limiter to restrict the creation of states
by this rule.
If capacity is not availabe, the rule does not match and evaluation
of the ruleset continues.
See the
.Sx Source Limiters
section for more information.
.Pp
.It Ar prio Aq Ar number
Only match packets which have the given queueing priority assigned.
.El
@@ -2609,6 +2627,145 @@ Example:
.Bd -literal -offset indent
pass in proto tcp from any to any port www synproxy state
.Ed
.Ss State Limiter
State limiters provide a mechanism to limit the number of states created,
or the rate of state creation,
by a set of rules.
State limiters are configured and loaded with the main ruleset, but
can be used by rules in any anchor.
The overall number of states is still subject to the limit set with
.Cm set limit states ,
but the number of states created by a subset of rules can be provided
by a state limiter.
.Pp
A state limiter is configured with the following statement:
.Pp
.Bl -tag -width xxxx -compact
.It Cm state limiter Ar name
Each state limiter is identified by a unique name.
.El
.Pp
State limiters support the following configuration:
.Pp
.Bl -tag -width xxxx -compact
.It Cm id Ar number
A unique identifier between 1 and 255.
This configuration is required.
.It Cm limit Ar number
Specify the maximum number of states.
This configuration is required.
.It Cm rate Ar number Ns / Ns Ar seconds
Limit the rate at which states can be created over a time interval.
The connection rate is an approximation calculated as a moving
average.
.El
.Pp
Pass rules can specify a state limiter using the
.Cm state limiter Ar name
option.
If the number of states allowed has hit the limit, the pass rule
does not match and ruleset evalation continues past it.
.Pp
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:
.Pp
.Bd -literal -offset indent -compact
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"
.Ed
.Ss Source Limiters
Source limiters apply limits on the number of states,
or the rate of state creation,
for connections coming from a source address or network for a set
of rules.
Source limiters are configured and loaded with the main ruleset, but
can be used by rules in any anchor.
The overall number of states is still subject to the limit set with
.Cm set limit states ,
but limits on states for a subset of source addresses and rules can
be provided with source limiters.
.Pp
Source address entries in source pools are created on demand,
and are used to account for the states created for each source
address or network.
A source limiter specifies the maximum number of source address
entries it will track, and can be configured to mask bits in network
prefixes to have source entries cover larger portions of the address
space if needed.
.Pp
A source limiter is configured with the following statement:
.Pp
.Bl -tag -width xxxx -compact
.It Cm source limiter Ar name
Each source limiter is uniquely identified by the specified name.
.El
.Pp
Source limiter support the following configuration:
.Pp
.Bl -tag -width xxxx -compact
.It Cm id Ar number
A unique identifier between 1 and 255.
This configuration is required.
.It Cm entries Ar number
Specify the maximum number of source address entries.
This configuration is required.
.It Cm limit Ar number
Specify the maximum number of states for each source address entry.
This configuration is required.
.It Cm rate Ar number Ns / Ns Ar seconds
Limit the rate at which states can be created by each source address
entry over a time interval.
The connection rate is an approximation calculated as a moving
average.
.It Cm inet mask Ar prefixlen
Mask IPv4 source addresses using the prefix length specified with
.Ar prefixlen
when creating an address entry.
The default IPv4 prefix length is 32 bits.
.It Cm inet6 mask Ar prefixlen
Mask IPv6 source addresses using the prefix length specified with
.Ar prefixlen
when creating an address entry.
The default IPv6 prefix length is 128 bits.
.It Cm table < Ns Ar table Ns > Cm above Ar hwm Op Cm below Ar lwm
Add the address to the specified
.Ar table
when the number of states goes above the
.Ar hwm
high water mark.
The address will be removed from the table when the number of states
drops below the
.Ar lwm
low water mark.
The default low water mark is 0.
.El
.Pp
Pass rules can specify a source limiter using the
.Cm source limiter Ar name
option.
.Pp
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:
.Pp
.Bd -literal -offset indent -compact
source limiter "internet" id 1 entries 10000 \e
limit 1000 rate 100/10 \e
inet6 mask 64
block in on egress
pass in on egress source limiter "internet"
.Ed
.Sh STATEFUL TRACKING OPTIONS
A number of options related to stateful tracking can be applied on a
per-rule basis.
@@ -3457,6 +3614,7 @@ filteropt = user | group | flags | icmp-type | icmp6-type | "tos" tos |
"max-pkt-size" number |
"queue" ( string | "(" string [ [ "," ] string ] ")" ) |
"rtable" number | "probability" number"%" | "prio" number |
"state limiter" name | "source limiter" name |
"dnpipe" ( number | "(" number "," number ")" ) |
"dnqueue" ( number | "(" number "," number ")" ) |
"ridentifier" number |