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, 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 for mode in ["sw", "hw", "lro"]: 327 for protocol in ["ipv4", "ipv6", "ipip", "ip6ip6"]: 328 for test_name in common_tests: 329 yield mode, protocol, test_name 330 331 if protocol in ["ipv4", "ipip"]: 332 for test_name in ipv4_tests: 333 yield mode, protocol, test_name 334 elif protocol == "ipv6": 335 for test_name in ipv6_tests: 336 yield mode, protocol, test_name 337 338 339@ksft_variants(_gro_variants()) 340def test(cfg, mode, protocol, test_name): 341 """Run a single GRO test with retries.""" 342 343 ipver = "6" if protocol[-1] == "6" else "4" 344 cfg.require_ipver(ipver) 345 346 _setup(cfg, mode, test_name) 347 348 # Each test is run 6 times to deflake, because given the receive timing, 349 # not all packets that should coalesce will be considered in the same flow 350 # on every try. 351 max_retries = 6 352 for attempt in range(max_retries): 353 fail_now = attempt >= max_retries - 1 354 rx_proc = _run_gro_bin(cfg, test_name, protocol=protocol, 355 verbose=True, fail=fail_now) 356 357 if rx_proc.ret == 0: 358 return 359 360 ksft_pr(rx_proc) 361 362 if test_name.startswith("large_") and os.environ.get("KSFT_MACHINE_SLOW"): 363 ksft_pr(f"Ignoring {protocol}/{test_name} failure due to slow environment") 364 return 365 366 ksft_pr(f"Attempt {attempt + 1}/{max_retries} failed, retrying...") 367 368 369def _capacity_variants(): 370 """Generate variants for capacity test: mode x queue setup.""" 371 setups = [ 372 ("isolated", _setup_isolated_queue), 373 ("1q", lambda cfg: _setup_queue_count(cfg, 1)), 374 ("8q", lambda cfg: _setup_queue_count(cfg, 8)), 375 ] 376 for mode in ["sw", "hw", "lro"]: 377 for name, func in setups: 378 yield KsftNamedVariant(f"{mode}_{name}", mode, func) 379 380 381@ksft_variants(_capacity_variants()) 382def test_gro_capacity(cfg, mode, setup_func): 383 """ 384 Probe GRO capacity. 385 386 Start with 8 flows and increase by 2x on each successful run. 387 Retry up to 3 times on failure. 388 389 Variants combine mode (sw, hw, lro) with queue setup: 390 - isolated: Use a single queue isolated from RSS 391 - 1q: Configure NIC to use 1 queue 392 - 8q: Configure NIC to use 8 queues 393 """ 394 max_retries = 3 395 396 _setup(cfg, mode, "capacity") 397 queue_id = setup_func(cfg) 398 399 num_flows = 8 400 while True: 401 success = False 402 for attempt in range(max_retries): 403 if queue_id is not None: 404 stats_before = _get_queue_stats(cfg, queue_id) 405 406 rx_proc = _run_gro_bin(cfg, "capacity", num_flows=num_flows) 407 output = rx_proc.stdout 408 409 if queue_id is not None: 410 stats_after = _get_queue_stats(cfg, queue_id) 411 qstat_pkts = (stats_after.get('rx-packets', 0) - 412 stats_before.get('rx-packets', 0)) 413 gro_pkts = (stats_after.get('rx-hw-gro-packets', 0) - 414 stats_before.get('rx-hw-gro-packets', 0)) 415 qstat_str = f" qstat={qstat_pkts} hw-gro={gro_pkts}" 416 else: 417 qstat_str = "" 418 419 # Parse and print STATS line 420 match = re.search( 421 r'STATS: received=(\d+) wire=(\d+) coalesced=(\d+)', output) 422 if match: 423 received = int(match.group(1)) 424 wire = int(match.group(2)) 425 coalesced = int(match.group(3)) 426 status = "PASS" if received == num_flows else "MISS" 427 ksft_pr(f"flows={num_flows} attempt={attempt + 1} " 428 f"received={received} wire={wire} " 429 f"coalesced={coalesced}{qstat_str} [{status}]") 430 if received == num_flows: 431 success = True 432 break 433 else: 434 ksft_pr(rx_proc) 435 ksft_pr(f"flows={num_flows} attempt={attempt + 1}" 436 f"{qstat_str} [FAIL - can't parse stats]") 437 438 if not success: 439 ksft_pr(f"Stopped at {num_flows} flows") 440 break 441 442 num_flows *= 2 443 444 445def main() -> None: 446 """ Ksft boiler plate main """ 447 448 with NetDrvEpEnv(__file__) as cfg: 449 cfg.ethnl = EthtoolFamily() 450 cfg.netnl = NetdevFamily() 451 ksft_run(cases=[test, test_gro_capacity], args=(cfg,)) 452 ksft_exit() 453 454 455if __name__ == "__main__": 456 main() 457