]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
selftests: drv-net: Add ntuple (NFC) flow steering test
authorDimitri Daskalakis <daskald@meta.com>
Tue, 7 Apr 2026 16:49:53 +0000 (09:49 -0700)
committerJakub Kicinski <kuba@kernel.org>
Fri, 10 Apr 2026 22:32:11 +0000 (15:32 -0700)
Add a test for ethtool NFC (ntuple) flow steering rules. The test
creates an ntuple rule matching on various flow fields and verifies
that traffic is steered to the correct queue.

The test forces all traffic to queue 0 via the indirection table,
then installs an ntuple rule to steer select traffic to a specific
queue. The test then verifies the expected number of packets is received
on the queue.

This test has variants for TCP/UDP over IPv4/IPv6, with rules matching
the source IP. Additional match fields will be added in the next commit.

 TAP version 13
 1..4
 ok 1 ntuple.queue.tcp4.src_ip
 ok 2 ntuple.queue.udp4.src_ip
 ok 3 ntuple.queue.tcp6.src_ip
 ok 4 ntuple.queue.udp6.src_ip
 # Totals: pass:4 fail:0 xfail:0 xpass:0 skip:0 error:0

Signed-off-by: Dimitri Daskalakis <daskald@meta.com>
Link: https://patch.msgid.link/20260407164954.2977820-2-dimitri.daskalakis1@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
tools/testing/selftests/drivers/net/hw/Makefile
tools/testing/selftests/drivers/net/hw/ntuple.py [new file with mode: 0755]

index 28d245e11bc453dc4408afa2d5b2b8bf019031ac..d84e955ac87b553310e9f63f17ad80ee7550c9b4 100644 (file)
@@ -36,6 +36,7 @@ TEST_PROGS = \
        nic_timestamp.py \
        nk_netns.py \
        nk_qlease.py \
+       ntuple.py \
        pp_alloc_fail.py \
        rss_api.py \
        rss_ctx.py \
diff --git a/tools/testing/selftests/drivers/net/hw/ntuple.py b/tools/testing/selftests/drivers/net/hw/ntuple.py
new file mode 100755 (executable)
index 0000000..e0d20e8
--- /dev/null
@@ -0,0 +1,145 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""Test ethtool NFC (ntuple) flow steering rules."""
+
+import random
+from enum import Enum, auto
+from lib.py import ksft_run, ksft_exit
+from lib.py import ksft_eq, ksft_ge
+from lib.py import ksft_variants, KsftNamedVariant
+from lib.py import EthtoolFamily, NetDrvEpEnv, NetdevFamily
+from lib.py import KsftSkipEx
+from lib.py import cmd, ethtool, defer, rand_port, bkg, wait_port_listen
+
+
+class NtupleField(Enum):
+    SRC_IP = auto()
+
+
+def _require_ntuple(cfg):
+    features = ethtool(f"-k {cfg.ifname}", json=True)[0]
+    if not features["ntuple-filters"]["active"]:
+        raise KsftSkipEx("Ntuple filters not enabled on the device: " + str(features["ntuple-filters"]))
+
+
+def _get_rx_cnts(cfg, prev=None):
+    """Get Rx packet counts for all queues, as a simple list of integers
+       if @prev is specified the prev counts will be subtracted"""
+    cfg.wait_hw_stats_settle()
+    data = cfg.netdevnl.qstats_get({"ifindex": cfg.ifindex, "scope": ["queue"]}, dump=True)
+    data = [x for x in data if x['queue-type'] == "rx"]
+    max_q = max([x["queue-id"] for x in data])
+    queue_stats = [0] * (max_q + 1)
+    for q in data:
+        queue_stats[q["queue-id"]] = q["rx-packets"]
+        if prev and q["queue-id"] < len(prev):
+            queue_stats[q["queue-id"]] -= prev[q["queue-id"]]
+    return queue_stats
+
+
+def _ntuple_rule_add(cfg, flow_spec):
+    """Install an NFC rule via ethtool."""
+
+    output = ethtool(f"-N {cfg.ifname} {flow_spec}").stdout
+    rule_id = int(output.split()[-1])
+    defer(ethtool, f"-N {cfg.ifname} delete {rule_id}")
+
+
+def _setup_isolated_queue(cfg):
+    """Default all traffic to queue 0, and pick a random queue to
+       steer NFC traffic to."""
+
+    channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
+    ch_max = channels['combined-max']
+    qcnt = channels['combined-count']
+
+    if ch_max < 2:
+        raise KsftSkipEx(f"Need at least 2 combined channels, max is {ch_max}")
+
+    desired_queues = min(ch_max, 4)
+    if qcnt >= desired_queues:
+        desired_queues = qcnt
+    else:
+        ethtool(f"-L {cfg.ifname} combined {desired_queues}")
+        defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
+
+    ethtool(f"-X {cfg.ifname} equal 1")
+    defer(ethtool, f"-X {cfg.ifname} default")
+
+    return random.randint(1, desired_queues - 1)
+
+
+def _send_traffic(cfg, ipver, proto, dst_port, pkt_cnt=40):
+    """Generate traffic with the desired flow signature."""
+
+    cfg.require_cmd("socat", remote=True)
+
+    socat_proto = proto.upper()
+    dst_addr = f"[{cfg.addr_v['6']}]" if ipver == '6' else cfg.addr_v['4']
+
+    extra_opts = ",nodelay" if proto == "tcp" else ",shut-null"
+
+    listen_cmd = (f"socat -{ipver} -t 2 -u "
+                  f"{socat_proto}-LISTEN:{dst_port},reuseport /dev/null")
+    with bkg(listen_cmd, exit_wait=True):
+        wait_port_listen(dst_port, proto=proto)
+        send_cmd = f"""
+        bash -c 'for i in $(seq {pkt_cnt}); do echo msg; sleep 0.02; done' |
+        socat -{ipver} -u - \
+            {socat_proto}:{dst_addr}:{dst_port},reuseaddr{extra_opts}
+        """
+        cmd(send_cmd, shell=True, host=cfg.remote)
+
+
+def _add_ntuple_rule_and_send_traffic(cfg, ipver, proto, fields, test_queue):
+    dst_port = rand_port()
+    flow_parts = [f"flow-type {proto}{ipver}"]
+
+    for field in fields:
+        if field == NtupleField.SRC_IP:
+            flow_parts.append(f"src-ip {cfg.remote_addr_v[ipver]}")
+
+    flow_parts.append(f"action {test_queue}")
+    _ntuple_rule_add(cfg, " ".join(flow_parts))
+    _send_traffic(cfg, ipver, proto, dst_port=dst_port)
+
+
+def _ntuple_variants():
+    for ipver in ["4", "6"]:
+        for proto in ["tcp", "udp"]:
+            for fields in [[NtupleField.SRC_IP]]:
+                name = ".".join(f.name.lower() for f in fields)
+                yield KsftNamedVariant(f"{proto}{ipver}.{name}",
+                                      ipver, proto, fields)
+
+
+@ksft_variants(_ntuple_variants())
+def queue(cfg, ipver, proto, fields):
+    """Test that an NFC rule steers traffic to the correct queue."""
+
+    cfg.require_ipver(ipver)
+    _require_ntuple(cfg)
+
+    test_queue = _setup_isolated_queue(cfg)
+
+    cnts = _get_rx_cnts(cfg)
+    _add_ntuple_rule_and_send_traffic(cfg, ipver, proto, fields, test_queue)
+    cnts = _get_rx_cnts(cfg, prev=cnts)
+
+    ksft_ge(cnts[test_queue], 40, f"Traffic on test queue {test_queue}: {cnts}")
+    sum_idle = sum(cnts) - cnts[0] - cnts[test_queue]
+    ksft_eq(sum_idle, 0, f"Traffic on idle queues: {cnts}")
+
+
+def main() -> None:
+    """Ksft boilerplate main."""
+
+    with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
+        cfg.ethnl = EthtoolFamily()
+        cfg.netdevnl = NetdevFamily()
+        ksft_run([queue], args=(cfg,))
+    ksft_exit()
+
+
+if __name__ == "__main__":
+    main()