Netdev List
 help / color / mirror / Atom feed
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 +
 .../net/tx_extra_headroom_dataplane.py        | 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 \
diff --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


      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