Commit 1cbcb1b2 authored by Mohsin Bashir's avatar Mohsin Bashir Committed by Jakub Kicinski
Browse files

selftests: drv-net: Test XDP_PASS/DROP support



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..4
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
\# Totals: pass:4 fail:0 xfail:0 xpass:0 skip:0 error:0

Signed-off-by: default avatarMohsin Bashir <mohsin.bashr@gmail.com>
Link: https://patch.msgid.link/20250719083059.3209169-3-mohsin.bashr@gmail.com


Signed-off-by: default avatarJakub Kicinski <kuba@kernel.org>
parent be09f0d1
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ TEST_PROGS := \
	stats.py \
	shaper.py \
	hds.py \
	xdp.py \
# end of TEST_PROGS

include ../../lib.mk
+303 −0
Original line number Diff line number Diff line
#!/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
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 -n {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()
+158 −0
Original line number Diff line number Diff line
// 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";