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