xref: /linux/tools/testing/selftests/drivers/net/hw/rss_flow_label.py (revision 07fdad3a93756b872da7b53647715c48d0f4a2d0)
1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0
3
4"""
5Tests for RSS hashing on IPv6 Flow Label.
6"""
7
8import glob
9import os
10import socket
11from lib.py import CmdExitFailure
12from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_ge, ksft_in, \
13    ksft_not_in, ksft_raises, KsftSkipEx
14from lib.py import bkg, cmd, defer, fd_read_timeout, rand_port
15from lib.py import NetDrvEpEnv
16
17
18def _check_system(cfg):
19    if not hasattr(socket, "SO_INCOMING_CPU"):
20        raise KsftSkipEx("socket.SO_INCOMING_CPU was added in Python 3.11")
21
22    qcnt = len(glob.glob(f"/sys/class/net/{cfg.ifname}/queues/rx-*"))
23    if qcnt < 2:
24        raise KsftSkipEx(f"Local has only {qcnt} queues")
25
26    for f in [f"/sys/class/net/{cfg.ifname}/queues/rx-0/rps_flow_cnt",
27              f"/sys/class/net/{cfg.ifname}/queues/rx-0/rps_cpus"]:
28        try:
29            with open(f, 'r') as fp:
30                setting = fp.read().strip()
31                # CPU mask will be zeros and commas
32                if setting.replace("0", "").replace(",", ""):
33                    raise KsftSkipEx(f"RPS/RFS is configured: {f}: {setting}")
34        except FileNotFoundError:
35            pass
36
37    # 1 is the default, if someone changed it we probably shouldn"t mess with it
38    af = cmd("cat /proc/sys/net/ipv6/auto_flowlabels", host=cfg.remote).stdout
39    if af.strip() != "1":
40        raise KsftSkipEx("Remote does not have auto_flowlabels enabled")
41
42
43def _ethtool_get_cfg(cfg, fl_type):
44    descr = cmd(f"ethtool -n {cfg.ifname} rx-flow-hash {fl_type}").stdout
45
46    converter = {
47        "IP SA": "s",
48        "IP DA": "d",
49        "L3 proto": "t",
50        "L4 bytes 0 & 1 [TCP/UDP src port]": "f",
51        "L4 bytes 2 & 3 [TCP/UDP dst port]": "n",
52        "IPv6 Flow Label": "l",
53    }
54
55    ret = ""
56    for line in descr.split("\n")[1:-2]:
57        # if this raises we probably need to add more keys to converter above
58        ret += converter[line]
59    return ret
60
61
62def _traffic(cfg, one_sock, one_cpu):
63    local_port  = rand_port(socket.SOCK_DGRAM)
64    remote_port = rand_port(socket.SOCK_DGRAM)
65
66    sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
67    sock.bind(("", local_port))
68    sock.connect((cfg.remote_addr_v["6"], 0))
69    if one_sock:
70        send = f"exec 5<>/dev/udp/{cfg.addr_v['6']}/{local_port}; " \
71                "for i in `seq 20`; do echo a >&5; sleep 0.02; done; exec 5>&-"
72    else:
73        send = "for i in `seq 20`; do echo a | socat -t0.02 - UDP6:" \
74              f"[{cfg.addr_v['6']}]:{local_port},sourceport={remote_port}; done"
75
76    cpus = set()
77    with bkg(send, shell=True, host=cfg.remote, exit_wait=True):
78        for _ in range(20):
79            fd_read_timeout(sock.fileno(), 1)
80            cpu = sock.getsockopt(socket.SOL_SOCKET, socket.SO_INCOMING_CPU)
81            cpus.add(cpu)
82
83    if one_cpu:
84        ksft_eq(len(cpus), 1,
85                f"{one_sock=} - expected one CPU, got traffic on: {cpus=}")
86    else:
87        ksft_ge(len(cpus), 2,
88                f"{one_sock=} - expected many CPUs, got traffic on: {cpus=}")
89
90
91def test_rss_flow_label(cfg):
92    """
93    Test hashing on IPv6 flow label. Send traffic over a single socket
94    and over multiple sockets. Depend on the remote having auto-label
95    enabled so that it randomizes the label per socket.
96    """
97
98    cfg.require_ipver("6")
99    cfg.require_cmd("socat", remote=True)
100    _check_system(cfg)
101
102    # Enable flow label hashing for UDP6
103    initial = _ethtool_get_cfg(cfg, "udp6")
104    no_lbl = initial.replace("l", "")
105    if "l" not in initial:
106        try:
107            cmd(f"ethtool -N {cfg.ifname} rx-flow-hash udp6 l{no_lbl}")
108        except CmdExitFailure as exc:
109            raise KsftSkipEx("Device doesn't support Flow Label for UDP6") from exc
110
111        defer(cmd, f"ethtool -N {cfg.ifname} rx-flow-hash udp6 {initial}")
112
113    _traffic(cfg, one_sock=True, one_cpu=True)
114    _traffic(cfg, one_sock=False, one_cpu=False)
115
116    # Disable it, we should see no hashing (reset was already defer()ed)
117    cmd(f"ethtool -N {cfg.ifname} rx-flow-hash udp6 {no_lbl}")
118
119    _traffic(cfg, one_sock=False, one_cpu=True)
120
121
122def _check_v4_flow_types(cfg):
123    for fl_type in ["tcp4", "udp4", "ah4", "esp4", "sctp4"]:
124        try:
125            cur = cmd(f"ethtool -n {cfg.ifname} rx-flow-hash {fl_type}").stdout
126            ksft_not_in("Flow Label", cur,
127                        comment=f"{fl_type=} has Flow Label:" + cur)
128        except CmdExitFailure:
129            # Probably does not support this flow type
130            pass
131
132
133def test_rss_flow_label_6only(cfg):
134    """
135    Test interactions with IPv4 flow types. It should not be possible to set
136    IPv6 Flow Label hashing for an IPv4 flow type. The Flow Label should also
137    not appear in the IPv4 "current config".
138    """
139
140    with ksft_raises(CmdExitFailure) as cm:
141        cmd(f"ethtool -N {cfg.ifname} rx-flow-hash tcp4 sdfnl")
142    ksft_in("Invalid argument", cm.exception.cmd.stderr)
143
144    _check_v4_flow_types(cfg)
145
146    # Try to enable Flow Labels and check again, in case it leaks thru
147    initial = _ethtool_get_cfg(cfg, "udp6")
148    changed = initial.replace("l", "") if "l" in initial else initial + "l"
149
150    cmd(f"ethtool -N {cfg.ifname} rx-flow-hash udp6 {changed}")
151    restore = defer(cmd, f"ethtool -N {cfg.ifname} rx-flow-hash udp6 {initial}")
152
153    _check_v4_flow_types(cfg)
154    restore.exec()
155    _check_v4_flow_types(cfg)
156
157
158def main() -> None:
159    with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
160        ksft_run([test_rss_flow_label,
161                  test_rss_flow_label_6only],
162                 args=(cfg, ))
163    ksft_exit()
164
165
166if __name__ == "__main__":
167    main()
168