From: David Keogh <dkeogh@amazon.com>
To: <netdev@vger.kernel.org>, Jakub Kicinski <kuba@kernel.org>,
Paolo Abeni <pabeni@redhat.com>,
Eric Dumazet <edumazet@kernel.org>
Cc: Dragos Tatulea <dtatulea@nvidia.com>,
Nimrod Oren <noren@nvidia.com>, <nh-open-source@amazon.com>,
<linux-kernel@vger.kernel.org>
Subject: [RFC net-next 6/6] selftests/net: add tx_extra_headroom data-plane test
Date: Thu, 15 Oct 2026 23:53:35 +0000 [thread overview]
Message-ID: <20261015235335.4162993-7-dkeogh@amazon.com> (raw)
In-Reply-To: <20261015235335.4162993-1-dkeogh@amazon.com>
The existing tx_extra_headroom test checks the IFLA_TX_EXTRA_HEADROOM
attribute itself. Add a test for its effect: with the knob at zero an
egress header-push reallocates the skb head on every packet, and with
the knob raised the realloc is eliminated. A second case checks that the
cliff sits at exactly the headroom the push needs.
The test loads the two BPF programs from the previous patch, attaching
count_realloc by SEC name with "bpftool prog loadall ... autoattach" and
reading realloc_cnt via bpf_map_dump(). The veth peer is kept in its own
netns so the datagram egresses the device under test, and a permanent
neighbor is primed so UDP is not dropped before egress.
Signed-off-by: David Keogh <dkeogh@amazon.com>
---
tools/testing/selftests/net/Makefile | 1 +
| 197 ++++++++++++++++++
2 files changed, 198 insertions(+)
create mode 100755 tools/testing/selftests/net/tx_extra_headroom_dataplane.py
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index 0eb9a69e9..28b0444ba 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -121,6 +121,7 @@ TEST_PROGS := \
tfo_passive.sh \
traceroute.sh \
tx_extra_headroom.py \
+ tx_extra_headroom_dataplane.py \
txtimestamp.sh \
udpgro.sh \
udpgro_bench.sh \
--git a/tools/testing/selftests/net/tx_extra_headroom_dataplane.py b/tools/testing/selftests/net/tx_extra_headroom_dataplane.py
new file mode 100755
index 000000000..d32bb1ed6
--- /dev/null
+++ b/tools/testing/selftests/net/tx_extra_headroom_dataplane.py
@@ -0,0 +1,197 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+
+"""
+Data-plane tests for IFLA_TX_EXTRA_HEADROOM: setting the knob eliminates the
+per-packet head reallocation that an egress header-push otherwise forces.
+
+Mechanism (fully in-tree, no bpftrace, no raw kprobe, no user-space loader):
+
+ * tx_extra_headroom_tc.bpf.o - a tc-egress program that pushes a 20-byte
+ outer header on UDP via bpf_skb_adjust_room(..., BPF_ADJ_ROOM_MAC, ...). On
+ an skb allocated with only LL_RESERVED_SPACE() of headroom that push
+ reallocates (pskb_expand_head) on every packet; with tx_extra_headroom
+ reserving enough room at allocation time it does not.
+
+ * tx_extra_headroom_probe.bpf.o - an fentry/pskb_expand_head program that
+ counts the reallocs for the device under test (filtered by cfg_ifindex) into
+ realloc_cnt. It is attached by its SEC name via
+ "bpftool prog loadall ... autoattach" and read via bpf_map_dump().
+
+Topology is two netns (peer in a separate netns) so the datagram genuinely
+egresses the device and hits the tc-egress hook; co-resident veth ends would be
+locally delivered via loopback and never reach egress. A static ARP neighbor is
+primed so UDP egresses instead of dropping at an unresolved neighbor.
+"""
+
+import socket
+import time
+
+from lib.py import ksft_run, ksft_exit, ksft_ge, ksft_lt
+from lib.py import ip, tc, bpftool, cmd
+from lib.py import bpf_map_dump, bpf_prog_map_ids
+from lib.py import NetNS, NetNSEnter
+from lib.py import RtnlFamily
+
+ATTR = "tx-extra-headroom"
+OUTER_LEN = 20
+# veth LL_RESERVED_SPACE = ((14 + 0) & ~15) + 16 = 16; the 14-byte L2 header
+# leaves 2 free, so a 20-byte push needs 20 - 2 = 18 more from the knob.
+THRESHOLD = 18
+NPKTS = 20000
+
+TC_OBJ = "tx_extra_headroom_tc.bpf.o"
+PROBE_OBJ = "tx_extra_headroom_probe.bpf.o"
+PINDIR = "/sys/fs/bpf/tx_extra_headroom"
+
+
+def ifindex(name):
+ return ip(f"link show dev {name}", json=True)[0]["ifindex"]
+
+
+def _hex_le(value, nbytes):
+ """bpftool 'key/value hex' wants space-separated little-endian bytes."""
+ return " ".join(f"{b:02x}" for b in int(value).to_bytes(nbytes, "little"))
+
+
+def _map_set_u32(map_id, key, value):
+ bpftool(f"map update id {map_id} key hex {_hex_le(key, 4)} "
+ f"value hex {_hex_le(value, 4)}")
+
+
+def _map_set_u64(map_id, key, value):
+ bpftool(f"map update id {map_id} key hex {_hex_le(key, 4)} "
+ f"value hex {_hex_le(value, 8)}")
+
+
+def set_hr(rtnl, idx, val):
+ rtnl.setlink({"ifi-index": idx, ATTR: val})
+
+
+class Observer:
+ """Loads the fentry realloc counter (autoattach) and the tc pusher, exposes
+ a reallocs(send_fn) call, and tears both down cleanly on close()."""
+
+ def __init__(self, dev, dev_ifindex, send_ns):
+ self.dev = dev
+ self.send_ns = send_ns
+ # Fresh pin dir; a leftover one would make loadall fail to pin.
+ cmd(f"rm -f {PINDIR}/*", shell=True, fail=False)
+ cmd(f"rmdir {PINDIR}", shell=True, fail=False)
+ # Single fentry program -> autoattach attaches it by SEC name and pins
+ # its maps under PINDIR. No bpftrace, no kprobe name.
+ bpftool(f"prog loadall {PROBE_OBJ} {PINDIR} autoattach")
+ # tc pusher: self-loading via object-file, attached inside the send ns.
+ tc(f"qdisc add dev {self.dev} clsact", ns=send_ns)
+ tc(f"filter add dev {self.dev} egress bpf da object-file {TC_OBJ} sec tc",
+ ns=send_ns)
+ # Resolve the two maps by name from the fentry program.
+ prog_id = self._probe_prog_id()
+ maps = bpf_prog_map_ids(prog_id)
+ self.cfg_id = maps["cfg_ifindex"]
+ self.cnt_id = maps["realloc_cnt"]
+ # Scope the counter to the device under test (by id: a stale pinned copy
+ # of the same-named map would make a by-name update ambiguous).
+ _map_set_u32(self.cfg_id, 0, dev_ifindex)
+
+ @staticmethod
+ def _probe_prog_id():
+ progs = bpftool("prog show", json=True)
+ for p in progs:
+ if p.get("name") == "count_realloc":
+ return p["id"]
+ raise RuntimeError("count_realloc fentry program not loaded")
+
+ def reallocs(self, send_fn):
+ """Reset the counter, run send_fn (traffic), return the realloc count."""
+ _map_set_u64(self.cnt_id, 0, 0)
+ send_fn()
+ time.sleep(0.2) # let the last in-flight reallocs land
+ return bpf_map_dump(self.cnt_id)[0]
+
+ def close(self):
+ # Detach the fentry link so it does not leak to later tests.
+ links = bpftool("link show", json=True)
+ for l in links:
+ if l.get("attach_type") == "trace_fentry":
+ cmd(f"bpftool link detach id {l['id']}", fail=False)
+ cmd(f"rm -f {PINDIR}/*", shell=True, fail=False)
+ cmd(f"rmdir {PINDIR}", shell=True, fail=False)
+
+
+def _blast_udp(dst, port, n):
+ """Send n small UDP datagrams; the receiver drops them, but the TX-side
+ realloc already happened, which is what we measure."""
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ s.connect((dst, port))
+ buf = b"x" * 1024
+ for _ in range(n):
+ try:
+ s.send(buf)
+ except OSError:
+ pass
+ s.close()
+
+
+def _setup_veth(peer_ns):
+ """hrt0 in the current netns (send side, tc push attached); hrt1 in peer_ns.
+ Prime a static ARP neighbor so UDP egresses instead of dropping at an
+ unresolved neighbor (which would read 0 reallocs regardless of the knob)."""
+ ip("link add hrt0 type veth peer name hrt1")
+ ip(f"link set hrt1 netns {peer_ns}")
+ ip("addr add 10.0.0.1/24 dev hrt0")
+ ip("link set hrt0 up")
+ ip("link set lo up")
+ with NetNSEnter(str(peer_ns)):
+ ip("addr add 10.0.0.2/24 dev hrt1")
+ ip("link set hrt1 up")
+ ip("link set lo up")
+ peer_mac = ip("link show dev hrt1", json=True)[0]["address"]
+ ip(f"neigh replace 10.0.0.2 lladdr {peer_mac} dev hrt0 nud permanent")
+
+
+def test_realloc_eliminated() -> None:
+ """Knob off -> push reallocates per packet; knob set -> realloc eliminated."""
+ with NetNS() as send_ns, NetNS() as peer_ns, NetNSEnter(str(send_ns)):
+ _setup_veth(peer_ns)
+ rtnl = RtnlFamily()
+ idx = ifindex("hrt0")
+ obs = Observer("hrt0", idx, str(send_ns))
+ try:
+ off = obs.reallocs(lambda: _blast_udp("10.0.0.2", 9999, NPKTS))
+ set_hr(rtnl, idx, 128)
+ on = obs.reallocs(lambda: _blast_udp("10.0.0.2", 9999, NPKTS))
+ finally:
+ obs.close()
+
+ ksft_ge(off, NPKTS // 2, "knob off: push should realloc most packets")
+ ksft_lt(on, NPKTS // 100, "knob on: realloc should be eliminated")
+
+
+def test_realloc_threshold() -> None:
+ """The realloc cliff sits at exactly the headroom the push needs."""
+ with NetNS() as send_ns, NetNS() as peer_ns, NetNSEnter(str(send_ns)):
+ _setup_veth(peer_ns)
+ rtnl = RtnlFamily()
+ idx = ifindex("hrt0")
+ obs = Observer("hrt0", idx, str(send_ns))
+ try:
+ set_hr(rtnl, idx, THRESHOLD - 1)
+ below = obs.reallocs(lambda: _blast_udp("10.0.0.2", 9999, NPKTS))
+ set_hr(rtnl, idx, THRESHOLD)
+ at = obs.reallocs(lambda: _blast_udp("10.0.0.2", 9999, NPKTS))
+ finally:
+ obs.close()
+
+ ksft_ge(below, NPKTS // 2, f"knob={THRESHOLD-1}: still reallocates")
+ ksft_lt(at, NPKTS // 100, f"knob={THRESHOLD}: realloc eliminated at cliff")
+
+
+def main() -> None:
+ ksft_run([test_realloc_eliminated,
+ test_realloc_threshold])
+ ksft_exit()
+
+
+if __name__ == "__main__":
+ main()
--
2.50.1
prev parent reply other threads:[~2026-10-15 23:57 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-15 23:53 [RFC net-next 0/6] net: configurable extra TX headroom for local egress David Keogh
2026-10-15 23:53 ` [RFC net-next 1/6] net: add tx_extra_headroom and LL_ALLOC_SPACE() David Keogh
2026-10-15 23:53 ` [RFC net-next 2/6] net: reserve extra TX headroom for local egress David Keogh
2026-10-15 23:53 ` [RFC net-next 3/6] rtnetlink: add IFLA_TX_EXTRA_HEADROOM David Keogh
2026-10-15 23:53 ` [RFC net-next 4/6] selftests: net: add tx_extra_headroom test David Keogh
2026-10-15 23:53 ` [RFC net-next 5/6] selftests/bpf: add tx_extra_headroom data-plane programs David Keogh
2026-10-15 23:53 ` David Keogh [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=20261015235335.4162993-7-dkeogh@amazon.com \
--to=dkeogh@amazon.com \
--cc=dtatulea@nvidia.com \
--cc=edumazet@kernel.org \
--cc=kuba@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=nh-open-source@amazon.com \
--cc=noren@nvidia.com \
--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