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