xref: /linux/tools/testing/selftests/drivers/net/gro.py (revision d3b35898de024796c43415f9535fd0bc69cb8f1b)
1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0
3
4"""
5GRO (Generic Receive Offload) conformance tests.
6
7Validates that GRO coalescing works correctly by running the gro
8binary in different configurations and checking for correct packet
9coalescing behavior.
10
11Test cases:
12  - data: Data packets with same size/headers and correct seq numbers coalesce
13  - ack: Pure ACK packets do not coalesce
14  - flags: Packets with PSH, SYN, URG, RST flags do not coalesce
15  - tcp: Packets with incorrect checksum, non-consecutive seqno don't coalesce
16  - ip: Packets with different ECN, TTL, TOS, or IP options don't coalesce
17  - large: Packets larger than GRO_MAX_SIZE don't coalesce
18"""
19
20import os
21from lib.py import ksft_run, ksft_exit, ksft_pr
22from lib.py import NetDrvEpEnv, KsftXfailEx
23from lib.py import bkg, cmd, defer, ethtool, ip
24from lib.py import ksft_variants
25
26
27def _resolve_dmac(cfg, ipver):
28    """
29    Find the destination MAC address remote host should use to send packets
30    towards the local host. It may be a router / gateway address.
31    """
32
33    attr = "dmac" + ipver
34    # Cache the response across test cases
35    if hasattr(cfg, attr):
36        return getattr(cfg, attr)
37
38    route = ip(f"-{ipver} route get {cfg.addr_v[ipver]}",
39               json=True, host=cfg.remote)[0]
40    gw = route.get("gateway")
41    # Local L2 segment, address directly
42    if not gw:
43        setattr(cfg, attr, cfg.dev['address'])
44        return getattr(cfg, attr)
45
46    # ping to make sure neighbor is resolved,
47    # bind to an interface, for v6 the GW is likely link local
48    cmd(f"ping -c1 -W0 -I{cfg.remote_ifname} {gw}", host=cfg.remote)
49
50    neigh = ip(f"neigh get {gw} dev {cfg.remote_ifname}",
51               json=True, host=cfg.remote)[0]
52    setattr(cfg, attr, neigh['lladdr'])
53    return getattr(cfg, attr)
54
55
56def _write_defer_restore(cfg, path, val, defer_undo=False):
57    with open(path, "r", encoding="utf-8") as fp:
58        orig_val = fp.read().strip()
59        if str(val) == orig_val:
60            return
61    with open(path, "w", encoding="utf-8") as fp:
62        fp.write(val)
63    if defer_undo:
64        defer(_write_defer_restore, cfg, path, orig_val)
65
66
67def _set_mtu_restore(dev, mtu, host):
68    if dev['mtu'] < mtu:
69        ip(f"link set dev {dev['ifname']} mtu {mtu}", host=host)
70        defer(ip, f"link set dev {dev['ifname']} mtu {dev['mtu']}", host=host)
71
72
73def _set_ethtool_feat(dev, current, feats, host=None):
74    s2n = {True: "on", False: "off"}
75
76    new = ["-K", dev]
77    old = ["-K", dev]
78    no_change = True
79    for name, state in feats.items():
80        new += [name, s2n[state]]
81        old += [name, s2n[current[name]["active"]]]
82
83        if current[name]["active"] != state:
84            no_change = False
85            if current[name]["fixed"]:
86                raise KsftXfailEx(f"Device does not support {name}")
87    if no_change:
88        return
89
90    eth_cmd = ethtool(" ".join(new), host=host)
91    defer(ethtool, " ".join(old), host=host)
92
93    # If ethtool printed something kernel must have modified some features
94    if eth_cmd.stdout:
95        ksft_pr(eth_cmd)
96
97
98def _setup(cfg, mode, test_name):
99    """ Setup hardware loopback mode for GRO testing. """
100
101    if not hasattr(cfg, "bin_remote"):
102        cfg.bin_local = cfg.test_dir / "gro"
103        cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)
104
105    if not hasattr(cfg, "feat"):
106        cfg.feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
107        cfg.remote_feat = ethtool(f"-k {cfg.remote_ifname}",
108                                  host=cfg.remote, json=True)[0]
109
110    # "large" test needs at least 4k MTU
111    if test_name == "large":
112        _set_mtu_restore(cfg.dev, 4096, None)
113        _set_mtu_restore(cfg.remote_dev, 4096, cfg.remote)
114
115    if mode == "sw":
116        flush_path = f"/sys/class/net/{cfg.ifname}/gro_flush_timeout"
117        irq_path = f"/sys/class/net/{cfg.ifname}/napi_defer_hard_irqs"
118
119        _write_defer_restore(cfg, flush_path, "200000", defer_undo=True)
120        _write_defer_restore(cfg, irq_path, "10", defer_undo=True)
121
122        _set_ethtool_feat(cfg.ifname, cfg.feat,
123                          {"generic-receive-offload": True,
124                           "rx-gro-hw": False,
125                           "large-receive-offload": False})
126    elif mode == "hw":
127        _set_ethtool_feat(cfg.ifname, cfg.feat,
128                          {"generic-receive-offload": False,
129                           "rx-gro-hw": True,
130                           "large-receive-offload": False})
131
132        # Some NICs treat HW GRO as a GRO sub-feature so disabling GRO
133        # will also clear HW GRO. Use a hack of installing XDP generic
134        # to skip SW GRO, even when enabled.
135        feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
136        if not feat["rx-gro-hw"]["active"]:
137            ksft_pr("Driver clears HW GRO and SW GRO is cleared, using generic XDP workaround")
138            prog = cfg.net_lib_dir / "xdp_dummy.bpf.o"
139            ip(f"link set dev {cfg.ifname} xdpgeneric obj {prog} sec xdp")
140            defer(ip, f"link set dev {cfg.ifname} xdpgeneric off")
141
142            # Attaching XDP may change features, fetch the latest state
143            feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
144
145            _set_ethtool_feat(cfg.ifname, feat,
146                              {"generic-receive-offload": True,
147                               "rx-gro-hw": True,
148                               "large-receive-offload": False})
149    elif mode == "lro":
150        # netdevsim advertises LRO for feature inheritance testing with
151        # bonding/team tests but it doesn't actually perform the offload
152        cfg.require_nsim(nsim_test=False)
153
154        _set_ethtool_feat(cfg.ifname, cfg.feat,
155                          {"generic-receive-offload": False,
156                           "rx-gro-hw": False,
157                           "large-receive-offload": True})
158
159    try:
160        # Disable TSO for local tests
161        cfg.require_nsim()  # will raise KsftXfailEx if not running on nsim
162
163        _set_ethtool_feat(cfg.remote_ifname, cfg.remote_feat,
164                          {"tcp-segmentation-offload": False},
165                          host=cfg.remote)
166    except KsftXfailEx:
167        pass
168
169
170def _gro_variants():
171    """Generator that yields all combinations of protocol and test types."""
172
173    for mode in ["sw", "hw", "lro"]:
174        for protocol in ["ipv4", "ipv6", "ipip"]:
175            for test_name in ["data", "ack", "flags", "tcp", "ip", "large"]:
176                yield mode, protocol, test_name
177
178
179@ksft_variants(_gro_variants())
180def test(cfg, mode, protocol, test_name):
181    """Run a single GRO test with retries."""
182
183    ipver = "6" if protocol[-1] == "6" else "4"
184    cfg.require_ipver(ipver)
185
186    _setup(cfg, mode, test_name)
187
188    base_cmd_args = [
189        f"--{protocol}",
190        f"--dmac {_resolve_dmac(cfg, ipver)}",
191        f"--smac {cfg.remote_dev['address']}",
192        f"--daddr {cfg.addr_v[ipver]}",
193        f"--saddr {cfg.remote_addr_v[ipver]}",
194        f"--test {test_name}",
195        "--verbose"
196    ]
197    base_args = " ".join(base_cmd_args)
198
199    # Each test is run 6 times to deflake, because given the receive timing,
200    # not all packets that should coalesce will be considered in the same flow
201    # on every try.
202    max_retries = 6
203    for attempt in range(max_retries):
204        rx_cmd = f"{cfg.bin_local} {base_args} --rx --iface {cfg.ifname}"
205        tx_cmd = f"{cfg.bin_remote} {base_args} --iface {cfg.remote_ifname}"
206
207        fail_now = attempt >= max_retries - 1
208
209        with bkg(rx_cmd, ksft_ready=True, exit_wait=True,
210                 fail=fail_now) as rx_proc:
211            cmd(tx_cmd, host=cfg.remote)
212
213        if rx_proc.ret == 0:
214            return
215
216        ksft_pr(rx_proc)
217
218        if test_name == "large" and os.environ.get("KSFT_MACHINE_SLOW"):
219            ksft_pr(f"Ignoring {protocol}/{test_name} failure due to slow environment")
220            return
221
222        ksft_pr(f"Attempt {attempt + 1}/{max_retries} failed, retrying...")
223
224
225def main() -> None:
226    """ Ksft boiler plate main """
227
228    with NetDrvEpEnv(__file__) as cfg:
229        ksft_run(cases=[test], args=(cfg,))
230    ksft_exit()
231
232
233if __name__ == "__main__":
234    main()
235