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