#!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0 import re import time import threading from os import path from lib.py import ( ksft_run, ksft_exit, ksft_eq, ksft_in, ksft_not_in, ksft_raises, ) from lib.py import ( NetDrvContEnv, NetNSEnter, EthtoolFamily, NetdevFamily, RtnlFamily, ) from lib.py import ( Netlink, bkg, cmd, defer, ethtool, ip, rand_port, wait_port_listen, ) from lib.py import KsftSkipEx, CmdExitFailure # iou-zcrx exits with 42 from setup_zcrx() when the NIC does not advertise # QCFG_RX_PAGE_SIZE (or otherwise rejects the requested rx_buf_len). SKIP_CODE = 42 def _restore_hugepages(count): with open("/proc/sys/vm/nr_hugepages", "w", encoding="utf-8") as f: f.write(str(count)) def _mp_clear_wait(cfg, src_queue): """Wait for the io_uring memory provider to clear from the leased physical queue; io_uring tears it down asynchronously after the process holding the ifq exits.""" netdevnl = NetdevFamily() deadline = time.time() + 5 while time.time() < deadline: queue_info = netdevnl.queue_get( {"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"} ) if "io-uring" not in queue_info: return time.sleep(0.1) raise TimeoutError("Timed out waiting for memory provider to clear") def _create_netkit_pair(cfg, rxqueues=2): if cfg.nk_host_ifname: cmd(f"ip link del dev {cfg.nk_host_ifname}", fail=False) cfg.nk_host_ifname = None cfg.nk_guest_ifname = None cfg.detach_bpf() all_links = ip("-d link show", json=True) old_idxs = { link["ifindex"] for link in all_links if link.get("linkinfo", {}).get("info_kind") == "netkit" } rtnl = RtnlFamily() rtnl.newlink( { "linkinfo": { "kind": "netkit", "data": { "mode": "l2", "policy": "forward", "peer-policy": "forward", }, }, "num-rx-queues": rxqueues, }, flags=[Netlink.NLM_F_CREATE, Netlink.NLM_F_EXCL], ) all_links = ip("-d link show", json=True) nk_links = [ link for link in all_links if link.get("linkinfo", {}).get("info_kind") == "netkit" and link["ifindex"] not in old_idxs ] if len(nk_links) != 2: raise KsftSkipEx("Failed to create netkit pair") nk_links.sort(key=lambda x: x["ifindex"]) cfg.nk_host_ifname = nk_links[1]["ifname"] cfg.nk_guest_ifname = nk_links[0]["ifname"] cfg.nk_host_ifindex = nk_links[1]["ifindex"] cfg.nk_guest_ifindex = nk_links[0]["ifindex"] ip(f"link set dev {cfg.nk_guest_ifname} netns {cfg.netns.name}") ip(f"link set dev {cfg.nk_host_ifname} up") ip(f"-6 addr add fe80::1/64 dev {cfg.nk_host_ifname} nodad") ip( f"-6 route add {cfg.nk_guest_ipv6}/128 via fe80::2 " f"dev {cfg.nk_host_ifname}" ) ip(f"link set dev {cfg.nk_guest_ifname} up", ns=cfg.netns) ip(f"-6 addr add fe80::2/64 dev {cfg.nk_guest_ifname}", ns=cfg.netns) ip( f"-6 addr add {cfg.nk_guest_ipv6}/64 dev {cfg.nk_guest_ifname} nodad", ns=cfg.netns, ) ip( f"-6 route add default via fe80::1 dev {cfg.nk_guest_ifname}", ns=cfg.netns, ) cfg.attach_bpf() def _setup_lease(cfg, rxqueues=2): _create_netkit_pair(cfg, rxqueues=rxqueues) ethnl = EthtoolFamily() channels = ethnl.channels_get({"header": {"dev-index": cfg.ifindex}})[ "combined-count" ] if channels < 2: raise KsftSkipEx( "Test requires NETIF with at least 2 combined channels" ) src_queue = channels - 1 with NetNSEnter(str(cfg.netns)): netdevnl = NetdevFamily() bind_result = netdevnl.queue_create( { "ifindex": cfg.nk_guest_ifindex, "type": "rx", "lease": { "ifindex": cfg.ifindex, "queue": {"id": src_queue, "type": "rx"}, "netns-id": 0, }, } ) return src_queue, bind_result["id"] def _teardown_netkit(cfg): if cfg.nk_host_ifname: cmd(f"ip link del dev {cfg.nk_host_ifname}", fail=False) cfg.nk_host_ifname = None cfg.nk_guest_ifname = None def set_flow_rule(cfg, src_queue): output = ethtool( f"-N {cfg.ifname} flow-type tcp6 dst-port {cfg.port} action {src_queue}" ).stdout values = re.search(r"ID (\d+)", output).group(1) return int(values) def test_iou_zcrx(cfg) -> None: cfg.require_ipver("6") src_queue, nk_queue = _setup_lease(cfg) defer(_teardown_netkit, cfg) ethnl = EthtoolFamily() rings = ethnl.rings_get({"header": {"dev-index": cfg.ifindex}}) rx_rings = rings["rx"] hds_thresh = rings.get("hds-thresh", 0) ethnl.rings_set( { "header": {"dev-index": cfg.ifindex}, "tcp-data-split": "enabled", "hds-thresh": 0, "rx": 64, } ) defer( ethnl.rings_set, { "header": {"dev-index": cfg.ifindex}, "tcp-data-split": "unknown", "hds-thresh": hds_thresh, "rx": rx_rings, }, ) ethtool(f"-X {cfg.ifname} equal {src_queue}") defer(ethtool, f"-X {cfg.ifname} default") flow_rule_id = set_flow_rule(cfg, src_queue) defer(ethtool, f"-N {cfg.ifname} delete {flow_rule_id}") rx_cmd = ( f"{cfg.bin_local} -s -p {cfg.port} " f"-i {cfg.nk_guest_ifname} -q {nk_queue}" ) tx_cmd = f"{cfg.bin_remote} -c -h {cfg.nk_guest_ipv6} -p {cfg.port} -l 12840" with bkg(rx_cmd, exit_wait=True, ns=cfg.netns): wait_port_listen(cfg.port, proto="tcp", ns=cfg.netns) cmd(tx_cmd, host=cfg.remote) def test_iou_zcrx_large_buf(cfg) -> None: """iou-zcrx with rx_buf_len > page size, going through a netkit-leased queue. Exercises the queue rx-buf-len path via netif_mp_open_rxq()'s lease redirect: the netkit ifindex is opaque to io_uring, but rx_page_size is honoured by the *physical* qops because the lease pointer rewrites the request from netkit onto the leased physical rxq before supported_params/validate_qcfg are consulted. """ cfg.require_ipver("6") src_queue, nk_queue = _setup_lease(cfg) defer(_teardown_netkit, cfg) ethnl = EthtoolFamily() with open("/proc/sys/vm/nr_hugepages", "r+", encoding="utf-8") as f: nr_hugepages = int(f.read().strip()) if nr_hugepages < 64: f.seek(0) f.write("64") defer(_restore_hugepages, nr_hugepages) rings = ethnl.rings_get({"header": {"dev-index": cfg.ifindex}}) rx_rings = rings["rx"] hds_thresh = rings.get("hds-thresh", 0) ethnl.rings_set( { "header": {"dev-index": cfg.ifindex}, "tcp-data-split": "enabled", "hds-thresh": 0, "rx": 64, } ) defer( ethnl.rings_set, { "header": {"dev-index": cfg.ifindex}, "tcp-data-split": "unknown", "hds-thresh": hds_thresh, "rx": rx_rings, }, ) ethtool(f"-X {cfg.ifname} equal {src_queue}") defer(ethtool, f"-X {cfg.ifname} default") flow_rule_id = set_flow_rule(cfg, src_queue) defer(ethtool, f"-N {cfg.ifname} delete {flow_rule_id}") # -x 2 asks iou-zcrx for rx_buf_len = 2 * page_size (8 KiB on x86_64), # backed by a 2 MiB hugepage area so the chunks are physically # contiguous, which is what zcrx requires for non-default rx_buf_len. rx_cmd = ( f"{cfg.bin_local} -s -p {cfg.port} " f"-i {cfg.nk_guest_ifname} -q {nk_queue} -x 2" ) tx_cmd = f"{cfg.bin_remote} -c -h {cfg.nk_guest_ipv6} -p {cfg.port} -l 12840" # Probe via -d (dry run): exits with SKIP_CODE if the leased physical # qops doesn't advertise QCFG_RX_PAGE_SIZE (e.g. older bnxt FW/HW). probe = cmd(rx_cmd + " -d", fail=False, ns=cfg.netns) if probe.ret == SKIP_CODE: msg = probe.stdout.strip() or "rx_buf_len not supported by leased NIC" raise KsftSkipEx(msg) # A successful dry run still registered the zcrx ifq on the leased # physical queue; wait for its async teardown before the real server # binds the same queue. _mp_clear_wait(cfg, src_queue) with bkg(rx_cmd, exit_wait=True, ns=cfg.netns): wait_port_listen(cfg.port, proto="tcp", ns=cfg.netns) cmd(tx_cmd, host=cfg.remote) def test_attrs(cfg) -> None: cfg.require_ipver("6") src_queue, nk_queue = _setup_lease(cfg) defer(_teardown_netkit, cfg) netdevnl = NetdevFamily() queue_info = netdevnl.queue_get( {"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"} ) ksft_eq(queue_info["id"], src_queue) ksft_eq(queue_info["type"], "rx") ksft_eq(queue_info["ifindex"], cfg.ifindex) ksft_in("lease", queue_info) lease = queue_info["lease"] ksft_eq(lease["ifindex"], cfg.nk_guest_ifindex) ksft_eq(lease["queue"]["id"], nk_queue) ksft_eq(lease["queue"]["type"], "rx") ksft_in("netns-id", lease) def test_attach_xdp_with_mp(cfg) -> None: cfg.require_ipver("6") src_queue, nk_queue = _setup_lease(cfg) defer(_teardown_netkit, cfg) ethnl = EthtoolFamily() rings = ethnl.rings_get({"header": {"dev-index": cfg.ifindex}}) rx_rings = rings["rx"] hds_thresh = rings.get("hds-thresh", 0) ethnl.rings_set( { "header": {"dev-index": cfg.ifindex}, "tcp-data-split": "enabled", "hds-thresh": 0, "rx": 64, } ) defer( ethnl.rings_set, { "header": {"dev-index": cfg.ifindex}, "tcp-data-split": "unknown", "hds-thresh": hds_thresh, "rx": rx_rings, }, ) ethtool(f"-X {cfg.ifname} equal {src_queue}") defer(ethtool, f"-X {cfg.ifname} default") netdevnl = NetdevFamily() rx_cmd = ( f"{cfg.bin_local} -s -p {cfg.port} " f"-i {cfg.nk_guest_ifname} -q {nk_queue}" ) with bkg(rx_cmd, ns=cfg.netns): wait_port_listen(cfg.port, proto="tcp", ns=cfg.netns) time.sleep(0.1) queue_info = netdevnl.queue_get( {"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"} ) ksft_in("io-uring", queue_info) prog = cfg.net_lib_dir / "xdp_dummy.bpf.o" with ksft_raises(CmdExitFailure): ip(f"link set dev {cfg.ifname} xdp obj {prog} sec xdp.frags") time.sleep(0.1) queue_info = netdevnl.queue_get( {"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"} ) ksft_not_in("io-uring", queue_info) def test_destroy(cfg) -> None: cfg.require_ipver("6") src_queue, nk_queue = _setup_lease(cfg) defer(_teardown_netkit, cfg) ethnl = EthtoolFamily() rings = ethnl.rings_get({"header": {"dev-index": cfg.ifindex}}) rx_rings = rings["rx"] hds_thresh = rings.get("hds-thresh", 0) ethnl.rings_set( { "header": {"dev-index": cfg.ifindex}, "tcp-data-split": "enabled", "hds-thresh": 0, "rx": 64, } ) defer( ethnl.rings_set, { "header": {"dev-index": cfg.ifindex}, "tcp-data-split": "unknown", "hds-thresh": hds_thresh, "rx": rx_rings, }, ) ethtool(f"-X {cfg.ifname} equal {src_queue}") defer(ethtool, f"-X {cfg.ifname} default") rx_cmd = ( f"{cfg.bin_local} -s -p {cfg.port} " f"-i {cfg.nk_guest_ifname} -q {nk_queue}" ) rx_proc = cmd(rx_cmd, background=True, ns=cfg.netns) wait_port_listen(cfg.port, proto="tcp", ns=cfg.netns) netdevnl = NetdevFamily() queue_info = netdevnl.queue_get( {"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"} ) ksft_in("io-uring", queue_info) # ip link del will wait for all refs to drop first, but iou-zcrx is holding # onto a ref. Terminate iou-zcrx async via a thread after a delay. kill_timer = threading.Timer(1, rx_proc.proc.terminate) kill_timer.start() ip(f"link del dev {cfg.nk_host_ifname}") kill_timer.join() cfg.nk_host_ifname = None cfg.nk_guest_ifname = None queue_info = netdevnl.queue_get( {"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"} ) ksft_not_in("io-uring", queue_info) flow_rule_id = set_flow_rule(cfg, src_queue) defer(ethtool, f"-N {cfg.ifname} delete {flow_rule_id}") rx_cmd = f"{cfg.bin_local} -s -p {cfg.port} -i {cfg.ifname} -q {src_queue}" tx_cmd = f"{cfg.bin_remote} -c -h {cfg.addr_v['6']} -p {cfg.port} -l 12840" with bkg(rx_cmd, exit_wait=True): wait_port_listen(cfg.port, proto="tcp") cmd(tx_cmd, host=cfg.remote) # Short delay since iou cleanup is async and takes a bit of time. time.sleep(0.1) queue_info = netdevnl.queue_get( {"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"} ) ksft_not_in("io-uring", queue_info) def main() -> None: with NetDrvContEnv(__file__, rxqueues=2) as cfg: cfg.bin_local = path.abspath( path.dirname(__file__) + "/../../../drivers/net/hw/iou-zcrx" ) cfg.bin_remote = cfg.remote.deploy(cfg.bin_local) cfg.port = rand_port() ksft_run( [ test_iou_zcrx, test_iou_zcrx_large_buf, test_attrs, test_attach_xdp_with_mp, test_destroy, ], args=(cfg,), ) ksft_exit() if __name__ == "__main__": main()