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