xref: /linux/tools/testing/selftests/drivers/net/gro_lib.py (revision f225a7317c18f2009fb4cdcc75337c726b6dda88)
1# SPDX-License-Identifier: GPL-2.0
2
3"""
4GRO (Generic Receive Offload) conformance tests.
5
6Validates that GRO coalescing works correctly by running the gro
7binary in different configurations and checking for correct packet
8coalescing behavior.
9
10The same test cases are run against all three coalescing implementations,
11one mode per test program, because a full sweep takes too long to fit in
12a single test's timeout:
13  - gro_sw.py:  SW GRO (generic-receive-offload)
14  - gro_hw.py:  HW GRO (rx-gro-hw)
15  - gro_lro.py: LRO    (large-receive-offload)
16
17Test cases:
18  - data_same: Same size data packets coalesce
19  - data_lrg_sml: Large packet followed by smaller one coalesces
20  - data_lrg_1byte: Large packet followed by 1B one coalesces (Ethernet padding)
21  - data_sml_lrg: Small packet followed by larger one doesn't coalesce
22  - ack: Pure ACK packets do not coalesce
23  - flags_psh: Packets with PSH flag don't coalesce
24  - flags_syn: Packets with SYN flag don't coalesce
25  - flags_rst: Packets with RST flag don't coalesce
26  - flags_urg: Packets with URG flag don't coalesce
27  - flags_cwr: Packets with CWR flag don't coalesce
28  - tcp_csum: Packets with incorrect checksum don't coalesce
29  - tcp_seq: Packets with non-consecutive seqno don't coalesce
30  - tcp_ts: Packets with different timestamp options don't coalesce
31  - tcp_opt: Packets with different TCP options don't coalesce
32  - ip_ecn: Packets with different ECN don't coalesce
33  - ip_tos: Packets with different TOS don't coalesce
34  - ip_ttl: (IPv4) Packets with different TTL don't coalesce
35  - ip_opt: (IPv4) Packets with IP options don't coalesce
36  - ip_frag4: (IPv4) IPv4 fragments don't coalesce
37  - ip_id_df*: (IPv4) IP ID field coalescing tests
38  - ip_frag6: (IPv6) IPv6 fragments don't coalesce
39  - ip_v6ext_same: (IPv6) IPv6 ext header with same payload coalesces
40  - ip_v6ext_diff: (IPv6) IPv6 ext header with different payload doesn't coalesce
41  - large_max: Packets exceeding GRO_MAX_SIZE don't coalesce
42  - large_rem: Large packet remainder handling
43"""
44
45import glob
46import os
47import re
48from lib.py import ksft_run, ksft_exit, ksft_pr
49from lib.py import NetDrvEpEnv, KsftFailEx, KsftXfailEx
50from lib.py import NetdevFamily, EthtoolFamily
51from lib.py import bkg, cmd, defer, ethtool, ip
52from lib.py import ksft_variants, KsftNamedVariant
53
54
55# gro.c uses hardcoded DPORT=8000
56GRO_DPORT = 8000
57
58
59def _resolve_dmac(cfg, ipver):
60    """
61    Find the destination MAC address remote host should use to send packets
62    towards the local host. It may be a router / gateway address.
63    """
64
65    attr = "dmac" + ipver
66    # Cache the response across test cases
67    if hasattr(cfg, attr):
68        return getattr(cfg, attr)
69
70    route = ip(f"-{ipver} route get {cfg.addr_v[ipver]}",
71               json=True, host=cfg.remote)[0]
72    gw = route.get("gateway")
73    # Local L2 segment, address directly
74    if not gw:
75        setattr(cfg, attr, cfg.dev['address'])
76        return getattr(cfg, attr)
77
78    # ping to make sure neighbor is resolved,
79    # bind to an interface, for v6 the GW is likely link local
80    cmd(f"ping -c1 -W0 -I{cfg.remote_ifname} {gw}", host=cfg.remote)
81
82    neigh = ip(f"neigh get {gw} dev {cfg.remote_ifname}",
83               json=True, host=cfg.remote)[0]
84    setattr(cfg, attr, neigh['lladdr'])
85    return getattr(cfg, attr)
86
87
88def _write_defer_restore(cfg, path, val, defer_undo=False):
89    with open(path, "r", encoding="utf-8") as fp:
90        orig_val = fp.read().strip()
91        if str(val) == orig_val:
92            return
93    with open(path, "w", encoding="utf-8") as fp:
94        fp.write(val)
95    if defer_undo:
96        defer(_write_defer_restore, cfg, path, orig_val)
97
98
99def _set_mtu_restore(dev, mtu, host):
100    if dev['mtu'] < mtu:
101        ip(f"link set dev {dev['ifname']} mtu {mtu}", host=host)
102        defer(ip, f"link set dev {dev['ifname']} mtu {dev['mtu']}", host=host)
103
104
105def _set_ethtool_feat(dev, current, feats, host=None):
106    s2n = {True: "on", False: "off"}
107
108    new = ["-K", dev]
109    old = ["-K", dev]
110    no_change = True
111    for name, state in feats.items():
112        new += [name, s2n[state]]
113        old += [name, s2n[current[name]["active"]]]
114
115        if current[name]["active"] != state:
116            no_change = False
117            if current[name]["fixed"]:
118                raise KsftXfailEx(f"Device does not support {name}")
119    if no_change:
120        return
121
122    eth_cmd = ethtool(" ".join(new), host=host)
123    defer(ethtool, " ".join(old), host=host)
124
125    # If ethtool printed something kernel must have modified some features
126    if eth_cmd.stdout:
127        ksft_pr(eth_cmd)
128
129
130def _get_queue_stats(cfg, queue_id):
131    """Get stats for a specific Rx queue."""
132    cfg.wait_hw_stats_settle()
133    data = cfg.netnl.qstats_get({"ifindex": cfg.ifindex, "scope": ["queue"]},
134                                dump=True)
135    for q in data:
136        if q.get('queue-type') == 'rx' and q.get('queue-id') == queue_id:
137            return q
138    return {}
139
140
141def _require_ntuple(cfg):
142    features = ethtool(f"-k {cfg.ifname}", json=True)[0]
143    if not features["ntuple-filters"]["active"]:
144        if features["ntuple-filters"]["fixed"]:
145            raise KsftXfailEx("Device does not support ntuple-filters")
146        ethtool(f"-K {cfg.ifname} ntuple-filters on")
147        defer(ethtool, f"-K {cfg.ifname} ntuple-filters off")
148
149
150def _setup_isolated_queue(cfg):
151    """Set up an isolated queue for testing using ntuple filter.
152
153    Remove queue 1 from the default RSS context and steer test traffic to it.
154    """
155    _require_ntuple(cfg)
156    test_queue = 1
157
158    qcnt = len(glob.glob(f"/sys/class/net/{cfg.ifname}/queues/rx-*"))
159    if qcnt < 2:
160        raise KsftXfailEx(f"Need at least 2 queues, have {qcnt}")
161
162    # Remove queue 1 from default RSS context by setting its weight to 0
163    weights = ["1"] * qcnt
164    weights[test_queue] = "0"
165    ethtool(f"-X {cfg.ifname} weight " + " ".join(weights))
166    defer(ethtool, f"-X {cfg.ifname} default")
167
168    # Set up ntuple filter to steer our test traffic to the isolated queue
169    flow  = f"flow-type tcp{cfg.addr_ipver} "
170    flow += f"dst-ip {cfg.addr} dst-port {GRO_DPORT} action {test_queue}"
171    output = ethtool(f"-N {cfg.ifname} {flow}").stdout
172    ntuple_id = int(output.split()[-1])
173    defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
174
175    return test_queue
176
177
178def _setup_queue_count(cfg, num_queues):
179    """Configure the NIC to use a specific number of queues."""
180    channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
181    ch_max = channels.get('combined-max', 0)
182    qcnt = channels['combined-count']
183
184    if ch_max < num_queues:
185        raise KsftXfailEx(f"Need at least {num_queues} queues, max={ch_max}")
186
187    defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
188    ethtool(f"-L {cfg.ifname} combined {num_queues}")
189
190
191def _run_gro_bin(cfg, test_name, protocol=None, num_flows=None,
192                 order_check=False, verbose=False, fail=False):
193    """Run gro binary with given test and return the process result."""
194    if not hasattr(cfg, "bin_remote"):
195        cfg.bin_local = cfg.net_lib_dir / "gro"
196        cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)
197
198    if protocol is None:
199        ipver = cfg.addr_ipver
200        protocol = f"ipv{ipver}"
201    else:
202        ipver = "6" if protocol[-1] == "6" else "4"
203
204    dmac = _resolve_dmac(cfg, ipver)
205
206    base_args = [
207        f"--{protocol}",
208        f"--dmac {dmac}",
209        f"--smac {cfg.remote_dev['address']}",
210        f"--daddr {cfg.addr_v[ipver]}",
211        f"--saddr {cfg.remote_addr_v[ipver]}",
212        f"--test {test_name}",
213    ]
214    if num_flows:
215        base_args.append(f"--num-flows {num_flows}")
216    if order_check:
217        base_args.append("--order-check")
218    if verbose:
219        base_args.append("--verbose")
220
221    args = " ".join(base_args)
222
223    rx_cmd = f"{cfg.bin_local} {args} --rx --iface {cfg.ifname}"
224    tx_cmd = f"{cfg.bin_remote} {args} --iface {cfg.remote_ifname}"
225
226    with bkg(rx_cmd, ksft_ready=True, exit_wait=True, fail=fail) as rx_proc:
227        cmd(tx_cmd, host=cfg.remote)
228
229    return rx_proc
230
231
232def _setup(cfg, mode, test_name):
233    """ Setup hardware loopback mode for GRO testing. """
234
235    if not hasattr(cfg, "bin_remote"):
236        cfg.bin_local = cfg.net_lib_dir / "gro"
237        cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)
238
239    if not hasattr(cfg, "feat"):
240        cfg.feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
241        cfg.remote_feat = ethtool(f"-k {cfg.remote_ifname}",
242                                  host=cfg.remote, json=True)[0]
243
244    # "large_*" tests need at least 4k MTU
245    if test_name.startswith("large_"):
246        _set_mtu_restore(cfg.dev, 4096, None)
247        _set_mtu_restore(cfg.remote_dev, 4096, cfg.remote)
248
249    if mode == "sw":
250        flush_path = f"/sys/class/net/{cfg.ifname}/gro_flush_timeout"
251        irq_path = f"/sys/class/net/{cfg.ifname}/napi_defer_hard_irqs"
252
253        _write_defer_restore(cfg, flush_path, "200000", defer_undo=True)
254        _write_defer_restore(cfg, irq_path, "10", defer_undo=True)
255
256        _set_ethtool_feat(cfg.ifname, cfg.feat,
257                          {"generic-receive-offload": True,
258                           "rx-gro-hw": False,
259                           "large-receive-offload": False})
260    elif mode == "hw":
261        _set_ethtool_feat(cfg.ifname, cfg.feat,
262                          {"generic-receive-offload": False,
263                           "rx-gro-hw": True,
264                           "large-receive-offload": False})
265
266        # Some NICs treat HW GRO as a GRO sub-feature so disabling GRO
267        # will also clear HW GRO. Use a hack of installing XDP generic
268        # to skip SW GRO, even when enabled.
269        feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
270        if not feat["rx-gro-hw"]["active"]:
271            ksft_pr("Driver clears HW GRO and SW GRO is cleared, using generic XDP workaround")
272            prog = cfg.net_lib_dir / "xdp_dummy.bpf.o"
273            ip(f"link set dev {cfg.ifname} xdpgeneric obj {prog} sec xdp")
274            defer(ip, f"link set dev {cfg.ifname} xdpgeneric off")
275
276            # Attaching XDP may change features, fetch the latest state
277            feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
278
279            _set_ethtool_feat(cfg.ifname, feat,
280                              {"generic-receive-offload": True,
281                               "rx-gro-hw": True,
282                               "large-receive-offload": False})
283    elif mode == "lro":
284        # netdevsim advertises LRO for feature inheritance testing with
285        # bonding/team tests but it doesn't actually perform the offload
286        cfg.require_nsim(nsim_test=False)
287
288        _set_ethtool_feat(cfg.ifname, cfg.feat,
289                          {"generic-receive-offload": False,
290                           "rx-gro-hw": False,
291                           "large-receive-offload": True})
292
293    try:
294        # Disable TSO for local tests
295        cfg.require_nsim()  # will raise KsftXfailEx if not running on nsim
296
297        _set_ethtool_feat(cfg.remote_ifname, cfg.remote_feat,
298                          {"tcp-segmentation-offload": False},
299                          host=cfg.remote)
300    except KsftXfailEx:
301        pass
302
303
304def _gro_variants():
305    """Generator that yields all combinations of protocol and test types."""
306
307    # Tests that work for all protocols
308    common_tests = [
309        "data_same", "data_lrg_sml", "data_sml_lrg", "data_lrg_1byte",
310        "data_burst",
311        "ack",
312        "flags_psh", "flags_syn", "flags_rst", "flags_urg", "flags_cwr",
313        "tcp_csum", "tcp_seq", "tcp_ts", "tcp_opt",
314        "ip_ecn", "ip_tos",
315        "large_max", "large_rem",
316    ]
317
318    # Tests specific to IPv4
319    ipv4_tests = [
320        "ip_csum",
321        "ip_ttl", "ip_opt", "ip_frag4",
322        "ip_id_df1_inc", "ip_id_df1_fixed",
323        "ip_id_df0_inc", "ip_id_df0_fixed",
324        "ip_id_df1_inc_fixed", "ip_id_df1_fixed_inc",
325    ]
326
327    # Tests specific to IPv6
328    ipv6_tests = [
329        "ip_frag6", "ip_v6ext_same", "ip_v6ext_diff",
330    ]
331
332    for protocol in ["ipv4", "ipv6", "ipip", "ip6ip6"]:
333        for test_name in common_tests:
334            yield protocol, test_name
335
336        if protocol in ["ipv4", "ipip"]:
337            for test_name in ipv4_tests:
338                yield protocol, test_name
339        elif protocol == "ipv6":
340            for test_name in ipv6_tests:
341                yield protocol, test_name
342
343
344def run_test(cfg, mode, protocol, test_name):
345    """Run a single GRO test with retries."""
346
347    ipver = "6" if protocol[-1] == "6" else "4"
348    cfg.require_ipver(ipver)
349
350    _setup(cfg, mode, test_name)
351
352    # Each test is run 6 times to deflake, because given the receive timing,
353    # not all packets that should coalesce will be considered in the same flow
354    # on every try.
355    max_retries = 6
356    for attempt in range(max_retries):
357        fail_now = attempt >= max_retries - 1
358        rx_proc = _run_gro_bin(cfg, test_name, protocol=protocol,
359                               verbose=True, fail=fail_now)
360
361        if rx_proc.ret == 0:
362            return
363
364        ksft_pr(rx_proc)
365
366        # ret==42 means the receiver detected over-coalescing.
367        # This is unambiguous proof of a bug, retries can only cause
368        # false negatives.
369        if rx_proc.ret == 42:
370            raise KsftFailEx(f"GRO over-coalesced in {protocol}/{test_name}")
371
372        if test_name.startswith("large_") and os.environ.get("KSFT_MACHINE_SLOW"):
373            ksft_pr(f"Ignoring {protocol}/{test_name} failure due to slow environment")
374            return
375
376        ksft_pr(f"Attempt {attempt + 1}/{max_retries} failed, retrying...")
377
378
379@ksft_variants(_gro_variants())
380def test(cfg, mode, protocol, test_name):
381    """Run a single GRO test case."""
382    run_test(cfg, mode, protocol, test_name)
383
384
385def _capacity_variants():
386    """Generate variants for capacity test: queue setup."""
387    setups = [
388        ("isolated", _setup_isolated_queue),
389        ("1q", lambda cfg: _setup_queue_count(cfg, 1)),
390        ("8q", lambda cfg: _setup_queue_count(cfg, 8)),
391    ]
392    for name, func in setups:
393        yield KsftNamedVariant(name, func)
394
395
396@ksft_variants(_capacity_variants())
397def test_gro_capacity(cfg, mode, setup_func):
398    """
399    Probe GRO capacity.
400
401    Start with 8 flows and increase by 2x on each successful run.
402    Retry up to 3 times on failure.
403
404    Queue setup variants:
405      - isolated: Use a single queue isolated from RSS
406      - 1q: Configure NIC to use 1 queue
407      - 8q: Configure NIC to use 8 queues
408    """
409    max_retries = 3
410
411    _setup(cfg, mode, "capacity")
412    queue_id = setup_func(cfg)
413
414    num_flows = 8
415    while True:
416        success = False
417        for attempt in range(max_retries):
418            if queue_id is not None:
419                stats_before = _get_queue_stats(cfg, queue_id)
420
421            rx_proc = _run_gro_bin(cfg, "capacity", num_flows=num_flows)
422            output = rx_proc.stdout
423
424            if queue_id is not None:
425                stats_after = _get_queue_stats(cfg, queue_id)
426                qstat_pkts = (stats_after.get('rx-packets', 0) -
427                              stats_before.get('rx-packets', 0))
428                gro_pkts = (stats_after.get('rx-hw-gro-packets', 0) -
429                            stats_before.get('rx-hw-gro-packets', 0))
430                qstat_str = f" qstat={qstat_pkts} hw-gro={gro_pkts}"
431            else:
432                qstat_str = ""
433
434            # Parse and print STATS line
435            match = re.search(
436                r'STATS: received=(\d+) wire=(\d+) coalesced=(\d+)', output)
437            if match:
438                received = int(match.group(1))
439                wire = int(match.group(2))
440                coalesced = int(match.group(3))
441                status = "PASS" if received == num_flows else "MISS"
442                ksft_pr(f"flows={num_flows} attempt={attempt + 1} "
443                        f"received={received} wire={wire} "
444                        f"coalesced={coalesced}{qstat_str} [{status}]")
445                if received == num_flows:
446                    success = True
447                    break
448            else:
449                ksft_pr(rx_proc)
450                ksft_pr(f"flows={num_flows} attempt={attempt + 1}"
451                        f"{qstat_str} [FAIL - can't parse stats]")
452
453        if not success:
454            ksft_pr(f"Stopped at {num_flows} flows")
455            break
456
457        num_flows *= 2
458
459
460def gro_main(src_path, mode) -> None:
461    """ Ksft boiler plate main, run all the cases in the given mode """
462
463    with NetDrvEpEnv(src_path) as cfg:
464        cfg.ethnl = EthtoolFamily()
465        cfg.netnl = NetdevFamily()
466        ksft_run(cases=[test, test_gro_capacity], args=(cfg, mode))
467    ksft_exit()
468