All of lore.kernel.org
 help / color / mirror / Atom feed
From: Valery Borovsky <vebohr@gmail.com>
To: pablo@netfilter.org, fw@strlen.de
Cc: netfilter-devel@vger.kernel.org, linux-kselftest@vger.kernel.org,
	shuah@kernel.org, linux-kernel@vger.kernel.org,
	Valery Borovsky <vebohr@gmail.com>
Subject: [PATCH] selftests: netfilter: add functional test for nft ct timeout policies
Date: Wed,  5 Aug 2026 09:05:07 +0300	[thread overview]
Message-ID: <20260805060507.777983-1-vebohr@gmail.com> (raw)

There is no selftest coverage for nftables ct timeout objects: no test
under tools/testing/selftests/net/netfilter/ exercises "ct timeout set"
or checks that a named policy reaches the conntrack state machine.

Add nft_ct_timeout.sh with three subtests, using two netns joined by a
veth pair and reading the remaining flow lifetime from "conntrack -L":

  1. test_policy_applied: a TCP connection matched by a "ct timeout set"
     rule gets the ESTABLISHED timeout carried by the policy (120s here)
     instead of the kernel default, which is 5 days for that state (see
     nf_conntrack_proto_tcp.c, tcp_timeouts[TCP_CONNTRACK_ESTABLISHED]).

  2. test_default_preserved: a connection on a port not covered by any
     rule keeps the default. The expected value is read from
     nf_conntrack_tcp_timeout_established rather than hardcoded, so a
     host that lowered the sysctl does not fail the test spuriously.

  3. test_policy_reload: after the table is deleted and reloaded, a new
     connection through the rule receives the policy timeout again,
     covering the create/assign/destroy/re-create object lifecycle.

The test checks kernel behaviour rather than the parsing round-trip,
which the nftables.git functional tests already cover.

Signed-off-by: Valery Borovsky <vebohr@gmail.com>
---
This replaces the nft_ct timeout regression test posted in April:
https://lore.kernel.org/netfilter-devel/20260422131818.106417-1-vebohr@gmail.com/

Florian's review of that patch asked for a functional test for timeout
policies rather than a regression script, and noted that the nftables.git
tests only cover the parsing side. This is that test: it checks that the
policy reaches the conntrack state machine, and does not re-check parsing.
The executable bit is set this time.

Test output on x86_64 (nft 1.1.6, conntrack-tools 1.4.9):

  PASS: test_policy_applied: timeout 119s <= policy 120s
  PASS: test_default_preserved: timeout 431999s matches default 432000s
  PASS: test_policy_reload: timeout 119s <= policy 120s

 .../testing/selftests/net/netfilter/Makefile  |   1 +
 .../selftests/net/netfilter/nft_ct_timeout.sh | 217 ++++++++++++++++++
 2 files changed, 218 insertions(+)
 create mode 100755 tools/testing/selftests/net/netfilter/nft_ct_timeout.sh

diff --git a/tools/testing/selftests/net/netfilter/Makefile b/tools/testing/selftests/net/netfilter/Makefile
index f88dd4ef8d26..39101af5d84a 100644
--- a/tools/testing/selftests/net/netfilter/Makefile
+++ b/tools/testing/selftests/net/netfilter/Makefile
@@ -25,6 +25,7 @@ TEST_PROGS := \
 	nft_audit.sh \
 	nft_concat_range.sh \
 	nft_conntrack_helper.sh \
+	nft_ct_timeout.sh \
 	nft_fib.sh \
 	nft_fib_nexthop.sh \
 	nft_flowtable.sh \
diff --git a/tools/testing/selftests/net/netfilter/nft_ct_timeout.sh b/tools/testing/selftests/net/netfilter/nft_ct_timeout.sh
new file mode 100755
index 000000000000..a41dad2329cc
--- /dev/null
+++ b/tools/testing/selftests/net/netfilter/nft_ct_timeout.sh
@@ -0,0 +1,217 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Functional test for nftables ct timeout objects.
+#
+# Verifies that a ct timeout policy actually changes the conntrack
+# state-machine timeout of the connections it is attached to:
+#
+#  1. test_policy_applied    - a connection matching a "ct timeout set" rule
+#                              gets the per-state timeout from the named
+#                              policy, i.e. <= POLICY_ESTAB_SECS.
+#
+#  2. test_default_preserved - a connection not matching any ct timeout rule
+#                              keeps the kernel default ESTABLISHED timeout.
+#
+#  3. test_policy_reload     - after the table is deleted and reloaded, a new
+#                              connection through the rule still receives the
+#                              policy timeout rather than the default.
+#
+# The remaining lifetime of a flow is the third field of a "conntrack -L"
+# line:
+#
+#   tcp  6  117  ESTABLISHED  src=10.0.1.1 dst=10.0.1.2 sport=... dport=...
+#           ^^^
+#
+# Topology: two netns connected by a veth pair.
+#   ns1 (10.0.1.1/24) --veth-- ns2 (10.0.1.2/24)
+# The ruleset is loaded in ns1, so ns1's conntrack table is used throughout.
+
+source lib.sh
+
+checktool "nft --version" "run test without nft"
+checktool "conntrack --version" "run test without conntrack"
+checktool "socat -h" "run test without socat"
+
+# TCP port covered by the ct timeout policy.
+PORT_POLICY=12345
+# TCP port not covered by any rule; uses the kernel default timeouts.
+PORT_DEFAULT=12346
+
+# ESTABLISHED timeout carried by the policy, in seconds. Must be well below
+# the kernel default (5 days) so the two cases cannot be confused.
+POLICY_ESTAB_SECS=120
+
+ret=0
+
+cleanup()
+{
+	ip netns pids "$ns1" 2>/dev/null | xargs -r kill
+	ip netns pids "$ns2" 2>/dev/null | xargs -r kill
+	cleanup_all_ns
+}
+
+load_ruleset()
+{
+	ip netns exec "$ns1" nft -f - <<EOF
+table ip ct_timeout_test {
+	ct timeout tcp_policy {
+		protocol tcp;
+		policy = { established: ${POLICY_ESTAB_SECS}s };
+	}
+
+	chain output {
+		type filter hook output priority filter; policy accept;
+		ip daddr 10.0.1.2 tcp dport $PORT_POLICY ct timeout set "tcp_policy"
+	}
+}
+EOF
+}
+
+listener_ready()
+{
+	ip netns exec "$ns2" ss -lnt -o "sport = :$1" | grep -q "$1"
+}
+
+conn_established()
+{
+	ip netns exec "$ns1" conntrack -L -p tcp --dport "$1" 2>/dev/null |
+		grep -q ESTABLISHED
+}
+
+# Prints the remaining lifetime, in seconds, of the first ESTABLISHED TCP
+# flow in ns1 matching the given destination port.
+established_timeout()
+{
+	ip netns exec "$ns1" conntrack -L -p tcp --dport "$1" 2>/dev/null |
+		awk '/ESTABLISHED/ { print $3; exit }'
+}
+
+# Opens a TCP connection from ns1 and holds it open in the background.
+# Prints the pid of the holder so the caller can tear it down.
+open_connection()
+{
+	# stdout/stderr must be redirected, else the background job keeps the
+	# command substitution that calls this function blocked until it exits.
+	ip netns exec "$ns1" bash -c "
+		exec 3<>/dev/tcp/10.0.1.2/$1 || exit 1
+		sleep 60
+	" >/dev/null 2>&1 &
+	echo $!
+}
+
+close_connection()
+{
+	kill "$1" 2>/dev/null
+	wait "$1" 2>/dev/null
+}
+
+# Asserts that the flow on $1 has a timeout matching expectation $2 ("policy"
+# or "default"), using $3 as the test name.
+check_timeout()
+{
+	local dport=$1 expect=$2 name=$3
+	local tval
+
+	if ! busywait "$BUSYWAIT_TIMEOUT" conn_established "$dport"; then
+		echo "FAIL: $name: connection did not reach ESTABLISHED"
+		ret=1
+		return
+	fi
+
+	tval=$(established_timeout "$dport")
+	if [ -z "$tval" ]; then
+		echo "FAIL: $name: could not read conntrack timeout"
+		ret=1
+		return
+	fi
+
+	if [ "$expect" = "policy" ]; then
+		if [ "$tval" -le "$POLICY_ESTAB_SECS" ]; then
+			echo "PASS: $name: timeout ${tval}s <= policy ${POLICY_ESTAB_SECS}s"
+		else
+			echo "FAIL: $name: timeout ${tval}s exceeds policy ${POLICY_ESTAB_SECS}s"
+			ret=1
+		fi
+	else
+		if [ "$tval" -gt "$POLICY_ESTAB_SECS" ] &&
+		   [ "$tval" -le "$default_estab" ]; then
+			echo "PASS: $name: timeout ${tval}s matches default ${default_estab}s"
+		else
+			echo "FAIL: $name: timeout ${tval}s is not the default ${default_estab}s"
+			ret=1
+		fi
+	fi
+}
+
+trap cleanup EXIT
+
+setup_ns ns1 ns2
+
+if ! ip link add veth0 netns "$ns1" type veth peer name veth0 netns "$ns2" \
+		>/dev/null 2>&1; then
+	echo "SKIP: No virtual ethernet pair device support in kernel"
+	exit $ksft_skip
+fi
+
+ip -net "$ns1" link set veth0 up
+ip -net "$ns2" link set veth0 up
+ip -net "$ns1" addr add 10.0.1.1/24 dev veth0
+ip -net "$ns2" addr add 10.0.1.2/24 dev veth0
+
+# SYSTEM:"cat" keeps each accepted connection open: cat blocks reading the
+# socket, so the flow stays ESTABLISHED until the client goes away. Sinking
+# to /dev/null instead would close it immediately and land in CLOSE_WAIT.
+ip netns exec "$ns2" socat TCP-LISTEN:$PORT_POLICY,reuseaddr,fork SYSTEM:"cat" &>/dev/null &
+ip netns exec "$ns2" socat TCP-LISTEN:$PORT_DEFAULT,reuseaddr,fork SYSTEM:"cat" &>/dev/null &
+
+busywait "$BUSYWAIT_TIMEOUT" listener_ready "$PORT_POLICY"
+busywait "$BUSYWAIT_TIMEOUT" listener_ready "$PORT_DEFAULT"
+
+# Loading the ruleset pulls in conntrack, so the sysctls below exist only
+# after this point.
+if ! load_ruleset; then
+	echo "SKIP: Could not load ct timeout ruleset"
+	exit $ksft_skip
+fi
+
+# Read the default rather than hardcoding it: a host that lowered
+# nf_conntrack_tcp_timeout_established must not fail test 2 spuriously.
+default_estab=$(ip netns exec "$ns1" \
+	cat /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established \
+	2>/dev/null)
+
+if [ -z "$default_estab" ]; then
+	echo "SKIP: conntrack tcp timeout sysctl not available"
+	exit $ksft_skip
+fi
+
+if [ "$default_estab" -le "$POLICY_ESTAB_SECS" ]; then
+	echo "SKIP: default ESTABLISHED timeout ${default_estab}s is not above the policy value"
+	exit $ksft_skip
+fi
+
+# Test 1: a flow matched by the rule gets the policy timeout.
+conn_pid=$(open_connection "$PORT_POLICY")
+check_timeout "$PORT_POLICY" policy test_policy_applied
+close_connection "$conn_pid"
+
+# Test 2: a flow not matched by any rule keeps the kernel default.
+conn_pid=$(open_connection "$PORT_DEFAULT")
+check_timeout "$PORT_DEFAULT" default test_default_preserved
+close_connection "$conn_pid"
+
+# Test 3: the policy survives a delete/reload cycle of the whole table.
+ip netns exec "$ns1" conntrack -F 2>/dev/null
+ip netns exec "$ns1" nft delete table ip ct_timeout_test
+
+if ! load_ruleset; then
+	echo "FAIL: test_policy_reload: could not reload ct timeout ruleset"
+	exit $ksft_fail
+fi
+
+conn_pid=$(open_connection "$PORT_POLICY")
+check_timeout "$PORT_POLICY" policy test_policy_reload
+close_connection "$conn_pid"
+
+exit $ret
-- 
2.53.0


                 reply	other threads:[~2026-08-05  6:05 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260805060507.777983-1-vebohr@gmail.com \
    --to=vebohr@gmail.com \
    --cc=fw@strlen.de \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=netfilter-devel@vger.kernel.org \
    --cc=pablo@netfilter.org \
    --cc=shuah@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.