On 7/12/25 5:26 AM, Mohsin Bashir wrote: > Test XDP_PASS/DROP in single buffer and multi buffer mode when > XDP native support is available. > > ./drivers/net/xdp.py > TAP version 13 > 1..6 > ok 1 xdp.test_xdp_native_pass_sb > ok 2 xdp.test_xdp_native_pass_mb > ok 3 xdp.test_xdp_native_drop_sb > ok 4 xdp.test_xdp_native_drop_mb n exit a summary of passed and failed tests should be printed. Probably a exit api call is missing? # Totals: pass:4 fail:0 xfail:0 xpass:0 skip:0 error:0 > > Signed-off-by: Jakub Kicinski <kuba@xxxxxxxxxx> > Signed-off-by: Mohsin Bashir <mohsin.bashr@xxxxxxxxx> > --- > tools/testing/selftests/drivers/net/Makefile | 1 + > tools/testing/selftests/drivers/net/xdp.py | 303 ++++++++++++++++++ > .../selftests/net/lib/xdp_native.bpf.c | 158 +++++++++ > 3 files changed, 462 insertions(+) > create mode 100755 tools/testing/selftests/drivers/net/xdp.py > create mode 100644 tools/testing/selftests/net/lib/xdp_native.bpf.c > > diff --git a/tools/testing/selftests/drivers/net/Makefile b/tools/testing/selftests/drivers/net/Makefile > index bd309b2d3909..2ba7ae2bfe11 100644 > --- a/tools/testing/selftests/drivers/net/Makefile > +++ b/tools/testing/selftests/drivers/net/Makefile > @@ -21,6 +21,7 @@ TEST_PROGS := \ > stats.py \ > shaper.py \ > hds.py \ > + xdp.py \ > # end of TEST_PROGS > > include ../../lib.mk > diff --git a/tools/testing/selftests/drivers/net/xdp.py b/tools/testing/selftests/drivers/net/xdp.py > new file mode 100755 > index 000000000000..79a8156ed416 > --- /dev/null > +++ b/tools/testing/selftests/drivers/net/xdp.py > @@ -0,0 +1,303 @@ > +#!/usr/bin/env python3 > +# SPDX-License-Identifier: GPL-2.0 > + > +""" > +This file contains tests to verify native XDP support in network drivers. > +The tests utilize the BPF program `xdp_native.bpf.o` from the `selftests.net.lib` > +directory, with each test focusing on a specific aspect of XDP functionality. > +""" > +import random > +import string > +from dataclasses import dataclass > +from enum import Enum > + > +from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_ne Not related, but we have 2 ksft.py libraries currently: tools/testing/selftests/kselftest/ksft.py tools/testing/selftests/net/lib/py/ksft.py > +from lib.py import KsftFailEx, NetDrvEpEnv > +from lib.py import bkg, cmd, rand_port > +from lib.py import ip, bpftool, defer > + > + > +class TestConfig(Enum): > + """Enum for XDP configuration options.""" > + MODE = 0 # Configures the BPF program for a specific test > + PORT = 1 # Port configuration to communicate with the remote host > + > + > +class XDPAction(Enum): > + """Enum for XDP actions.""" > + PASS = 0 # Pass the packet up to the stack > + DROP = 1 # Drop the packet > + > + > +class XDPStats(Enum): > + """Enum for XDP statistics.""" > + RX = 0 # Count of valid packets received for testing > + PASS = 1 # Count of packets passed up to the stack > + DROP = 2 # Count of packets dropped > + > + > +@dataclass > +class BPFProgInfo: > + """Data class to store information about a BPF program.""" > + name: str # Name of the BPF program > + file: str # BPF program object file > + xdp_sec: str = "xdp" # XDP section name (e.g., "xdp" or "xdp.frags") > + mtu: int = 1500 # Maximum Transmission Unit, default is 1500 > + > + > +def _exchg_udp(cfg, port, test_string): > + """ > + Exchanges UDP packets between a local and remote host using the socat tool. > + > + Args: > + cfg: Configuration object containing network settings. > + port: Port number to use for the UDP communication. > + test_string: String that the remote host will send. > + > + Returns: > + The string received by the test host. > + """ > + cfg.require_cmd("socat", remote=True) > + > + rx_udp_cmd = f"socat -{cfg.addr_ipver} -T 2 -u UDP-RECV:{port},reuseport STDOUT" > + tx_udp_cmd = f"echo {test_string} | socat -t 2 -u STDIN UDP:{cfg.baddr}:{port}" > + > + with bkg(rx_udp_cmd, exit_wait=True) as nc: > + cmd(tx_udp_cmd, host=cfg.remote, shell=True) > + > + return nc.stdout.strip() > + > + > +def _test_udp(cfg, port, size=256): > + """ > + Tests UDP packet exchange between a local and remote host. > + > + Args: > + cfg: Configuration object containing network settings. > + port: Port number to use for the UDP communication. > + size: The length of the test string to be exchanged, default is 256 characters. > + > + Returns: > + bool: True if the received string matches the sent string, False otherwise. > + """ > + test_str = "".join(random.choice(string.ascii_lowercase) for _ in range(size)) > + recvd_str = _exchg_udp(cfg, port, test_str) > + > + return recvd_str == test_str > + > + > +def _load_xdp_prog(cfg, bpf_info): > + """ > + Loads an XDP program onto a network interface. > + > + Args: > + cfg: Configuration object containing network settings. > + bpf_info: BPFProgInfo object containing information about the BPF program. > + > + Returns: > + dict: A dictionary containing the XDP program ID, name, and associated map IDs. > + """ > + abs_path = cfg.net_lib_dir / bpf_info.file > + prog_info = {} > + > + cmd(f"ip link set dev {cfg.remote_ifname} mtu {bpf_info.mtu}", shell=True, host=cfg.remote) > + defer(ip, f"link set dev {cfg.remote_ifname} mtu 1500", host=cfg.remote) > + > + cmd( > + f"ip link set dev {cfg.ifname} mtu {bpf_info.mtu} xdp obj {abs_path} sec {bpf_info.xdp_sec}", > + shell=True > + ) > + defer(ip, f"link set dev {cfg.ifname} mtu 1500 xdp off") > + > + xdp_info = ip(f"-d link show dev {cfg.ifname}", json=True)[0] > + prog_info["id"] = xdp_info["xdp"]["prog"]["id"] > + prog_info["name"] = xdp_info["xdp"]["prog"]["name"] > + prog_id = prog_info["id"] > + > + map_ids = bpftool(f"prog show id {prog_id}", json=True)["map_ids"] > + prog_info["maps"] = {} > + for map_id in map_ids: > + name = bpftool(f"map show id {map_id}", json=True)["name"] > + prog_info["maps"][name] = map_id > + > + return prog_info > + > + > +def format_hex_bytes(value): > + """ > + Helper function that converts an integer into a formatted hexadecimal byte string. > + > + Args: > + value: An integer representing the number to be converted. > + > + Returns: > + A string representing hexadecimal equivalent of value, with bytes separated by spaces. > + """ > + hex_str = value.to_bytes(4, byteorder='little', signed=True) > + return ' '.join(f'{byte:02x}' for byte in hex_str) > + > + > +def _set_xdp_map(map_name, key, value): > + """ > + Updates an XDP map with a given key-value pair using bpftool. > + > + Args: > + map_name: The name of the XDP map to update. > + key: The key to update in the map, formatted as a hexadecimal string. > + value: The value to associate with the key, formatted as a hexadecimal string. > + """ > + key_formatted = format_hex_bytes(key) > + value_formatted = format_hex_bytes(value) > + bpftool( > + f"map update name {map_name} key hex {key_formatted} value hex {value_formatted}" > + ) > + > + > +def _get_stats(xdp_map_id): > + """ > + Retrieves and formats statistics from an XDP map. > + > + Args: > + xdp_map_id: The ID of the XDP map from which to retrieve statistics. > + > + Returns: > + A dictionary containing formatted packet statistics for various XDP actions. > + The keys are based on the XDPStats Enum values. > + > + Raises: > + KsftFailEx: If the stats retrieval fails. > + """ > + stats_dump = bpftool(f"map dump id {xdp_map_id}", json=True) > + if not stats_dump: > + raise KsftFailEx(f"Failed to get stats for map {xdp_map_id}") > + > + stats_formatted = {} > + for key in range(0, 4): > + val = stats_dump[key]["formatted"]["value"] > + if stats_dump[key]["formatted"]["key"] == XDPStats.RX.value: > + stats_formatted[XDPStats.RX.value] = val > + elif stats_dump[key]["formatted"]["key"] == XDPStats.PASS.value: > + stats_formatted[XDPStats.PASS.value] = val > + elif stats_dump[key]["formatted"]["key"] == XDPStats.DROP.value: > + stats_formatted[XDPStats.DROP.value] = val > + > + return stats_formatted > + > + > +def _test_pass(cfg, bpf_info, msg_sz): > + """ > + Tests the XDP_PASS action by exchanging UDP packets. > + > + Args: > + cfg: Configuration object containing network settings. > + bpf_info: BPFProgInfo object containing information about the BPF program. > + msg_sz: Size of the test message to send. > + """ > + > + prog_info = _load_xdp_prog(cfg, bpf_info) > + port = rand_port() > + > + _set_xdp_map("map_xdp_setup", TestConfig.MODE.value, XDPAction.PASS.value) > + _set_xdp_map("map_xdp_setup", TestConfig.PORT.value, port) > + > + ksft_eq(_test_udp(cfg, port, msg_sz), True, "UDP packet exchange failed") > + stats = _get_stats(prog_info["maps"]["map_xdp_stats"]) > + > + ksft_ne(stats[XDPStats.RX.value], 0, "RX stats should not be zero") > + ksft_eq(stats[XDPStats.RX.value], stats[XDPStats.PASS.value], "RX and PASS stats mismatch") > + > + > +def test_xdp_native_pass_sb(cfg): > + """ > + Tests the XDP_PASS action for single buffer case. > + > + Args: > + cfg: Configuration object containing network settings. > + """ > + bpf_info = BPFProgInfo("xdp_prog", "xdp_native.bpf.o", "xdp", 1500) > + > + _test_pass(cfg, bpf_info, 256) > + > + > +def test_xdp_native_pass_mb(cfg): > + """ > + Tests the XDP_PASS action for a multi-buff size. > + > + Args: > + cfg: Configuration object containing network settings. > + """ > + bpf_info = BPFProgInfo("xdp_prog_frags", "xdp_native.bpf.o", "xdp.frags", 9000) > + > + _test_pass(cfg, bpf_info, 8000) > + > + > +def _test_drop(cfg, bpf_info, msg_sz): > + """ > + Tests the XDP_DROP action by exchanging UDP packets. > + > + Args: > + cfg: Configuration object containing network settings. > + bpf_info: BPFProgInfo object containing information about the BPF program. > + msg_sz: Size of the test message to send. > + """ > + > + prog_info = _load_xdp_prog(cfg, bpf_info) > + port = rand_port() > + > + _set_xdp_map("map_xdp_setup", TestConfig.MODE.value, XDPAction.DROP.value) > + _set_xdp_map("map_xdp_setup", TestConfig.PORT.value, port) > + > + ksft_eq(_test_udp(cfg, port, msg_sz), False, "UDP packet exchange should fail") > + stats = _get_stats(prog_info["maps"]["map_xdp_stats"]) > + > + ksft_ne(stats[XDPStats.RX.value], 0, "RX stats should be zero") > + ksft_eq(stats[XDPStats.RX.value], stats[XDPStats.DROP.value], "RX and DROP stats mismatch") > + > + > +def test_xdp_native_drop_sb(cfg): > + """ > + Tests the XDP_DROP action for a signle-buff case. > + > + Args: > + cfg: Configuration object containing network settings. > + """ > + bpf_info = BPFProgInfo("xdp_prog", "xdp_native.bpf.o", "xdp", 1500) > + > + _test_drop(cfg, bpf_info, 256) > + > + > +def test_xdp_native_drop_mb(cfg): > + """ > + Tests the XDP_DROP action for a multi-buff case. > + > + Args: > + cfg: Configuration object containing network settings. > + """ > + bpf_info = BPFProgInfo("xdp_prog_frags", "xdp_native.bpf.o", "xdp.frags", 9000) > + > + _test_drop(cfg, bpf_info, 8000) > + > + > +def main(): > + """ > + Main function to execute the XDP tests. > + > + This function runs a series of tests to validate the XDP support for > + both the single and multi-buffer. It uses the NetDrvEpEnv context > + manager to manage the network driver environment and the ksft_run > + function to execute the tests. > + """ > + with NetDrvEpEnv(__file__) as cfg: > + ksft_run( > + [ > + test_xdp_native_pass_sb, > + test_xdp_native_pass_mb, > + test_xdp_native_drop_sb, > + test_xdp_native_drop_mb, > + ], > + args=(cfg,)) > + ksft_exit() > + > + > +if __name__ == "__main__": > + main() > diff --git a/tools/testing/selftests/net/lib/xdp_native.bpf.c b/tools/testing/selftests/net/lib/xdp_native.bpf.c > new file mode 100644 > index 000000000000..90b34b2a4fef > --- /dev/null > +++ b/tools/testing/selftests/net/lib/xdp_native.bpf.c > @@ -0,0 +1,158 @@ > +// SPDX-License-Identifier: GPL-2.0 > + > +#include <stddef.h> > +#include <linux/bpf.h> > +#include <linux/in.h> > +#include <linux/if_ether.h> > +#include <linux/ip.h> > +#include <linux/ipv6.h> > +#include <linux/udp.h> > +#include <bpf/bpf_endian.h> > +#include <bpf/bpf_helpers.h> > + > +enum { > + XDP_MODE = 0, > + XDP_PORT = 1, > +} xdp_map_setup_keys; > + > +enum { > + XDP_MODE_PASS = 0, > + XDP_MODE_DROP = 1, > +} xdp_map_modes; > + > +enum { > + STATS_RX = 0, > + STATS_PASS = 1, > + STATS_DROP = 2, > +} xdp_stats; > + > +struct { > + __uint(type, BPF_MAP_TYPE_ARRAY); > + __uint(max_entries, 2); > + __type(key, __u32); > + __type(value, __s32); > +} map_xdp_setup SEC(".maps"); > + > +struct { > + __uint(type, BPF_MAP_TYPE_ARRAY); > + __uint(max_entries, 4); > + __type(key, __u32); > + __type(value, __u64); > +} map_xdp_stats SEC(".maps"); > + > +static void record_stats(struct xdp_md *ctx, __u32 stat_type) > +{ > + __u64 *count; > + > + count = bpf_map_lookup_elem(&map_xdp_stats, &stat_type); > + > + if (count) > + __sync_fetch_and_add(count, 1); > +} > + > +static struct udphdr *filter_udphdr(struct xdp_md *ctx, __u16 port) > +{ > + void *data_end = (void *)(long)ctx->data_end; > + void *data = (void *)(long)ctx->data; > + struct udphdr *udph = NULL; > + struct ethhdr *eth = data; > + > + if (data + sizeof(*eth) > data_end) > + return NULL; > + > + if (eth->h_proto == bpf_htons(ETH_P_IP)) { > + struct iphdr *iph = data + sizeof(*eth); > + > + if (iph + 1 > (struct iphdr *)data_end || > + iph->protocol != IPPROTO_UDP) > + return NULL; > + > + udph = (void *)eth + sizeof(*iph) + sizeof(*eth); > + } else if (eth->h_proto == bpf_htons(ETH_P_IPV6)) { > + struct ipv6hdr *ipv6h = data + sizeof(*eth); > + > + if (ipv6h + 1 > (struct ipv6hdr *)data_end || > + ipv6h->nexthdr != IPPROTO_UDP) > + return NULL; > + > + udph = (void *)eth + sizeof(*ipv6h) + sizeof(*eth); > + } else { > + return NULL; > + } > + > + if (udph + 1 > (struct udphdr *)data_end) > + return NULL; > + > + if (udph->dest != bpf_htons(port)) > + return NULL; > + > + record_stats(ctx, STATS_RX); > + > + return udph; > +} > + > +static int xdp_mode_pass(struct xdp_md *ctx, __u16 port) > +{ > + struct udphdr *udph = NULL; > + > + udph = filter_udphdr(ctx, port); > + if (!udph) > + return XDP_PASS; > + > + record_stats(ctx, STATS_PASS); > + > + return XDP_PASS; > +} > + > +static int xdp_mode_drop_handler(struct xdp_md *ctx, __u16 port) > +{ > + struct udphdr *udph = NULL; > + > + udph = filter_udphdr(ctx, port); > + if (!udph) > + return XDP_PASS; > + > + record_stats(ctx, STATS_DROP); > + > + return XDP_DROP; > +} > + > +static int xdp_prog_common(struct xdp_md *ctx) > +{ > + __u32 key, *port; > + __s32 *mode; > + > + key = XDP_MODE; > + mode = bpf_map_lookup_elem(&map_xdp_setup, &key); > + if (!mode) > + return XDP_PASS; > + > + key = XDP_PORT; > + port = bpf_map_lookup_elem(&map_xdp_setup, &key); > + if (!port) > + return XDP_PASS; > + > + switch (*mode) { > + case XDP_MODE_PASS: > + return xdp_mode_pass(ctx, (__u16)(*port)); > + case XDP_MODE_DROP: > + return xdp_mode_drop_handler(ctx, (__u16)(*port)); > + } > + > + /* Default action is to simple pass */ > + return XDP_PASS; > +} > + > +SEC("xdp") > +int xdp_prog(struct xdp_md *ctx) > +{ > + return xdp_prog_common(ctx); > +} > + > +SEC("xdp.frags") > +int xdp_prog_frags(struct xdp_md *ctx) > +{ > + return xdp_prog_common(ctx); > +} > + > +char _license[] SEC("license") = "GPL";