xref: /linux/tools/testing/selftests/drivers/net/gro_lib.py (revision 42a9fb3382fc2573e92f41d203b095d9a372cfc9)
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                          check=False)
268
269        # Some NICs treat HW GRO as a GRO sub-feature so disabling GRO
270        # will also clear HW GRO. Use a hack of installing XDP generic
271        # to skip SW GRO, even when enabled.
272        feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
273        if not feat["rx-gro-hw"]["active"]:
274            ksft_pr("Driver clears HW GRO and SW GRO is cleared, using generic XDP workaround")
275            prog = cfg.net_lib_dir / "xdp_dummy.bpf.o"
276            ip(f"link set dev {cfg.ifname} xdpgeneric obj {prog} sec xdp")
277            defer(ip, f"link set dev {cfg.ifname} xdpgeneric off")
278
279            # Attaching XDP may change features, fetch the latest state
280            feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
281
282            set_ethtool_feat(cfg.ifname, feat,
283                             {"generic-receive-offload": True,
284                              "rx-gro-hw": True,
285                              "large-receive-offload": False})
286    elif mode == "lro":
287        # netdevsim advertises LRO for feature inheritance testing with
288        # bonding/team tests but it doesn't actually perform the offload
289        cfg.require_nsim(nsim_test=False)
290
291        set_ethtool_feat(cfg.ifname, cfg.feat,
292                         {"generic-receive-offload": False,
293                          "rx-gro-hw": False,
294                          "large-receive-offload": True})
295
296    try:
297        # Disable TSO for local tests
298        cfg.require_nsim()  # will raise KsftXfailEx if not running on nsim
299
300        set_ethtool_feat(cfg.remote_ifname, cfg.remote_feat,
301                         {"tcp-segmentation-offload": False},
302                         host=cfg.remote)
303    except KsftXfailEx:
304        pass
305
306    if test_name.startswith("big_tcp_"):
307        _set_gro_size_restore(cfg, BIG_TCP_GRO_MAX_SIZE)
308
309
310def _gro_variants():
311    """Generator that yields all combinations of protocol and test types."""
312
313    # Tests that work for all protocols
314    common_tests = [
315        "data_same", "data_lrg_sml", "data_sml_lrg", "data_lrg_1byte",
316        "data_burst",
317        "ack",
318        "flags_psh", "flags_syn", "flags_rst", "flags_urg", "flags_cwr",
319        "tcp_csum", "tcp_seq", "tcp_ts", "tcp_opt",
320        "ip_ecn", "ip_tos",
321        "large_max", "large_rem",
322    ]
323
324    big_tcp_tests = [
325        "big_tcp_data_same", "big_tcp_data_lrg_sml",
326        "big_tcp_tcp_seq", "big_tcp_large_max",
327    ]
328
329    # Tests specific to IPv4
330    ipv4_tests = [
331        "ip_csum",
332        "ip_ttl", "ip_opt", "ip_frag4",
333        "ip_id_df1_inc", "ip_id_df1_fixed",
334        "ip_id_df0_inc", "ip_id_df0_fixed",
335        "ip_id_df1_inc_fixed", "ip_id_df1_fixed_inc",
336    ]
337
338    # Tests specific to IPv6
339    ipv6_tests = [
340        "ip_frag6", "ip_v6ext_same", "ip_v6ext_diff",
341    ]
342
343    for protocol in ["ipv4", "ipv6", "ipip", "ip6ip6"]:
344        for test_name in common_tests:
345            yield protocol, test_name
346
347        if protocol in ["ipv4", "ipv6"]:
348            for test_name in big_tcp_tests:
349                yield protocol, test_name
350
351        if protocol in ["ipv4", "ipip"]:
352            for test_name in ipv4_tests:
353                yield protocol, test_name
354        elif protocol == "ipv6":
355            for test_name in ipv6_tests:
356                yield protocol, test_name
357
358
359def run_test(cfg, mode, protocol, test_name):
360    """Run a single GRO test with retries."""
361
362    ipver = "6" if protocol[-1] == "6" else "4"
363    cfg.require_ipver(ipver)
364
365    _setup(cfg, mode, test_name)
366
367    # Each test is run 6 times to deflake, because given the receive timing,
368    # not all packets that should coalesce will be considered in the same flow
369    # on every try.
370    max_retries = 6
371    for attempt in range(max_retries):
372        fail_now = attempt >= max_retries - 1
373        rx_proc = _run_gro_bin(cfg, test_name, protocol=protocol,
374                               verbose=True, fail=fail_now)
375
376        if rx_proc.ret == 0:
377            return
378
379        ksft_pr(rx_proc)
380
381        # ret==42 means the receiver detected over-coalescing.
382        # This is unambiguous proof of a bug, retries can only cause
383        # false negatives.
384        if rx_proc.ret == 42:
385            raise KsftFailEx(f"GRO over-coalesced in {protocol}/{test_name}")
386
387        if (test_name.startswith(("large_", "big_tcp_")) and
388                os.environ.get("KSFT_MACHINE_SLOW")):
389            ksft_pr(f"Ignoring {protocol}/{test_name} failure due to slow environment")
390            return
391
392        ksft_pr(f"Attempt {attempt + 1}/{max_retries} failed, retrying...")
393
394
395@ksft_variants(_gro_variants())
396def test(cfg, mode, protocol, test_name):
397    """Run a single GRO test case."""
398    run_test(cfg, mode, protocol, test_name)
399
400
401def _capacity_variants():
402    """Generate variants for capacity test: queue setup."""
403    setups = [
404        ("isolated", _setup_isolated_queue),
405        ("1q", lambda cfg: _setup_queue_count(cfg, 1)),
406        ("8q", lambda cfg: _setup_queue_count(cfg, 8)),
407    ]
408    for name, func in setups:
409        yield KsftNamedVariant(name, func)
410
411
412@ksft_variants(_capacity_variants())
413def test_gro_capacity(cfg, mode, setup_func):
414    """
415    Probe GRO capacity.
416
417    Start with 8 flows and increase by 2x on each successful run.
418    Retry up to 3 times on failure.
419
420    Queue setup variants:
421      - isolated: Use a single queue isolated from RSS
422      - 1q: Configure NIC to use 1 queue
423      - 8q: Configure NIC to use 8 queues
424    """
425    max_retries = 3
426
427    _setup(cfg, mode, "capacity")
428    queue_id = setup_func(cfg)
429
430    num_flows = 8
431    while True:
432        success = False
433        for attempt in range(max_retries):
434            if queue_id is not None:
435                stats_before = _get_queue_stats(cfg, queue_id)
436
437            rx_proc = _run_gro_bin(cfg, "capacity", num_flows=num_flows)
438            output = rx_proc.stdout
439
440            if queue_id is not None:
441                stats_after = _get_queue_stats(cfg, queue_id)
442                qstat_pkts = (stats_after.get('rx-packets', 0) -
443                              stats_before.get('rx-packets', 0))
444                gro_pkts = (stats_after.get('rx-hw-gro-packets', 0) -
445                            stats_before.get('rx-hw-gro-packets', 0))
446                qstat_str = f" qstat={qstat_pkts} hw-gro={gro_pkts}"
447            else:
448                qstat_str = ""
449
450            # Parse and print STATS line
451            match = re.search(
452                r'STATS: received=(\d+) wire=(\d+) coalesced=(\d+)', output)
453            if match:
454                received = int(match.group(1))
455                wire = int(match.group(2))
456                coalesced = int(match.group(3))
457                status = "PASS" if received == num_flows else "MISS"
458                ksft_pr(f"flows={num_flows} attempt={attempt + 1} "
459                        f"received={received} wire={wire} "
460                        f"coalesced={coalesced}{qstat_str} [{status}]")
461                if received == num_flows:
462                    success = True
463                    break
464            else:
465                ksft_pr(rx_proc)
466                ksft_pr(f"flows={num_flows} attempt={attempt + 1}"
467                        f"{qstat_str} [FAIL - can't parse stats]")
468
469        if not success:
470            ksft_pr(f"Stopped at {num_flows} flows")
471            break
472
473        num_flows *= 2
474
475
476def gro_main(src_path, mode) -> None:
477    """ Ksft boiler plate main, run all the cases in the given mode """
478
479    with NetDrvEpEnv(src_path) as cfg:
480        cfg.ethnl = EthtoolFamily()
481        cfg.netnl = NetdevFamily()
482        ksft_run(cases=[test, test_gro_capacity], args=(cfg, mode))
483    ksft_exit()
484