xref: /linux/tools/testing/selftests/drivers/net/hw/ntuple.py (revision d603517771d8e08a2d8fc9e1f7682ce393d3973a)
1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0
3"""Test ethtool NFC (ntuple) flow steering rules."""
4
5import random
6from enum import Enum, auto
7from lib.py import ksft_run, ksft_exit
8from lib.py import ksft_eq, ksft_ge
9from lib.py import ksft_variants, KsftNamedVariant
10from lib.py import EthtoolFamily, NetDrvEpEnv, NetdevFamily
11from lib.py import KsftSkipEx
12from lib.py import cmd, ethtool, defer, rand_ports, bkg, wait_port_listen
13
14
15class NtupleField(Enum):
16    SRC_IP = auto()
17    DST_IP = auto()
18    SRC_PORT = auto()
19    DST_PORT = auto()
20
21
22def _require_ntuple(cfg):
23    features = ethtool(f"-k {cfg.ifname}", json=True)[0]
24    if not features["ntuple-filters"]["active"]:
25        if features["ntuple-filters"]["fixed"]:
26            raise KsftSkipEx("Device does not support ntuple-filters")
27        ethtool(f"-K {cfg.ifname} ntuple-filters on")
28        defer(ethtool, f"-K {cfg.ifname} ntuple-filters off")
29
30
31def _get_rx_cnts(cfg, prev=None):
32    """Get Rx packet counts for all queues, as a simple list of integers
33       if @prev is specified the prev counts will be subtracted"""
34    cfg.wait_hw_stats_settle()
35    data = cfg.netdevnl.qstats_get({"ifindex": cfg.ifindex, "scope": ["queue"]}, dump=True)
36    data = [x for x in data if x['queue-type'] == "rx"]
37    max_q = max([x["queue-id"] for x in data])
38    queue_stats = [0] * (max_q + 1)
39    for q in data:
40        queue_stats[q["queue-id"]] = q["rx-packets"]
41        if prev and q["queue-id"] < len(prev):
42            queue_stats[q["queue-id"]] -= prev[q["queue-id"]]
43    return queue_stats
44
45
46def _ntuple_rule_add(cfg, flow_spec):
47    """Install an NFC rule via ethtool."""
48
49    output = ethtool(f"-N {cfg.ifname} {flow_spec}").stdout
50    rule_id = int(output.split()[-1])
51    defer(ethtool, f"-N {cfg.ifname} delete {rule_id}")
52
53
54def _setup_isolated_queue(cfg):
55    """Default all traffic to queue 0, and pick a random queue to
56       steer NFC traffic to."""
57
58    channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
59    ch_max = channels['combined-max']
60    qcnt = channels['combined-count']
61
62    if ch_max < 2:
63        raise KsftSkipEx(f"Need at least 2 combined channels, max is {ch_max}")
64
65    desired_queues = min(ch_max, 4)
66    if qcnt >= desired_queues:
67        desired_queues = qcnt
68    else:
69        ethtool(f"-L {cfg.ifname} combined {desired_queues}")
70        defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
71
72    ethtool(f"-X {cfg.ifname} equal 1")
73    defer(ethtool, f"-X {cfg.ifname} default")
74
75    return random.randint(1, desired_queues - 1)
76
77
78def _send_traffic(cfg, ipver, proto, dst_port, src_port, pkt_cnt=40):
79    """Generate traffic with the desired flow signature."""
80
81    cfg.require_cmd("socat", remote=True)
82
83    socat_proto = proto.upper()
84    dst_addr = f"[{cfg.addr_v['6']}]" if ipver == '6' else cfg.addr_v['4']
85
86    extra_opts = ",nodelay" if proto == "tcp" else ",shut-null"
87
88    listen_cmd = (f"socat -{ipver} -t 2 -u "
89                  f"{socat_proto}-LISTEN:{dst_port},reuseport /dev/null")
90    with bkg(listen_cmd, exit_wait=True):
91        wait_port_listen(dst_port, proto=proto)
92        send_cmd = f"""
93        bash -c 'for i in $(seq {pkt_cnt}); do echo msg; sleep 0.02; done' |
94        socat -{ipver} -u - \
95            {socat_proto}:{dst_addr}:{dst_port},sourceport={src_port},reuseaddr{extra_opts}
96        """
97        cmd(send_cmd, shell=True, host=cfg.remote)
98
99
100def _add_ntuple_rule_and_send_traffic(cfg, ipver, proto, fields, test_queue):
101    ports = rand_ports(2)
102    src_port = ports[0]
103    dst_port = ports[1]
104    flow_parts = [f"flow-type {proto}{ipver}"]
105
106    for field in fields:
107        if field == NtupleField.SRC_IP:
108            flow_parts.append(f"src-ip {cfg.remote_addr_v[ipver]}")
109        elif field == NtupleField.DST_IP:
110            flow_parts.append(f"dst-ip {cfg.addr_v[ipver]}")
111        elif field == NtupleField.SRC_PORT:
112            flow_parts.append(f"src-port {src_port}")
113        elif field == NtupleField.DST_PORT:
114            flow_parts.append(f"dst-port {dst_port}")
115
116    flow_parts.append(f"action {test_queue}")
117    _ntuple_rule_add(cfg, " ".join(flow_parts))
118    _send_traffic(cfg, ipver, proto, dst_port=dst_port, src_port=src_port)
119
120
121def _ntuple_variants():
122    for ipver in ["4", "6"]:
123        for proto in ["tcp", "udp"]:
124            for fields in [[NtupleField.SRC_IP],
125                           [NtupleField.DST_IP],
126                           [NtupleField.SRC_PORT],
127                           [NtupleField.DST_PORT],
128                           [NtupleField.SRC_IP, NtupleField.DST_IP],
129                           [NtupleField.SRC_IP, NtupleField.DST_IP,
130                            NtupleField.SRC_PORT, NtupleField.DST_PORT]]:
131                name = ".".join(f.name.lower() for f in fields)
132                yield KsftNamedVariant(f"{proto}{ipver}.{name}",
133                                      ipver, proto, fields)
134
135
136@ksft_variants(_ntuple_variants())
137def queue(cfg, ipver, proto, fields):
138    """Test that an NFC rule steers traffic to the correct queue."""
139
140    cfg.require_ipver(ipver)
141    _require_ntuple(cfg)
142
143    test_queue = _setup_isolated_queue(cfg)
144
145    cnts = _get_rx_cnts(cfg)
146    _add_ntuple_rule_and_send_traffic(cfg, ipver, proto, fields, test_queue)
147    cnts = _get_rx_cnts(cfg, prev=cnts)
148
149    ksft_ge(cnts[test_queue], 40, f"Traffic on test queue {test_queue}: {cnts}")
150    sum_idle = sum(cnts) - cnts[0] - cnts[test_queue]
151    ksft_eq(sum_idle, 0, f"Traffic on idle queues: {cnts}")
152
153
154def main() -> None:
155    """Ksft boilerplate main."""
156
157    with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
158        cfg.ethnl = EthtoolFamily()
159        cfg.netdevnl = NetdevFamily()
160        ksft_run([queue], args=(cfg,))
161    ksft_exit()
162
163
164if __name__ == "__main__":
165    main()
166