| 1 | |
| 2 | |
| 3 |
|
| 4 | |
| 5 | GRO (Generic Receive Offload) conformance tests.
|
| 6 |
|
| 7 | Validates that GRO coalescing works correctly by running the gro
|
| 8 | binary in different configurations and checking for correct packet
|
| 9 | coalescing behavior.
|
| 10 |
|
| 11 | Test 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 |
|
| 39 | import glob
|
| 40 | import os
|
| 41 | import re
|
| 42 | from lib.py import ksft_run, ksft_exit, ksft_pr
|
| 43 | from lib.py import NetDrvEpEnv, KsftFailEx, KsftXfailEx
|
| 44 | from lib.py import NetdevFamily, EthtoolFamily
|
| 45 | from lib.py import bkg, cmd, defer, ethtool, ip
|
| 46 | from lib.py import ksft_variants, KsftNamedVariant
|
| 47 |
|
| 48 |
|
| 49 | |
| 50 | GRO_DPORT = 8000
|
| 51 |
|
| 52 |
|
| 53 | def _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 | |
| 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 | |
| 68 | if not gw:
|
| 69 | setattr(cfg, attr, cfg.dev['address'])
|
| 70 | return getattr(cfg, attr)
|
| 71 |
|
| 72 | |
| 73 | |
| 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 |
|
| 82 | def _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 |
|
| 93 | def _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 |
|
| 99 | def _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 | |
| 120 | if eth_cmd.stdout:
|
| 121 | ksft_pr(eth_cmd)
|
| 122 |
|
| 123 |
|
| 124 | def _get_queue_stats(cfg, queue_id):
|
| 125 |
|
| 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 |
|
| 135 | def _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 |
|
| 144 | def _setup_isolated_queue(cfg):
|
| 145 | |
| 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 | |
| 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 | |
| 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 |
|
| 172 | def _setup_queue_count(cfg, num_queues):
|
| 173 |
|
| 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 |
|
| 185 | def _run_gro_bin(cfg, test_name, protocol=None, num_flows=None,
|
| 186 | order_check=False, verbose=False, fail=False):
|
| 187 |
|
| 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 |
|
| 226 | def _setup(cfg, mode, test_name):
|
| 227 |
|
| 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 | |
| 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 | |
| 261 | |
| 262 | |
| 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 | |
| 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 | |
| 279 | |
| 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 | |
| 289 | cfg.require_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 |
|
| 298 | def _gro_variants():
|
| 299 |
|
| 300 |
|
| 301 | |
| 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 | |
| 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 | |
| 322 | ipv6_tests = [
|
| 323 | "ip_frag6", "ip_v6ext_same", "ip_v6ext_diff",
|
| 324 | ]
|
| 325 |
|
| 326 | |
| 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())
|
| 351 | def test(cfg, mode, protocol, test_name):
|
| 352 |
|
| 353 |
|
| 354 | ipver = "6" if protocol[-1] == "6" else "4"
|
| 355 | cfg.require_ipver(ipver)
|
| 356 |
|
| 357 | _setup(cfg, mode, test_name)
|
| 358 |
|
| 359 | |
| 360 | |
| 361 | |
| 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 | |
| 374 | |
| 375 | |
| 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 |
|
| 386 | def _capacity_variants():
|
| 387 |
|
| 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())
|
| 399 | def 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 | |
| 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 |
|
| 462 | def main() -> None:
|
| 463 |
|
| 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 |
|
| 472 | if __name__ == "__main__":
|
| 473 | main()
|