projects/linux·blob master · refs

473 lines · 16.4 KiB

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