git: 5b59b0c61e29 - main - pfctl: add -T `reset` to touch pfras_tzero only for non-zero entries

From: Kristof Provost <kp_at_FreeBSD.org>
Date: Mon, 09 Dec 2024 09:39:27 UTC
The branch main has been updated by kp:

URL: https://cgit.FreeBSD.org/src/commit/?id=5b59b0c61e29f684a019afdd2848ffe2d5604e0c

commit 5b59b0c61e29f684a019afdd2848ffe2d5604e0c
Author:     Leonid Evdokimov <leon+freebsd@darkk.net.ru>
AuthorDate: 2024-12-06 12:08:54 +0000
Commit:     Kristof Provost <kp@FreeBSD.org>
CommitDate: 2024-12-09 09:36:34 +0000

    pfctl: add -T `reset` to touch pfras_tzero only for non-zero entries
    
    This will make it easier for scripts to detect idle hosts in tables.
    
    PR:             282984
    Reviewed by:    kp
    MFC after:      2 weeks
---
 sbin/pfctl/pfctl.8            |  7 +++-
 sbin/pfctl/pfctl.c            |  2 +-
 sbin/pfctl/pfctl_radix.c      |  2 +-
 sbin/pfctl/pfctl_table.c      | 44 ++++++++++++++++++++++++
 tests/sys/netpfil/pf/table.sh | 80 +++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 132 insertions(+), 3 deletions(-)

diff --git a/sbin/pfctl/pfctl.8 b/sbin/pfctl/pfctl.8
index 45a6ea525694..00fbda042b7c 100644
--- a/sbin/pfctl/pfctl.8
+++ b/sbin/pfctl/pfctl.8
@@ -24,7 +24,7 @@
 .\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 .\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 .\"
-.Dd November 20, 2024
+.Dd November 25, 2024
 .Dt PFCTL 8
 .Os
 .Sh NAME
@@ -508,6 +508,11 @@ Show the content (addresses) of a table.
 Test if the given addresses match a table.
 .It Fl T Cm zero Op Ar address ...
 Clear all the statistics of a table, or only for specified addresses.
+.It Fl T Cm reset
+Clear statistics only for addresses with non-zero statistics. Addresses
+with counter values at zero and their
+.Dq Cleared
+timestamp are left untouched.
 .It Fl T Cm load
 Load only the table definitions from
 .Xr pf.conf 5 .
diff --git a/sbin/pfctl/pfctl.c b/sbin/pfctl/pfctl.c
index 555c5181eac8..aa3db4619972 100644
--- a/sbin/pfctl/pfctl.c
+++ b/sbin/pfctl/pfctl.c
@@ -239,7 +239,7 @@ static const char * const showopt_list[] = {
 
 static const char * const tblcmdopt_list[] = {
 	"kill", "flush", "add", "delete", "load", "replace", "show",
-	"test", "zero", "expire", NULL
+	"test", "zero", "expire", "reset", NULL
 };
 
 static const char * const debugopt_list[] = {
diff --git a/sbin/pfctl/pfctl_radix.c b/sbin/pfctl/pfctl_radix.c
index 749e8b4affc9..9bea219a7d81 100644
--- a/sbin/pfctl/pfctl_radix.c
+++ b/sbin/pfctl/pfctl_radix.c
@@ -292,7 +292,7 @@ pfr_clr_astats(struct pfr_table *tbl, struct pfr_addr *addr, int size,
 {
 	struct pfioc_table io;
 
-	if (size < 0 || (size && !tbl) || addr == NULL) {
+	if (size < 0 || !tbl || (size && !addr)) {
 		errno = EINVAL;
 		return (-1);
 	}
diff --git a/sbin/pfctl/pfctl_table.c b/sbin/pfctl/pfctl_table.c
index 25d9b87d8718..aac031ce26b2 100644
--- a/sbin/pfctl/pfctl_table.c
+++ b/sbin/pfctl/pfctl_table.c
@@ -60,6 +60,7 @@ static void	print_table(struct pfr_table *, int, int);
 static void	print_tstats(struct pfr_tstats *, int);
 static int	load_addr(struct pfr_buffer *, int, char *[], char *, int);
 static void	print_addrx(struct pfr_addr *, struct pfr_addr *, int);
+static int 	nonzero_astats(struct pfr_astats *);
 static void	print_astats(struct pfr_astats *, int);
 static void	radix_perror(void);
 static void	xprintf(int, const char *, ...);
@@ -293,6 +294,36 @@ pfctl_table(int argc, char *argv[], char *tname, const char *command,
 				if ((opts & PF_OPT_VERBOSE2) || a->pfra_fback)
 					print_addrx(a, NULL,
 					    opts & PF_OPT_USEDNS);
+	} else if (!strcmp(command, "reset")) {
+		struct pfr_astats 	*as;
+
+		b.pfrb_type = PFRB_ASTATS;
+		b2.pfrb_type = PFRB_ADDRS;
+		if (argc || file != NULL)
+			usage();
+		do {
+			pfr_buf_grow(&b, b.pfrb_size);
+			b.pfrb_size = b.pfrb_msize;
+			RVTEST(pfr_get_astats(&table, b.pfrb_caddr,
+			    &b.pfrb_size, flags));
+		} while (b.pfrb_size > b.pfrb_msize);
+		PFRB_FOREACH(as, &b) {
+			as->pfras_a.pfra_fback = 0;
+			if (nonzero_astats(as))
+				if (pfr_buf_add(&b2, &as->pfras_a))
+					err(1, "duplicate buffer");
+		}
+
+		if (opts & PF_OPT_VERBOSE)
+			flags |= PFR_FLAG_FEEDBACK;
+		RVTEST(pfr_clr_astats(&table, b2.pfrb_caddr, b2.pfrb_size,
+		    &nzero, flags));
+		xprintf(opts, "%d/%d stats cleared", nzero, b.pfrb_size);
+		if (opts & PF_OPT_VERBOSE)
+			PFRB_FOREACH(a, &b2)
+				if ((opts & PF_OPT_VERBOSE2) || a->pfra_fback)
+					print_addrx(a, NULL,
+					    opts & PF_OPT_USEDNS);
 	} else if (!strcmp(command, "show")) {
 		b.pfrb_type = (opts & PF_OPT_VERBOSE) ?
 			PFRB_ASTATS : PFRB_ADDRS;
@@ -484,6 +515,19 @@ print_addrx(struct pfr_addr *ad, struct pfr_addr *rad, int dns)
 	printf("\n");
 }
 
+int
+nonzero_astats(struct pfr_astats *as)
+{
+	uint64_t s = 0;
+
+	for (int dir = 0; dir < PFR_DIR_MAX; dir++)
+		for (int op = 0; op < PFR_OP_ADDR_MAX; op++)
+			s |= as->pfras_packets[dir][op] |
+			     as->pfras_bytes[dir][op];
+
+	return (!!s);
+}
+
 void
 print_astats(struct pfr_astats *as, int dns)
 {
diff --git a/tests/sys/netpfil/pf/table.sh b/tests/sys/netpfil/pf/table.sh
index 828d76a998be..62c9d66d80ce 100644
--- a/tests/sys/netpfil/pf/table.sh
+++ b/tests/sys/netpfil/pf/table.sh
@@ -165,6 +165,85 @@ zero_one_cleanup()
 	pft_cleanup
 }
 
+atf_test_case "reset_nonzero" "cleanup"
+reset_nonzero_head()
+{
+	atf_set descr 'Test zeroing an address with non-zero counters'
+	atf_set require.user root
+}
+
+reset_nonzero_body()
+{
+	epair_send=$(vnet_mkepair)
+	ifconfig ${epair_send}a 192.0.2.1/24 up
+	ifconfig ${epair_send}a inet alias 192.0.2.3/24
+
+	vnet_mkjail alcatraz ${epair_send}b
+	jexec alcatraz ifconfig ${epair_send}b 192.0.2.2/24 up
+	jexec alcatraz pfctl -e
+
+	pft_set_rules alcatraz \
+	    "table <foo> counters { 192.0.2.1, 192.0.2.3 }" \
+	    "table <bar> counters { }" \
+	    "block all" \
+	    "pass in from <foo> to any" \
+	    "pass out from any to <foo>" \
+	    "pass on notReallyAnIf from <bar> to <bar>" \
+	    "set skip on lo"
+
+	# Nonexisting table can't be reset, following `-T show`.
+	atf_check -o ignore \
+	    -s not-exit:0 \
+	    -e inline:"pfctl: Table does not exist.\n" \
+	    jexec alcatraz pfctl -t nonexistent -T reset
+
+	atf_check -o ignore \
+	    -s exit:0 \
+	    -e inline:"0/0 stats cleared.\n" \
+	    jexec alcatraz pfctl -t bar -T reset
+
+	# No-op is a valid operation.
+	atf_check -s exit:0 \
+	    -e inline:"0/2 stats cleared.\n" \
+	    jexec alcatraz pfctl -t foo -T reset
+
+	atf_check -s exit:0 -o ignore ping -c 3 -S 192.0.2.3 192.0.2.2
+
+	atf_check -s exit:0 -e ignore \
+	    -o match:'In/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \
+	    -o match:'In/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \
+	    -o match:'Out/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \
+	    -o match:'Out/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \
+	    jexec alcatraz pfctl -t foo -vvT show
+
+	local clrd uniq
+	clrd=`jexec alcatraz pfctl -t foo -vvT show | grep -c Cleared`
+	uniq=`jexec alcatraz pfctl -t foo -vvT show | sort -u | grep -c Cleared`
+	atf_check_equal "$clrd" 2
+	atf_check_equal "$uniq" 1 # time they were added
+
+	atf_check -s exit:0 -e ignore \
+	    -e inline:"1/2 stats cleared.\n" \
+	    jexec alcatraz pfctl -t foo -T reset
+
+	clrd=`jexec alcatraz pfctl -t foo -vvT show | grep -c Cleared`
+	uniq=`jexec alcatraz pfctl -t foo -vvT show | sort -u | grep -c Cleared`
+	atf_check_equal "$clrd" 2
+	atf_check_equal "$uniq" 2 # 192.0.2.3 should get new timestamp
+
+	atf_check -s exit:0 -e ignore \
+	    -o not-match:'In/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \
+	    -o not-match:'Out/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \
+	    -o match:'In/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \
+	    -o match:'Out/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \
+	    jexec alcatraz pfctl -t foo -vvT show
+}
+
+reset_nonzero_cleanup()
+{
+	pft_cleanup
+}
+
 atf_test_case "pr251414" "cleanup"
 pr251414_head()
 {
@@ -381,6 +460,7 @@ atf_init_test_cases()
 	atf_add_test_case "v4_counters"
 	atf_add_test_case "v6_counters"
 	atf_add_test_case "zero_one"
+	atf_add_test_case "reset_nonzero"
 	atf_add_test_case "pr251414"
 	atf_add_test_case "automatic"
 	atf_add_test_case "network"