[PATCH net-next 5/8] selftests: net: tx_extra_headroom: test a push at TC egress

From
Tianyi Gao <hidden>
Date
2026-11-07 03:34:53
Also in
bpf, linux-doc, linux-kselftest
Reply to
Tianyi Gao, 2026-11-07
The tx_extra_headroom test only checks the IFLA_TX_EXTRA_HEADROOM
attribute itself. Add test cases for its effect on UDP over IPv4
packets.

tx_extra_headroom.bpf.c is a TC egress program. It matches the UDP test
packet by destination port and records its headroom. It then pushes 20
bytes after the MAC header with bpf_skb_adjust_room(). It records
whether skb->head changed. A changed skb->head means that
pskb_expand_head() reallocated the packet.

tx_extra_headroom.py is extended to create a veth pair. veth0 sends, and
its peer is in a second netns. The program runs at the TC egress of
veth0. Each case sets tx_extra_headroom on veth0, sends one UDP packet
and checks:

tx_extra_headroom  headroom  push reallocates
0                  2         yes
17                 18        yes
18                 34        no
256                258       no

The headroom is LL_ALLOC_SPACE() minus the 14-byte Ethernet header.
LL_ALLOC_SPACE() aligns to 16 bytes. So the headroom steps from 18 to 34
when tx_extra_headroom goes from 17 to 18. The test checks these exact
values. HH_DATA_MOD and the rounding in LL_RESERVED_SPACE() have not
changed since the beginning of git history (Linux 2.6.12-rc2, 2005).

Signed-off-by: Tianyi Gao <redacted>
---
The tc program follows tools/testing/selftests/bpf/progs/test_dst_clear.c.
---
 .../selftests/net/tx_extra_headroom.bpf.c     | 70 ++++++++++++++++++
 .../selftests/net/tx_extra_headroom.py        | 74 ++++++++++++++++++-
 2 files changed, 142 insertions(+), 2 deletions(-)
 create mode 100644 tools/testing/selftests/net/tx_extra_headroom.bpf.c
diff --git a/tools/testing/selftests/net/tx_extra_headroom.bpf.c b/tools/testing/selftests/net/tx_extra_headroom.bpf.c
new file mode 100644
index 000000000000..e74f44afbe56
--- /dev/null
+++ b/tools/testing/selftests/net/tx_extra_headroom.bpf.c
@@ -0,0 +1,70 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Record the headroom of a test packet and whether a push reallocates it. */
+#include <stdbool.h>
+#include <linux/bpf.h>
+#include <linux/if_ether.h>
+#include <linux/in.h>
+#include <linux/ip.h>
+#include <linux/pkt_cls.h>
+#include <linux/udp.h>
+#include <bpf/bpf_endian.h>
+#include <bpf/bpf_helpers.h>
+
+#define UDP_TEST_PORT 9999
+#define PUSH_LEN 20
+
+struct sk_buff {
+	unsigned char *head;
+	unsigned char *data;
+} __attribute__((preserve_access_index));
+
+void *bpf_cast_to_kern_ctx(void *obj) __ksym;
+
+__u32 count;
+__u32 headroom;
+int err;
+bool reallocated;
+
+static bool is_test_packet(struct __sk_buff *skb)
+{
+	struct udphdr udph;
+	struct iphdr iph;
+
+	if (skb->protocol != bpf_htons(ETH_P_IP))
+		return false;
+
+	if (bpf_skb_load_bytes(skb, ETH_HLEN, &iph, sizeof(iph)))
+		return false;
+
+	if (iph.protocol != IPPROTO_UDP)
+		return false;
+
+	if (bpf_skb_load_bytes(skb, ETH_HLEN + sizeof(iph), &udph,
+			       sizeof(udph)))
+		return false;
+
+	return udph.dest == bpf_htons(UDP_TEST_PORT);
+}
+
+SEC("tc/egress")
+int push(struct __sk_buff *skb)
+{
+	struct sk_buff *kskb;
+	unsigned char *head;
+
+	if (!is_test_packet(skb))
+		return TC_ACT_OK;
+
+	kskb = bpf_cast_to_kern_ctx(skb);
+	count++;
+	headroom = kskb->data - kskb->head;
+
+	/* pskb_expand_head() always allocates a new skb->head. */
+	head = kskb->head;
+	err = bpf_skb_adjust_room(skb, PUSH_LEN, BPF_ADJ_ROOM_MAC, 0);
+	reallocated = kskb->head != head;
+
+	return TC_ACT_SHOT;
+}
+
+char __license[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/net/tx_extra_headroom.py b/tools/testing/selftests/net/tx_extra_headroom.py
index 153384a1fb0d..a4db5aa7268b 100755
--- a/tools/testing/selftests/net/tx_extra_headroom.py
+++ b/tools/testing/selftests/net/tx_extra_headroom.py
@@ -3,13 +3,20 @@
 
 """Tests for the IFLA_TX_EXTRA_HEADROOM link attribute."""
 
+import socket
+from pathlib import Path
+
 from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_raises, KsftSkipEx
-from lib.py import ip
+from lib.py import ksft_variants, KsftNamedVariant
+from lib.py import ip, tc, bpftool, bpf_prog_map_ids
 from lib.py import NetNS, NetNSEnter
 from lib.py import Netlink, NlError, RtnlFamily
 
 ATTR = "tx-extra-headroom"
 MAX = 512
+BPF_OBJ = Path(__file__).parent / "tx_extra_headroom.bpf.o"
+PORT = 9999  # UDP_TEST_PORT in tx_extra_headroom.bpf.c
+PUSH = 20  # PUSH_LEN in tx_extra_headroom.bpf.c
 
 
 def ifindex(name):
@@ -131,6 +138,68 @@ def test_not_inherited() -> None:
                 4, "vlan needed_headroom")
 
 
+def setup_veth(peer_ns):
+    """Create veth0 in the current netns and its peer veth1 in peer_ns."""
+    ip("link add veth0 type veth peer name veth1")
+    ip(f"link set veth1 netns {peer_ns}")
+    ip("addr add 192.0.2.1/24 dev veth0")
+    ip("link set veth0 up")
+    ip("addr add 192.0.2.2/24 dev veth1", ns=peer_ns)
+    ip("link set veth1 up", ns=peer_ns)
+    peer_mac = ip("link show dev veth1", json=True, ns=peer_ns)[0]["address"]
+    ip(f"neigh replace 192.0.2.2 lladdr {peer_mac} dev veth0 nud permanent")
+
+
+def load_tc_prog(dev, direction):
+    """Attach the tc/<direction> program to dev, return its .bss map id."""
+    tc(f"qdisc add dev {dev} clsact")
+    tc(f"filter add dev {dev} {direction} bpf da obj {BPF_OBJ} "
+       f"sec tc/{direction}")
+    filters = tc(f"filter show dev {dev} {direction}", json=True)
+    prog_id = next(f["options"]["prog"]["id"] for f in filters
+                   if "options" in f)
+    return next(map_id for name, map_id in bpf_prog_map_ids(prog_id).items()
+                if name.endswith(".bss"))
+
+
+def read_bss(map_id):
+    """Return the global variables of the tc program as a dict."""
+    dump = bpftool(f"map dump id {map_id}", json=True)[0]
+    return {name: val for var in dump["formatted"]["value"][".bss"]
+            for name, val in var.items()}
+
+
+def send_udp4():
+    """Send one UDP packet from veth0 to veth1."""
+    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
+        s.sendto(b"x", ("192.0.2.2", PORT))
+
+
+# A local IPv4 UDP packet on veth gets LL_ALLOC_SPACE() =
+# ((14 + tx_extra_headroom) & ~15) + 16 bytes of headroom. The Ethernet
+# header takes 14 of them. The push reallocates if less than PUSH bytes are
+# left.
+@ksft_variants([
+    KsftNamedVariant("udp4_0", send_udp4, 0, 2),
+    KsftNamedVariant("udp4_17", send_udp4, 17, 18),
+    KsftNamedVariant("udp4_18", send_udp4, 18, 34),
+    KsftNamedVariant("udp4_256", send_udp4, 256, 258),
+])
+def test_push(send, hr, headroom) -> None:
+    """Check the headroom of one packet and whether a push reallocates it."""
+    with NetNS() as send_ns, NetNS() as peer_ns, NetNSEnter(str(send_ns)):
+        setup_veth(peer_ns)
+        set_hr(RtnlFamily(), ifindex("veth0"), hr)
+        map_id = load_tc_prog("veth0", "egress")
+        send()
+        bss = read_bss(map_id)
+
+    ksft_eq(bss["count"], 1, "test packets seen")
+    ksft_eq(bss["headroom"], headroom, "headroom before the push")
+    ksft_eq(bss["err"], 0, "bpf_skb_adjust_room() return value")
+    ksft_eq(bss["reallocated"], headroom < PUSH, "push reallocated the head")
+
+
 def main() -> None:
     """Run all test cases."""
     ksft_run([test_default_and_set,
@@ -139,7 +208,8 @@ def main() -> None:
               test_notification,
               test_survives_down_up,
               test_survives_netns_move,
-              test_not_inherited])
+              test_not_inherited,
+              test_push])
     ksft_exit()
 
 
-- 
2.56.0
Thread (15 messages) 15 messages, 3 authors, last reply 21h ago
Keyboard shortcuts
hback out one level
jnext message in thread / next series-diff position
kprevious message in thread / previous series-diff position
ldrill in
Escclose help / fold thread tree
?toggle this help