xref: /linux/tools/testing/selftests/drivers/net/gro_lib.py (revision d3630df401a8ee91117db26b14a64520aa44fbdb)
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, set_ethtool_feat
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 _get_queue_stats(cfg, queue_id):
129    """Get stats for a specific Rx queue."""
130    cfg.wait_hw_stats_settle()
131    data = cfg.netnl.qstats_get({"ifindex": cfg.ifindex, "scope": ["queue"]},
132                                dump=True)
133    for q in data:
134        if q.get('queue-type') == 'rx' and q.get('queue-id') == queue_id:
135            return q
136    return {}
137
138
139def _require_ntuple(cfg):
140    features = ethtool(f"-k {cfg.ifname}", json=True)[0]
141    if not features["ntuple-filters"]["active"]:
142        if features["ntuple-filters"]["fixed"]:
143            raise KsftXfailEx("Device does not support ntuple-filters")
144        ethtool(f"-K {cfg.ifname} ntuple-filters on")
145        defer(ethtool, f"-K {cfg.ifname} ntuple-filters off")
146
147
148def _setup_isolated_queue(cfg):
149    """Set up an isolated queue for testing using ntuple filter.
150
151    Remove queue 1 from the default RSS context and steer test traffic to it.
152    """
153    _require_ntuple(cfg)
154    test_queue = 1
155
156    qcnt = len(glob.glob(f"/sys/class/net/{cfg.ifname}/queues/rx-*"))
157    if qcnt < 2:
158        raise KsftXfailEx(f"Need at least 2 queues, have {qcnt}")
159
160    # Remove queue 1 from default RSS context by setting its weight to 0
161    weights = ["1"] * qcnt
162    weights[test_queue] = "0"
163    ethtool(f"-X {cfg.ifname} weight " + " ".join(weights))
164    defer(ethtool, f"-X {cfg.ifname} default")
165
166    # Set up ntuple filter to steer our test traffic to the isolated queue
167    flow  = f"flow-type tcp{cfg.addr_ipver} "
168    flow += f"dst-ip {cfg.addr} dst-port {GRO_DPORT} action {test_queue}"
169    output = ethtool(f"-N {cfg.ifname} {flow}").stdout
170    ntuple_id = int(output.split()[-1])
171    defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
172
173    return test_queue
174
175
176def _setup_queue_count(cfg, num_queues):
177    """Configure the NIC to use a specific number of queues."""
178    channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
179    ch_max = channels.get('combined-max', 0)
180    qcnt = channels['combined-count']
181
182    if ch_max < num_queues:
183        raise KsftXfailEx(f"Need at least {num_queues} queues, max={ch_max}")
184
185    defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
186    ethtool(f"-L {cfg.ifname} combined {num_queues}")
187
188
189def _run_gro_bin(cfg, test_name, protocol=None, num_flows=None,
190                 order_check=False, verbose=False, fail=False):
191    """Run gro binary with given test and return the process result."""
192    if not hasattr(cfg, "bin_remote"):
193        cfg.bin_local = cfg.net_lib_dir / "gro"
194        cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)
195
196    if protocol is None:
197        ipver = cfg.addr_ipver
198        protocol = f"ipv{ipver}"
199    else:
200        ipver = "6" if protocol[-1] == "6" else "4"
201
202    dmac = _resolve_dmac(cfg, ipver)
203
204    base_args = [
205        f"--{protocol}",
206        f"--dmac {dmac}",
207        f"--smac {cfg.remote_dev['address']}",
208        f"--daddr {cfg.addr_v[ipver]}",
209        f"--saddr {cfg.remote_addr_v[ipver]}",
210        f"--test {test_name}",
211    ]
212    if num_flows:
213        base_args.append(f"--num-flows {num_flows}")
214    if order_check:
215        base_args.append("--order-check")
216    if verbose:
217        base_args.append("--verbose")
218
219    args = " ".join(base_args)
220
221    rx_cmd = f"{cfg.bin_local} {args} --rx --iface {cfg.ifname}"
222    tx_cmd = f"{cfg.bin_remote} {args} --iface {cfg.remote_ifname}"
223
224    with bkg(rx_cmd, ksft_ready=True, exit_wait=True, fail=fail) as rx_proc:
225        cmd(tx_cmd, host=cfg.remote)
226
227    return rx_proc
228
229
230def _setup(cfg, mode, test_name):
231    """ Setup hardware loopback mode for GRO testing. """
232
233    if not hasattr(cfg, "bin_remote"):
234        cfg.bin_local = cfg.net_lib_dir / "gro"
235        cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)
236
237    if not hasattr(cfg, "feat"):
238        cfg.feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
239        cfg.remote_feat = ethtool(f"-k {cfg.remote_ifname}",
240                                  host=cfg.remote, json=True)[0]
241
242    # "large_*" tests need at least 4k MTU
243    if test_name.startswith("large_"):
244        _set_mtu_restore(cfg.dev, 4096, None)
245        _set_mtu_restore(cfg.remote_dev, 4096, cfg.remote)
246
247    if mode == "sw":
248        flush_path = f"/sys/class/net/{cfg.ifname}/gro_flush_timeout"
249        irq_path = f"/sys/class/net/{cfg.ifname}/napi_defer_hard_irqs"
250
251        # "big_tcp_*" tests need a longer timeout, use 2x the regular timeout
252        if test_name.startswith("big_tcp_"):
253            ctl_file_write(flush_path, "400000")
254        else:
255            ctl_file_write(flush_path, "200000")
256        ctl_file_write(irq_path, "10")
257
258        set_ethtool_feat(cfg.ifname, cfg.feat,
259                         {"generic-receive-offload": True,
260                          "rx-gro-hw": False,
261                          "large-receive-offload": False})
262    elif mode == "hw":
263        set_ethtool_feat(cfg.ifname, cfg.feat,
264                         {"generic-receive-offload": False,
265                          "rx-gro-hw": True,
266                          "large-receive-offload": False})
267
268        # Some NICs treat HW GRO as a GRO sub-feature so disabling GRO
269        # will also clear HW GRO. Use a hack of installing XDP generic
270        # to skip SW GRO, even when enabled.
271        feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
272        if not feat["rx-gro-hw"]["active"]:
273            ksft_pr("Driver clears HW GRO and SW GRO is cleared, using generic XDP workaround")
274            prog = cfg.net_lib_dir / "xdp_dummy.bpf.o"
275            ip(f"link set dev {cfg.ifname} xdpgeneric obj {prog} sec xdp")
276            defer(ip, f"link set dev {cfg.ifname} xdpgeneric off")
277
278            # Attaching XDP may change features, fetch the latest state
279            feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
280
281            set_ethtool_feat(cfg.ifname, feat,
282                             {"generic-receive-offload": True,
283                              "rx-gro-hw": True,
284                              "large-receive-offload": False})
285    elif mode == "lro":
286        # netdevsim advertises LRO for feature inheritance testing with
287        # bonding/team tests but it doesn't actually perform the offload
288        cfg.require_nsim(nsim_test=False)
289
290        set_ethtool_feat(cfg.ifname, cfg.feat,
291                         {"generic-receive-offload": False,
292                          "rx-gro-hw": False,
293                          "large-receive-offload": True})
294
295    try:
296        # Disable TSO for local tests
297        cfg.require_nsim()  # will raise KsftXfailEx if not running on nsim
298
299        set_ethtool_feat(cfg.remote_ifname, cfg.remote_feat,
300                         {"tcp-segmentation-offload": False},
301                         host=cfg.remote)
302    except KsftXfailEx:
303        pass
304
305    if test_name.startswith("big_tcp_"):
306        _set_gro_size_restore(cfg, BIG_TCP_GRO_MAX_SIZE)
307
308
309def _gro_variants():
310    """Generator that yields all combinations of protocol and test types."""
311
312    # Tests that work for all protocols
313    common_tests = [
314        "data_same", "data_lrg_sml", "data_sml_lrg", "data_lrg_1byte",
315        "data_burst",
316        "ack",
317        "flags_psh", "flags_syn", "flags_rst", "flags_urg", "flags_cwr",
318        "tcp_csum", "tcp_seq", "tcp_ts", "tcp_opt",
319        "ip_ecn", "ip_tos",
320        "large_max", "large_rem",
321    ]
322
323    big_tcp_tests = [
324        "big_tcp_data_same", "big_tcp_data_lrg_sml",
325        "big_tcp_tcp_seq", "big_tcp_large_max",
326    ]
327
328    # Tests specific to IPv4
329    ipv4_tests = [
330        "ip_csum",
331        "ip_ttl", "ip_opt", "ip_frag4",
332        "ip_id_df1_inc", "ip_id_df1_fixed",
333        "ip_id_df0_inc", "ip_id_df0_fixed",
334        "ip_id_df1_inc_fixed", "ip_id_df1_fixed_inc",
335    ]
336
337    # Tests specific to IPv6
338    ipv6_tests = [
339        "ip_frag6", "ip_v6ext_same", "ip_v6ext_diff",
340    ]
341
342    for protocol in ["ipv4", "ipv6", "ipip", "ip6ip6"]:
343        for test_name in common_tests:
344            yield protocol, test_name
345
346        if protocol in ["ipv4", "ipv6"]:
347            for test_name in big_tcp_tests:
348                yield protocol, test_name
349
350        if protocol in ["ipv4", "ipip"]:
351            for test_name in ipv4_tests:
352                yield protocol, test_name
353        elif protocol == "ipv6":
354            for test_name in ipv6_tests:
355                yield protocol, test_name
356
357
358def run_test(cfg, mode, protocol, test_name):
359    """Run a single GRO test with retries."""
360
361    ipver = "6" if protocol[-1] == "6" else "4"
362    cfg.require_ipver(ipver)
363
364    _setup(cfg, mode, test_name)
365
366    # Each test is run 6 times to deflake, because given the receive timing,
367    # not all packets that should coalesce will be considered in the same flow
368    # on every try.
369    max_retries = 6
370    for attempt in range(max_retries):
371        fail_now = attempt >= max_retries - 1
372        rx_proc = _run_gro_bin(cfg, test_name, protocol=protocol,
373                               verbose=True, fail=fail_now)
374
375        if rx_proc.ret == 0:
376            return
377
378        ksft_pr(rx_proc)
379
380        # ret==42 means the receiver detected over-coalescing.
381        # This is unambiguous proof of a bug, retries can only cause
382        # false negatives.
383        if rx_proc.ret == 42:
384            raise KsftFailEx(f"GRO over-coalesced in {protocol}/{test_name}")
385
386        if (test_name.startswith(("large_", "big_tcp_")) and
387                os.environ.get("KSFT_MACHINE_SLOW")):
388            ksft_pr(f"Ignoring {protocol}/{test_name} failure due to slow environment")
389            return
390
391        ksft_pr(f"Attempt {attempt + 1}/{max_retries} failed, retrying...")
392
393
394@ksft_variants(_gro_variants())
395def test(cfg, mode, protocol, test_name):
396    """Run a single GRO test case."""
397    run_test(cfg, mode, protocol, test_name)
398
399
400def _capacity_variants():
401    """Generate variants for capacity test: queue setup."""
402    setups = [
403        ("isolated", _setup_isolated_queue),
404        ("1q", lambda cfg: _setup_queue_count(cfg, 1)),
405        ("8q", lambda cfg: _setup_queue_count(cfg, 8)),
406    ]
407    for name, func in setups:
408        yield KsftNamedVariant(name, func)
409
410
411@ksft_variants(_capacity_variants())
412def test_gro_capacity(cfg, mode, setup_func):
413    """
414    Probe GRO capacity.
415
416    Start with 8 flows and increase by 2x on each successful run.
417    Retry up to 3 times on failure.
418
419    Queue setup variants:
420      - isolated: Use a single queue isolated from RSS
421      - 1q: Configure NIC to use 1 queue
422      - 8q: Configure NIC to use 8 queues
423    """
424    max_retries = 3
425
426    _setup(cfg, mode, "capacity")
427    queue_id = setup_func(cfg)
428
429    num_flows = 8
430    while True:
431        success = False
432        for attempt in range(max_retries):
433            if queue_id is not None:
434                stats_before = _get_queue_stats(cfg, queue_id)
435
436            rx_proc = _run_gro_bin(cfg, "capacity", num_flows=num_flows)
437            output = rx_proc.stdout
438
439            if queue_id is not None:
440                stats_after = _get_queue_stats(cfg, queue_id)
441                qstat_pkts = (stats_after.get('rx-packets', 0) -
442                              stats_before.get('rx-packets', 0))
443                gro_pkts = (stats_after.get('rx-hw-gro-packets', 0) -
444                            stats_before.get('rx-hw-gro-packets', 0))
445                qstat_str = f" qstat={qstat_pkts} hw-gro={gro_pkts}"
446            else:
447                qstat_str = ""
448
449            # Parse and print STATS line
450            match = re.search(
451                r'STATS: received=(\d+) wire=(\d+) coalesced=(\d+)', output)
452            if match:
453                received = int(match.group(1))
454                wire = int(match.group(2))
455                coalesced = int(match.group(3))
456                status = "PASS" if received == num_flows else "MISS"
457                ksft_pr(f"flows={num_flows} attempt={attempt + 1} "
458                        f"received={received} wire={wire} "
459                        f"coalesced={coalesced}{qstat_str} [{status}]")
460                if received == num_flows:
461                    success = True
462                    break
463            else:
464                ksft_pr(rx_proc)
465                ksft_pr(f"flows={num_flows} attempt={attempt + 1}"
466                        f"{qstat_str} [FAIL - can't parse stats]")
467
468        if not success:
469            ksft_pr(f"Stopped at {num_flows} flows")
470            break
471
472        num_flows *= 2
473
474
475def gro_main(src_path, mode) -> None:
476    """ Ksft boiler plate main, run all the cases in the given mode """
477
478    with NetDrvEpEnv(src_path) as cfg:
479        cfg.ethnl = EthtoolFamily()
480        cfg.netnl = NetdevFamily()
481        ksft_run(cases=[test, test_gro_capacity], args=(cfg, mode))
482    ksft_exit()
483