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