From: Pablo Neira Ayuso <pablo@netfilter.org>
To: netfilter-devel@vger.kernel.org
Cc: davem@davemloft.net, netdev@vger.kernel.org, kuba@kernel.org,
pabeni@redhat.com, edumazet@kernel.org, horms@kernel.org,
fw@strlen.de, ja@ssi.bg
Subject: [PATCH net-next 15/15] selftests: netfilter: add UDP flowtable offload test
Date: Fri, 13 Nov 2026 04:12:51 +0100 [thread overview]
Message-ID: <20261113031251.2437074-16-pablo@netfilter.org> (raw)
In-Reply-To: <20261113031251.2437074-1-pablo@netfilter.org>
From: Jiri Peska <jpeska@redhat.com>
nft_flowtable.sh offloads only TCP, so the UDP flowtable paths never
run: the UDP tuple lookup, the UDP checksum update, IPv6 source NAT,
and nf_ct_acct_update behind the flowtable counter flag.
Add an echo test for IPv4 and IPv6, with and without masquerade. The
payload must come back intact, the forward chain must see only the
handshake, and conntrack must mark the flow OFFLOAD and account every
datagram.
Signed-off-by: Jiri Peska <jpeska@redhat.com>
Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
---
.../testing/selftests/net/netfilter/Makefile | 1 +
.../net/netfilter/nft_flowtable_udp.sh | 238 ++++++++++++++++++
2 files changed, 239 insertions(+)
create mode 100755 tools/testing/selftests/net/netfilter/nft_flowtable_udp.sh
diff --git a/tools/testing/selftests/net/netfilter/Makefile b/tools/testing/selftests/net/netfilter/Makefile
index df3c20c90f5d..1bc7f7a75f1e 100644
--- a/tools/testing/selftests/net/netfilter/Makefile
+++ b/tools/testing/selftests/net/netfilter/Makefile
@@ -30,6 +30,7 @@ TEST_PROGS := \
nft_fib.sh \
nft_fib_nexthop.sh \
nft_flowtable.sh \
+ nft_flowtable_udp.sh \
nft_interface_stress.sh \
nft_meta.sh \
nft_nat.sh \
diff --git a/tools/testing/selftests/net/netfilter/nft_flowtable_udp.sh b/tools/testing/selftests/net/netfilter/nft_flowtable_udp.sh
new file mode 100755
index 000000000000..e0f05c76bd8e
--- /dev/null
+++ b/tools/testing/selftests/net/netfilter/nft_flowtable_udp.sh
@@ -0,0 +1,238 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# nft_flowtable.sh offloads TCP only. These UDP paths therefore stay at
+# zero coverage in that run:
+#
+# nf_flow_tuple_ip / nf_flow_tuple_ipv6 IPPROTO_UDP
+# nf_flow_nat_ip_udp / nf_flow_nat_port_udp IPv4 NAT checksum
+# nf_flow_snat_ipv6 / nf_flow_dnat_ipv6 IPv6 NAT, including UDP
+# flow_offload_get_timeout / flow_offload_fixup_ct for UDP
+# nf_ct_acct_update when the flowtable counter flag is set
+#
+# Forward a burst of UDP echoes, with and without masquerade. The forward
+# hook must see only the handshake: the rest is carried by the ingress
+# flowtable, and conntrack accounting must include those packets.
+
+source lib.sh
+
+ret=0
+port=12345
+count=32
+
+checktool "nft --version" "run test without nft"
+checktool "socat -h" "run test without socat"
+
+setup_ns ns1 nsr ns2
+
+cleanup() {
+ cleanup_all_ns
+}
+trap cleanup EXIT
+
+# Disable DAD before the devices exist. A link-local still running DAD
+# delays the first IPv6 packet by about two seconds.
+for ns in "$ns1" "$ns2" "$nsr"; do
+ ip netns exec "$ns" sysctl -wq net.ipv6.conf.default.accept_dad=0
+ ip netns exec "$ns" sysctl -wq net.ipv6.conf.default.dad_transmits=0
+ ip netns exec "$ns" sysctl -wq net.ipv6.conf.all.accept_dad=0
+done
+
+ip link add veth0 netns "$nsr" type veth peer name eth0 netns "$ns1"
+ip link add veth1 netns "$nsr" type veth peer name eth0 netns "$ns2"
+
+ip -net "$nsr" link set veth0 up
+ip -net "$nsr" link set veth1 up
+ip -net "$ns1" link set eth0 up
+ip -net "$ns2" link set eth0 up
+
+ip netns exec "$nsr" sysctl -wq net.ipv4.ip_forward=1
+ip netns exec "$nsr" sysctl -wq net.ipv6.conf.all.forwarding=1
+
+ip -net "$nsr" addr add 10.0.1.1/24 dev veth0
+ip -net "$nsr" addr add 10.0.2.1/24 dev veth1
+ip -net "$ns1" addr add 10.0.1.99/24 dev eth0
+ip -net "$ns2" addr add 10.0.2.99/24 dev eth0
+
+ip -net "$nsr" addr add dead:1::1/64 dev veth0 nodad
+ip -net "$nsr" addr add dead:2::1/64 dev veth1 nodad
+ip -net "$ns1" addr add dead:1::99/64 dev eth0 nodad
+ip -net "$ns2" addr add dead:2::99/64 dev eth0 nodad
+
+ip -net "$ns1" route add default via 10.0.1.1
+ip -net "$ns2" route add default via 10.0.2.1
+ip -6 -net "$ns1" route add default via dead:1::1
+ip -6 -net "$ns2" route add default via dead:2::1
+
+if ! ip netns exec "$nsr" nft -f - <<EOF
+table inet filter {
+ flowtable f1 {
+ hook ingress priority 0
+ devices = { veth0, veth1 }
+ counter
+ }
+
+ counter slow {}
+
+ chain forward {
+ type filter hook forward priority 0; policy accept;
+
+ # Installing the flow does not stop this packet. Later
+ # packets of the same flow are taken at ingress and never
+ # reach this chain, so "slow" counts only the handshake.
+ meta l4proto udp ct state established,related flow add @f1
+ meta l4proto udp counter name slow
+ }
+}
+
+table inet nat {
+ chain postrouting {
+ type nat hook postrouting priority 100; policy accept;
+ }
+}
+EOF
+then
+ echo "SKIP: Could not load nft ruleset"
+ exit "$ksft_skip"
+fi
+
+ip netns exec "$nsr" sysctl -wq net.netfilter.nf_conntrack_acct=1
+
+if ! ip netns exec "$ns1" ping -c 1 -W 2 -q 10.0.2.99 >/dev/null; then
+ echo "FAIL: IPv4 connectivity" >&2
+ exit 1
+fi
+if ! ip netns exec "$ns1" ping -6 -c 1 -W 2 -q dead:2::99 >/dev/null; then
+ echo "FAIL: IPv6 connectivity" >&2
+ exit 1
+fi
+
+listener_ready() {
+ ss -N "$ns2" -uln -H "sport = :$port" | grep -q "$port"
+}
+
+ip netns exec "$ns2" socat -T 60 \
+ UDP4-LISTEN:"$port",reuseaddr,fork EXEC:cat >/dev/null 2>&1 &
+ip netns exec "$ns2" socat -T 60 \
+ UDP6-LISTEN:"$port",reuseaddr,ipv6only=1,fork EXEC:cat >/dev/null 2>&1 &
+
+if ! busywait 3000 listener_ready; then
+ echo "FAIL: UDP listener did not start" >&2
+ exit 1
+fi
+
+udp_exchange() {
+ local fam=$1
+ local dst=$2
+ local sport=$3
+ local i msg got target
+
+ if [ "$fam" = 6 ]; then
+ target="UDP6:[${dst}]:${port},bind=[dead:1::99]:${sport}"
+ else
+ target="UDP4:${dst}:${port},bind=10.0.1.99:${sport}"
+ fi
+
+ # One socat for the whole burst. Closing it after every datagram
+ # makes socat emit an extra empty packet.
+ coproc SO { ip netns exec "$ns1" socat -T 5 - \
+ "$target,reuseaddr,shut-none" 2>/dev/null; }
+ i=0
+ while [ "$i" -lt "$count" ]; do
+ msg=$(printf '%04d' "$i")
+ printf '%s' "$msg" >&"${SO[1]}"
+ if ! IFS= read -r -n 4 -t 2 got <&"${SO[0]}"; then
+ echo "FAIL: packet $i timeout" >&2
+ kill "$SO_PID" 2>/dev/null || true
+ wait "$SO_PID" 2>/dev/null || true
+ return 1
+ fi
+ if [ "$got" != "$msg" ]; then
+ echo "FAIL: packet $i mismatch" >&2
+ kill "$SO_PID" 2>/dev/null || true
+ wait "$SO_PID" 2>/dev/null || true
+ return 1
+ fi
+ i=$((i + 1))
+ done
+ kill "$SO_PID" 2>/dev/null || true
+ wait "$SO_PID" 2>/dev/null || true
+}
+
+ct_line() {
+ local sport=$1
+
+ ip netns exec "$nsr" cat /proc/net/nf_conntrack \
+ | grep -F "sport=${sport} dport=${port}" | head -n 1
+}
+
+slow_packets() {
+ ip netns exec "$nsr" nft list counter inet filter slow \
+ | sed -n 's/.*packets \([0-9][0-9]*\).*/\1/p' | head -n 1
+}
+
+run_case() {
+ local what=$1
+ local fam=$2
+ local dst=$3
+ local sport=$4
+ local line orig repl slow
+
+ ip netns exec "$nsr" nft reset counter inet filter slow >/dev/null
+
+ if ! udp_exchange "$fam" "$dst" "$sport"; then
+ echo "FAIL: $what: payload was not forwarded" >&2
+ ret=1
+ return
+ fi
+
+ slow=$(slow_packets)
+ line=$(ct_line "$sport")
+
+ if ! printf '%s\n' "$line" | grep -Fq '[OFFLOAD]'; then
+ echo "FAIL: $what: conntrack entry is not offloaded" >&2
+ printf '%s\n' "$line" >&2
+ echo "forward counter packets=${slow:-none}" >&2
+ ret=1
+ return
+ fi
+
+ # Forward hook sees the handshake only. A full burst means the
+ # ingress flowtable did not take over.
+ if [ "${slow:-999}" -gt 8 ]; then
+ echo "FAIL: $what: forward hook saw ${slow} UDP packets" >&2
+ printf '%s\n' "$line" >&2
+ ret=1
+ return
+ fi
+
+ orig=$(printf '%s\n' "$line" | grep -o 'packets=[0-9]*' | head -n 1)
+ repl=$(printf '%s\n' "$line" | grep -o 'packets=[0-9]*' | sed -n '2p')
+ orig=${orig#packets=}
+ repl=${repl#packets=}
+
+ # nf_ct_acct_update is what the flowtable counter flag does. Without
+ # it, only the handshake is accounted.
+ if [ "${orig:-0}" -lt "$count" ] || [ "${repl:-0}" -lt "$count" ]; then
+ echo "FAIL: $what: got ${orig:-0}/${repl:-0}, need $count" >&2
+ printf '%s\n' "$line" >&2
+ ret=1
+ return
+ fi
+
+ echo "PASS: $what (forward=${slow}, orig=${orig}, reply=${repl})"
+}
+
+run_case "IPv4 UDP" 4 10.0.2.99 5401
+run_case "IPv6 UDP" 6 dead:2::99 5402
+
+if ! ip netns exec "$nsr" nft add rule inet nat postrouting \
+ oifname "veth1" masquerade; then
+ echo "FAIL: could not add masquerade rule" >&2
+ exit 1
+fi
+
+run_case "IPv4 UDP masquerade" 4 10.0.2.99 5403
+run_case "IPv6 UDP masquerade" 6 dead:2::99 5404
+
+exit $ret
--
2.47.3
prev parent reply other threads:[~2026-11-13 3:13 UTC|newest]
Thread overview: 18+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-13 3:12 [PATCH net-next 00/15] Netfilter updates for net-next Pablo Neira Ayuso
2026-11-13 3:12 ` [PATCH net-next 01/15] netfilter: flowtable: do not hw offload flows in teardown state Pablo Neira Ayuso
2026-11-13 3:17 ` netdev-bot+sinfo
2026-11-13 3:24 ` Pablo Neira Ayuso
2026-11-13 3:12 ` [PATCH net-next 02/15] netfilter: flowtable: fix pmtu discovery in nf_flow_tunnel_ip6ip6_push() Pablo Neira Ayuso
2026-11-13 3:12 ` [PATCH net-next 03/15] netfilter: flowtable: retrieve stats on flow deletion Pablo Neira Ayuso
2026-11-13 3:12 ` [PATCH net-next 04/15] net: ethernet: mtk_ppe: update " Pablo Neira Ayuso
2026-11-13 3:12 ` [PATCH net-next 05/15] net: airoha: " Pablo Neira Ayuso
2026-11-13 3:12 ` [PATCH net-next 06/15] netfilter: xt_time: fix monthday computation for dates past 2039 Pablo Neira Ayuso
2026-11-13 3:12 ` [PATCH net-next 07/15] netfilter: nft_flow_offload: do not offload multicast Pablo Neira Ayuso
2026-11-13 3:12 ` [PATCH net-next 08/15] netfilter: lwtunnel: expose read-only sysctl nf_hooks_lwtunnel for non init-netns Pablo Neira Ayuso
2026-11-13 3:12 ` [PATCH net-next 09/15] netfilter: flowtable: purge entries immediately when destroying the flowtable Pablo Neira Ayuso
2026-11-13 3:12 ` [PATCH net-next 10/15] selftests: netfilter: nf_queue: drop the no-op loop initializer Pablo Neira Ayuso
2026-11-13 3:12 ` [PATCH net-next 11/15] netfilter: flowtable: do not scrub as crossing netns Pablo Neira Ayuso
2026-11-13 3:12 ` [PATCH net-next 12/15] netfilter: flowtable: tunnel device poisons h_source of fast-path frames Pablo Neira Ayuso
2026-11-13 3:12 ` [PATCH net-next 13/15] netfilter: flowtable: do not clear the offload bit twice Pablo Neira Ayuso
2026-11-13 3:12 ` [PATCH net-next 14/15] netfilter: nfnetlink_queue: make skb writable before writing NFQA_L2HDR Pablo Neira Ayuso
2026-11-13 3:12 ` Pablo Neira Ayuso [this message]
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=20261113031251.2437074-16-pablo@netfilter.org \
--to=pablo@netfilter.org \
--cc=davem@davemloft.net \
--cc=edumazet@kernel.org \
--cc=fw@strlen.de \
--cc=horms@kernel.org \
--cc=ja@ssi.bg \
--cc=kuba@kernel.org \
--cc=netdev@vger.kernel.org \
--cc=netfilter-devel@vger.kernel.org \
--cc=pabeni@redhat.com \
/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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox