The branch main has been updated by kp: URL: https://cgit.FreeBSD.org/src/commit/?id=41fd03c08f67fc9c891f4fb0ebf912658f30f212
commit 41fd03c08f67fc9c891f4fb0ebf912658f30f212 Author: Kristof Provost <k...@freebsd.org> AuthorDate: 2025-06-06 14:45:43 +0000 Commit: Kristof Provost <k...@freebsd.org> CommitDate: 2025-06-27 14:55:15 +0000 pf: add 'max-pkt-size' Allow pf to limit packets to a specified maximum size. This applies to all packets, and if reassembly is enabled, looks at the reassembled size, not the size of individual fragments. Sponsored by: Rubicon Communications, LLC ("Netgate") --- lib/libpfctl/libpfctl.c | 2 + lib/libpfctl/libpfctl.h | 1 + sbin/pfctl/parse.y | 13 +++++- sbin/pfctl/pfctl_parser.c | 2 + sbin/pfctl/tests/files/pf1069.in | 1 + sbin/pfctl/tests/files/pf1069.ok | 1 + sbin/pfctl/tests/pfctl_test_list.inc | 1 + share/man/man5/pf.conf.5 | 4 ++ sys/net/pfvar.h | 1 + sys/netpfil/pf/pf.c | 8 ++++ sys/netpfil/pf/pf_nl.c | 2 + sys/netpfil/pf/pf_nl.h | 1 + tests/sys/netpfil/pf/Makefile | 1 + tests/sys/netpfil/pf/max_pkt_size.sh | 85 ++++++++++++++++++++++++++++++++++++ 14 files changed, 122 insertions(+), 1 deletion(-) diff --git a/lib/libpfctl/libpfctl.c b/lib/libpfctl/libpfctl.c index 4789448d2a37..e4123fe02211 100644 --- a/lib/libpfctl/libpfctl.c +++ b/lib/libpfctl/libpfctl.c @@ -1251,6 +1251,7 @@ snl_add_msg_attr_pf_rule(struct snl_writer *nw, uint32_t type, const struct pfct snl_add_msg_attr_u32(nw, PF_RT_MAX_SRC_CONN, r->max_src_conn); snl_add_msg_attr_u32(nw, PF_RT_MAX_SRC_CONN_RATE_LIMIT, r->max_src_conn_rate.limit); snl_add_msg_attr_u32(nw, PF_RT_MAX_SRC_CONN_RATE_SECS, r->max_src_conn_rate.seconds); + snl_add_msg_attr_u16(nw, PF_RT_MAX_PKT_SIZE, r->max_pkt_size); snl_add_msg_attr_u16(nw, PF_RT_DNPIPE, r->dnpipe); snl_add_msg_attr_u16(nw, PF_RT_DNRPIPE, r->dnrpipe); @@ -1692,6 +1693,7 @@ static struct snl_attr_parser ap_getrule[] = { { .type = PF_RT_SRC_NODES_NAT, .off = _OUT(r.src_nodes_type[PF_SN_NAT]), .cb = snl_attr_get_uint64 }, { .type = PF_RT_SRC_NODES_ROUTE, .off = _OUT(r.src_nodes_type[PF_SN_ROUTE]), .cb = snl_attr_get_uint64 }, { .type = PF_RT_PKTRATE, .off = _OUT(r.pktrate), .arg = &pfctl_threshold_parser, .cb = snl_attr_get_nested }, + { .type = PF_RT_MAX_PKT_SIZE, .off =_OUT(r.max_pkt_size), .cb = snl_attr_get_uint16 }, }; #undef _OUT SNL_DECLARE_PARSER(getrule_parser, struct genlmsghdr, snl_f_p_empty, ap_getrule); diff --git a/lib/libpfctl/libpfctl.h b/lib/libpfctl/libpfctl.h index 7de7a08e90bf..98a80758ca74 100644 --- a/lib/libpfctl/libpfctl.h +++ b/lib/libpfctl/libpfctl.h @@ -211,6 +211,7 @@ struct pfctl_rule { uint32_t limit; uint32_t seconds; } max_src_conn_rate; + uint16_t max_pkt_size; uint32_t qid; uint32_t pqid; uint16_t dnpipe; diff --git a/sbin/pfctl/parse.y b/sbin/pfctl/parse.y index e0bd5ce4aee0..257a62df76f4 100644 --- a/sbin/pfctl/parse.y +++ b/sbin/pfctl/parse.y @@ -312,6 +312,7 @@ static struct filter_opts { uint32_t limit; uint32_t seconds; } pktrate; + int max_pkt_size; } filter_opts; static struct antispoof_opts { @@ -535,7 +536,7 @@ int parseport(char *, struct range *r, int); %token MAXSRCCONN MAXSRCCONNRATE OVERLOAD FLUSH SLOPPY PFLOW ALLOW_RELATED %token TAGGED TAG IFBOUND FLOATING STATEPOLICY STATEDEFAULTS ROUTE SETTOS %token DIVERTTO DIVERTREPLY BRIDGE_TO RECEIVEDON NE LE GE AFTO NATTO RDRTO -%token BINATTO MAXPKTRATE +%token BINATTO MAXPKTRATE MAXPKTSIZE %token <v.string> STRING %token <v.number> NUMBER %token <v.i> PORTBINARY @@ -1020,6 +1021,7 @@ anchorrule : ANCHOR anchorname dir quick interface af proto fromto r.ridentifier = $9.ridentifier; r.pktrate.limit = $9.pktrate.limit; r.pktrate.seconds = $9.pktrate.seconds; + r.max_pkt_size = $9.max_pkt_size; if ($9.tag) if (strlcpy(r.tagname, $9.tag, @@ -2499,6 +2501,7 @@ pfrule : action dir logquick interface route af proto fromto r.keep_state = $9.keep.action; r.pktrate.limit = $9.pktrate.limit; r.pktrate.seconds = $9.pktrate.seconds; + r.max_pkt_size = $9.max_pkt_size; o = $9.keep.options; /* 'keep state' by default on pass rules. */ @@ -3135,6 +3138,13 @@ filter_opt : USER uids { filter_opts.pktrate.limit = $2; filter_opts.pktrate.seconds = $4; } + | MAXPKTSIZE NUMBER { + if ($2 < 0 || $2 > UINT16_MAX) { + yyerror("only positive values permitted"); + YYERROR; + } + filter_opts.max_pkt_size = $2; + } | filter_sets ; @@ -6721,6 +6731,7 @@ lookup(char *s) { "max", MAXIMUM}, { "max-mss", MAXMSS}, { "max-pkt-rate", MAXPKTRATE}, + { "max-pkt-size", MAXPKTSIZE}, { "max-src-conn", MAXSRCCONN}, { "max-src-conn-rate", MAXSRCCONNRATE}, { "max-src-nodes", MAXSRCNODES}, diff --git a/sbin/pfctl/pfctl_parser.c b/sbin/pfctl/pfctl_parser.c index 32e98eb20b7c..dbc7ff00782f 100644 --- a/sbin/pfctl/pfctl_parser.c +++ b/sbin/pfctl/pfctl_parser.c @@ -1010,6 +1010,8 @@ print_rule(struct pfctl_rule *r, const char *anchor_call, int verbose, int numer if (r->pktrate.limit) printf(" max-pkt-rate %u/%u", r->pktrate.limit, r->pktrate.seconds); + if (r->max_pkt_size) + printf( " max-pkt-size %u", r->max_pkt_size); if (r->scrub_flags & PFSTATE_SETMASK) { char *comma = ""; printf(" set ("); diff --git a/sbin/pfctl/tests/files/pf1069.in b/sbin/pfctl/tests/files/pf1069.in new file mode 100644 index 000000000000..3a69158fff7e --- /dev/null +++ b/sbin/pfctl/tests/files/pf1069.in @@ -0,0 +1 @@ +pass in proto icmp max-pkt-size 128 diff --git a/sbin/pfctl/tests/files/pf1069.ok b/sbin/pfctl/tests/files/pf1069.ok new file mode 100644 index 000000000000..b79228266156 --- /dev/null +++ b/sbin/pfctl/tests/files/pf1069.ok @@ -0,0 +1 @@ +pass in proto icmp all max-pkt-size 128 keep state diff --git a/sbin/pfctl/tests/pfctl_test_list.inc b/sbin/pfctl/tests/pfctl_test_list.inc index 2b237ed0922e..f798fedb70a8 100644 --- a/sbin/pfctl/tests/pfctl_test_list.inc +++ b/sbin/pfctl/tests/pfctl_test_list.inc @@ -177,3 +177,4 @@ PFCTL_TEST(1065, "no nat") PFCTL_TEST(1066, "no rdr") PFCTL_TEST_FAIL(1067, "route-to can't be used on block rules") PFCTL_TEST(1068, "max-pkt-rate") +PFCTL_TEST(1069, "max-pkt-size") diff --git a/share/man/man5/pf.conf.5 b/share/man/man5/pf.conf.5 index 49c81f51294c..5d802f81984f 100644 --- a/share/man/man5/pf.conf.5 +++ b/share/man/man5/pf.conf.5 @@ -2227,6 +2227,9 @@ pass in proto icmp max-pkt-rate 100/10 When the rate is exceeded, all ICMP is blocked until the rate falls below 100 per 10 seconds again. .Pp +.It Ar max-pkt-size Aq Ar number +Limit each packet to be no more than the specified number of bytes. +This includes the IP header, but not any layer 2 header. .It Xo Ar queue Aq Ar queue .No \*(Ba ( Aq Ar queue , .Aq Ar queue ) @@ -3401,6 +3404,7 @@ filteropt = user | group | flags | icmp-type | icmp6-type | "tos" tos | "label" string | "tag" string | [ "!" ] "tagged" string | "max-pkt-rate" number "/" seconds | "set prio" ( number | "(" number [ [ "," ] number ] ")" ) | + "max-pkt-size" number | "queue" ( string | "(" string [ [ "," ] string ] ")" ) | "rtable" number | "probability" number"%" | "prio" number | "dnpipe" ( number | "(" number "," number ")" ) | diff --git a/sys/net/pfvar.h b/sys/net/pfvar.h index 33574dbd5c2a..5c798216f9f5 100644 --- a/sys/net/pfvar.h +++ b/sys/net/pfvar.h @@ -845,6 +845,7 @@ struct pf_krule { u_int32_t limit; u_int32_t seconds; } max_src_conn_rate; + uint16_t max_pkt_size; u_int16_t qid; u_int16_t pqid; u_int16_t dnpipe; diff --git a/sys/netpfil/pf/pf.c b/sys/netpfil/pf/pf.c index 6533b06c5d9d..6da537aaa2cd 100644 --- a/sys/netpfil/pf/pf.c +++ b/sys/netpfil/pf/pf.c @@ -10677,6 +10677,14 @@ done: ("pf: dropping packet with dangerous headers\n")); } + if (r && r->max_pkt_size && pd.tot_len > r->max_pkt_size) { + action = PF_DROP; + REASON_SET(&reason, PFRES_NORM); + pd.act.log = PF_LOG_FORCE; + DPFPRINTF(PF_DEBUG_MISC, + ("pf: dropping overly long packet\n")); + } + if (s) { uint8_t log = pd.act.log; memcpy(&pd.act, &s->act, sizeof(struct pf_rule_actions)); diff --git a/sys/netpfil/pf/pf_nl.c b/sys/netpfil/pf/pf_nl.c index 48cba96b04b0..381e966eacf1 100644 --- a/sys/netpfil/pf/pf_nl.c +++ b/sys/netpfil/pf/pf_nl.c @@ -761,6 +761,7 @@ static const struct nlattr_parser nla_p_rule[] = { { .type = PF_RT_RPOOL_RT, .off = _OUT(route), .arg = &pool_parser, .cb = nlattr_get_nested }, { .type = PF_RT_RCV_IFNOT, .off = _OUT(rcvifnot), .cb = nlattr_get_bool }, { .type = PF_RT_PKTRATE, .off = _OUT(pktrate), .arg = &threshold_parser, .cb = nlattr_get_nested }, + { .type = PF_RT_MAX_PKT_SIZE, .off = _OUT(max_pkt_size), .cb = nlattr_get_uint16 }, }; NL_DECLARE_ATTR_PARSER(rule_parser, nla_p_rule); #undef _OUT @@ -945,6 +946,7 @@ pf_handle_getrule(struct nlmsghdr *hdr, struct nl_pstate *npt) nlattr_add_u32(nw, PF_RT_MAX_SRC_CONN, rule->max_src_conn); nlattr_add_u32(nw, PF_RT_MAX_SRC_CONN_RATE_LIMIT, rule->max_src_conn_rate.limit); nlattr_add_u32(nw, PF_RT_MAX_SRC_CONN_RATE_SECS, rule->max_src_conn_rate.seconds); + nlattr_add_u16(nw, PF_RT_MAX_PKT_SIZE, rule->max_pkt_size); nlattr_add_u16(nw, PF_RT_DNPIPE, rule->dnpipe); nlattr_add_u16(nw, PF_RT_DNRPIPE, rule->dnrpipe); diff --git a/sys/netpfil/pf/pf_nl.h b/sys/netpfil/pf/pf_nl.h index 97ef574995f5..929c20e4c582 100644 --- a/sys/netpfil/pf/pf_nl.h +++ b/sys/netpfil/pf/pf_nl.h @@ -279,6 +279,7 @@ enum pf_rule_type_t { PF_RT_SRC_NODES_NAT = 80, /* u64 */ PF_RT_SRC_NODES_ROUTE = 81, /* u64 */ PF_RT_PKTRATE = 82, /* nested, pf_threshold_type_t */ + PF_RT_MAX_PKT_SIZE = 83, /* u16 */ }; enum pf_addrule_type_t { diff --git a/tests/sys/netpfil/pf/Makefile b/tests/sys/netpfil/pf/Makefile index fe2740ed0e7f..3adaef09ddbd 100644 --- a/tests/sys/netpfil/pf/Makefile +++ b/tests/sys/netpfil/pf/Makefile @@ -23,6 +23,7 @@ ATF_TESTS_SH+= altq \ macro \ match \ max_pkt_rate \ + max_pkt_size \ max_states \ mbuf \ modulate \ diff --git a/tests/sys/netpfil/pf/max_pkt_size.sh b/tests/sys/netpfil/pf/max_pkt_size.sh new file mode 100644 index 000000000000..05aab0b7990a --- /dev/null +++ b/tests/sys/netpfil/pf/max_pkt_size.sh @@ -0,0 +1,85 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2025 Rubicon Communications, LLC (Netgate) +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +. $(atf_get_srcdir)/utils.subr + +atf_test_case "basic" "cleanup" +basic_head() +{ + atf_set descr 'Basic max-pkt-size test' + atf_set require.user root +} + +basic_body() +{ + pft_init + + epair=$(vnet_mkepair) + + ifconfig ${epair}b 192.0.2.2/24 up + + vnet_mkjail alcatraz ${epair}a + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "pass max-pkt-size 128" + + # Small packets pass + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + atf_check -s exit:0 -o ignore \ + ping -c 1 -s 100 192.0.2.1 + + # Larger packets do not + atf_check -s exit:2 -o ignore \ + ping -c 3 -s 101 192.0.2.1 + atf_check -s exit:2 -o ignore \ + ping -c 3 -s 128 192.0.2.1 + + # We can enforce this on fragmented packets too + pft_set_rules alcatraz \ + "pass max-pkt-size 2000" + + atf_check -s exit:0 -o ignore \ + ping -c 1 -s 1400 192.0.2.1 + atf_check -s exit:0 -o ignore \ + ping -c 1 -s 1972 192.0.2.1 + atf_check -s exit:2 -o ignore \ + ping -c 1 -s 1973 192.0.2.1 + atf_check -s exit:2 -o ignore \ + ping -c 3 -s 3000 192.0.2.1 +} + +basic_cleanup() +{ + pft_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case "basic" +}