From: Wei Wang Add a new param to NetDrvContEnv to add an additional bpf redirect program on nk_host to redirect traffic to the psp_dev_local. The topology looks like this: Host NS: psp_dev_local <---> nk_host | | | | (netkit pair) | | Remote NS: psp_dev_peer Guest NS: nk_guest (responder) (PSP tests) Add following tests for dev-assoc/dev-disassoc functionality: 1. Test the output of `./tools/net/ynl/pyynl/cli.py --spec Documentation/netlink/specs/psp.yaml --dump dev-get` in both default and the guest netns. 2. Test the case where we associate netkit with psp_dev_local, and send PSP traffic from nk_guest to psp_dev_peer in 2 different netns. 3. Test to make sure the key rotation notification is sent to the netns for associated dev as well 4. Test to make sure the dev change notification is sent to the netns for associated dev as well 5. Test the deletion of nk_guest in client netns, and proper cleanup in the assoc-list for psp dev. Signed-off-by: Wei Wang --- .../selftests/drivers/net/lib/py/env.py | 54 ++- tools/testing/selftests/drivers/net/psp.py | 387 +++++++++++++++++- 2 files changed, 433 insertions(+), 8 deletions(-) diff --git a/tools/testing/selftests/drivers/net/lib/py/env.py b/tools/testing/selftests/drivers/net/lib/py/env.py index ccff345fe1c1..2b88e4738fae 100644 --- a/tools/testing/selftests/drivers/net/lib/py/env.py +++ b/tools/testing/selftests/drivers/net/lib/py/env.py @@ -2,6 +2,7 @@ import ipaddress import os +import re import time import json from pathlib import Path @@ -327,7 +328,7 @@ class NetDrvContEnv(NetDrvEpEnv): +---------------+ """ - def __init__(self, src_path, rxqueues=1, **kwargs): + def __init__(self, src_path, rxqueues=1, install_tx_redirect_bpf=False, **kwargs): self.netns = None self._nk_host_ifname = None self._nk_guest_ifname = None @@ -338,6 +339,8 @@ class NetDrvContEnv(NetDrvEpEnv): self._init_ns_attached = False self._old_fwd = None self._old_accept_ra = None + self._nk_host_tc_attached = False + self._nk_host_bpf_prog_pref = None super().__init__(src_path, **kwargs) @@ -388,7 +391,13 @@ class NetDrvContEnv(NetDrvEpEnv): self._setup_ns() self._attach_bpf() + if install_tx_redirect_bpf: + self._attach_tx_redirect_bpf() + def __del__(self): + if self._nk_host_tc_attached: + cmd(f"tc filter del dev {self._nk_host_ifname} ingress pref {self._nk_host_bpf_prog_pref}", fail=False) + self._nk_host_tc_attached = False if self._tc_attached: cmd(f"tc filter del dev {self.ifname} ingress pref {self._bpf_prog_pref}") self._tc_attached = False @@ -496,3 +505,46 @@ class NetDrvContEnv(NetDrvEpEnv): value = ipv6_bytes + ifindex_bytes value_hex = ' '.join(f'{b:02x}' for b in value) bpftool(f"map update id {bss_map_id} key hex 00 00 00 00 value hex {value_hex}") + + def _attach_tx_redirect_bpf(self): + """ + Attach BPF program on nk_host ingress to redirect TX traffic. + + Packets from nk_guest destined for the nsim network arrive at nk_host + via the netkit pair. This BPF program redirects them to the physical + interface so they can reach the remote peer. + """ + bpf_obj = self.test_dir / "nk_redirect.bpf.o" + if not bpf_obj.exists(): + raise KsftSkipEx("BPF prog nk_redirect.bpf.o not found") + + cmd(f"tc qdisc add dev {self._nk_host_ifname} clsact") + + cmd(f"tc filter add dev {self._nk_host_ifname} ingress bpf obj {bpf_obj} sec tc/ingress direct-action") + self._nk_host_tc_attached = True + + tc_info = cmd(f"tc filter show dev {self._nk_host_ifname} ingress").stdout + match = re.search(r'pref (\d+).*nk_redirect\.bpf.*id (\d+)', tc_info) + if not match: + raise Exception("Failed to get TX redirect BPF prog ID") + self._nk_host_bpf_prog_pref = int(match.group(1)) + nk_host_bpf_prog_id = int(match.group(2)) + + prog_info = bpftool(f"prog show id {nk_host_bpf_prog_id}", json=True) + map_ids = prog_info.get("map_ids", []) + + bss_map_id = None + for map_id in map_ids: + map_info = bpftool(f"map show id {map_id}", json=True) + if map_info.get("name").endswith("bss"): + bss_map_id = map_id + + if bss_map_id is None: + raise Exception("Failed to find TX redirect BPF .bss map") + + ipv6_addr = ipaddress.IPv6Address(self.nsim_v6_pfx) + ipv6_bytes = ipv6_addr.packed + ifindex_bytes = self.ifindex.to_bytes(4, byteorder='little') + value = ipv6_bytes + ifindex_bytes + value_hex = ' '.join(f'{b:02x}' for b in value) + bpftool(f"map update id {bss_map_id} key hex 00 00 00 00 value hex {value_hex}") diff --git a/tools/testing/selftests/drivers/net/psp.py b/tools/testing/selftests/drivers/net/psp.py index 864d9fce1094..d292437193cc 100755 --- a/tools/testing/selftests/drivers/net/psp.py +++ b/tools/testing/selftests/drivers/net/psp.py @@ -5,6 +5,7 @@ import errno import fcntl +import os import socket import struct import termios @@ -14,9 +15,11 @@ from lib.py import defer from lib.py import ksft_run, ksft_exit, ksft_pr from lib.py import ksft_true, ksft_eq, ksft_ne, ksft_gt, ksft_raises from lib.py import ksft_not_none -from lib.py import KsftSkipEx -from lib.py import NetDrvEpEnv, PSPFamily, NlError -from lib.py import bkg, rand_port, wait_port_listen +from lib.py import KsftSkipEx, KsftFailEx +from lib.py import NetDrvContEnv, PSPFamily, NlError +from lib.py import NetNSEnter +from lib.py import bkg, cmd, rand_port, wait_port_listen +from lib.py import ip def _get_outq(s): @@ -117,11 +120,13 @@ def _get_stat(cfg, key): # Test case boiler plate # -def _init_psp_dev(cfg): +def _init_psp_dev(cfg, use_psp_ifindex=False): if not hasattr(cfg, 'psp_dev_id'): # Figure out which local device we are testing against + # For NetDrvContEnv: use psp_ifindex instead of ifindex + target_ifindex = cfg.psp_ifindex if use_psp_ifindex else cfg.ifindex for dev in cfg.pspnl.dev_get({}, dump=True): - if dev['ifindex'] == cfg.ifindex: + if dev['ifindex'] == target_ifindex: cfg.psp_info = dev cfg.psp_dev_id = cfg.psp_info['id'] break @@ -394,6 +399,262 @@ def _data_basic_send(cfg, version, ipver): _close_psp_conn(cfg, s) +def _data_basic_send_netkit_psp_assoc(cfg, version, ipver): + """ + Test basic data send with netkit interface associated with PSP dev. + """ + + _init_psp_dev(cfg, True) + psp_dev_id_for_assoc = cfg.psp_dev_id + + # Associate PSP device with nk_guest interface (in guest namespace) + nk_guest_dev = ip(f"link show dev {cfg._nk_guest_ifname}", json=True, ns=cfg.netns)[0] + nk_guest_ifindex = nk_guest_dev['ifindex'] + + cfg.pspnl.dev_assoc({'id': psp_dev_id_for_assoc, 'ifindex': nk_guest_ifindex, 'nsid': cfg.psp_dev_peer_nsid}) + + # Test connectivity in both directions before PSP operations + remote_addr = cfg.remote_addr_v["6"] # remote peer address + nk_guest_addr = cfg.nk_guest_ipv6 # nk_guest address + + # Check if assoc-list contains nk_guest + dev_info = cfg.pspnl.dev_get({'id': psp_dev_id_for_assoc}) + + if 'assoc-list' in dev_info: + found = False + for assoc in dev_info['assoc-list']: + if assoc['ifindex'] == nk_guest_ifindex and assoc['nsid'] == cfg.psp_dev_peer_nsid: + found = True + break + ksft_true(found, "Associated device not found in dev_get() response") + else: + raise RuntimeError("No assoc-list in dev_get() response after association") + + # Enter guest namespace (netns) to run PSP test + with NetNSEnter(cfg.netns.name): + cfg.pspnl = PSPFamily() + + s = _make_psp_conn(cfg, version, ipver) + + rx_assoc = cfg.pspnl.rx_assoc({"version": version, + "dev-id": cfg.psp_dev_id, + "sock-fd": s.fileno()}) + rx = rx_assoc['rx-key'] + tx = _spi_xchg(s, rx) + + cfg.pspnl.tx_assoc({"dev-id": cfg.psp_dev_id, + "version": version, + "tx-key": tx, + "sock-fd": s.fileno()}) + + data_len = _send_careful(cfg, s, 100) + _check_data_rx(cfg, data_len) + _close_psp_conn(cfg, s) + + # Clean up - back in host namespace + cfg.pspnl = PSPFamily() + cfg.pspnl.dev_disassoc({'id': psp_dev_id_for_assoc, 'ifindex': nk_guest_ifindex, 'nsid': cfg.psp_dev_peer_nsid}) + + del cfg.psp_dev_id + del cfg.psp_info + + +def _key_rotation_notify_multi_ns_netkit(cfg, version, ipver): + """ Test key rotation notifications across multiple namespaces using netkit """ + _init_psp_dev(cfg, True) + psp_dev_id_for_assoc = cfg.psp_dev_id + + # Associate PSP device with nk_guest interface (in guest namespace) + nk_guest_dev = ip(f"link show dev {cfg._nk_guest_ifname}", json=True, ns=cfg.netns)[0] + nk_guest_ifindex = nk_guest_dev['ifindex'] + + cfg.pspnl.dev_assoc({'id': psp_dev_id_for_assoc, 'ifindex': nk_guest_ifindex, 'nsid': cfg.psp_dev_peer_nsid}) + + # Create listener in guest namespace; socket stays bound to that ns + with NetNSEnter(cfg.netns.name): + peer_pspnl = PSPFamily() + peer_pspnl.ntf_subscribe('use') + + # Create listener in main namespace + main_pspnl = PSPFamily() + main_pspnl.ntf_subscribe('use') + + # Trigger key rotation on the PSP device + cfg.pspnl.key_rotate({"id": psp_dev_id_for_assoc}) + + # Poll both sockets from main thread + for pspnl, label in [(main_pspnl, "main"), (peer_pspnl, "guest")]: + for i in range(100): + pspnl.check_ntf() + + try: + msg = pspnl.async_msg_queue.get_nowait() + break + except Exception: + pass + + time.sleep(0.1) + else: + raise KsftFailEx(f"No key rotation notification received in {label} namespace") + + ksft_true(msg['msg'].get('id') == psp_dev_id_for_assoc, + f"Key rotation notification for correct device not found in {label} namespace") + + # Clean up + cfg.pspnl.dev_disassoc({'id': psp_dev_id_for_assoc, 'ifindex': nk_guest_ifindex, 'nsid': cfg.psp_dev_peer_nsid}) + del cfg.psp_dev_id + del cfg.psp_info + + +def _dev_change_notify_multi_ns_netkit(cfg, version, ipver): + """ Test dev_change notifications across multiple namespaces using netkit """ + _init_psp_dev(cfg, True) + psp_dev_id_for_assoc = cfg.psp_dev_id + + # Associate PSP device with nk_guest interface (in guest namespace) + nk_guest_dev = ip(f"link show dev {cfg._nk_guest_ifname}", json=True, ns=cfg.netns)[0] + nk_guest_ifindex = nk_guest_dev['ifindex'] + + cfg.pspnl.dev_assoc({'id': psp_dev_id_for_assoc, 'ifindex': nk_guest_ifindex, 'nsid': cfg.psp_dev_peer_nsid}) + + # Create listener in guest namespace; socket stays bound to that ns + with NetNSEnter(cfg.netns.name): + peer_pspnl = PSPFamily() + peer_pspnl.ntf_subscribe('mgmt') + + # Create listener in main namespace + main_pspnl = PSPFamily() + main_pspnl.ntf_subscribe('mgmt') + + # Trigger dev_change by calling dev_set (notification is always sent) + cfg.pspnl.dev_set({'id': psp_dev_id_for_assoc, 'psp-versions-ena': cfg.psp_info['psp-versions-cap']}) + + # Poll both sockets from main thread + for pspnl, label in [(main_pspnl, "main"), (peer_pspnl, "guest")]: + for i in range(100): + pspnl.check_ntf() + + try: + msg = pspnl.async_msg_queue.get_nowait() + break + except Exception: + pass + + time.sleep(0.1) + else: + raise KsftFailEx(f"No dev_change notification received in {label} namespace") + + ksft_true(msg['msg'].get('id') == psp_dev_id_for_assoc, + f"Dev_change notification for correct device not found in {label} namespace") + + # Clean up + cfg.pspnl.dev_disassoc({'id': psp_dev_id_for_assoc, 'ifindex': nk_guest_ifindex, 'nsid': cfg.psp_dev_peer_nsid}) + del cfg.psp_dev_id + del cfg.psp_info + + +def _psp_dev_get_check_netkit_psp_assoc(cfg, version, ipver): + """ Check psp dev-get output with netkit interface associated with PSP dev """ + + _init_psp_dev(cfg, True) + psp_dev_id_for_assoc = cfg.psp_dev_id + + # Associate PSP device with nk_guest interface (in guest namespace) + nk_guest_dev = ip(f"link show dev {cfg._nk_guest_ifname}", json=True, ns=cfg.netns)[0] + nk_guest_ifindex = nk_guest_dev['ifindex'] + + cfg.pspnl.dev_assoc({'id': psp_dev_id_for_assoc, 'ifindex': nk_guest_ifindex, 'nsid': cfg.psp_dev_peer_nsid}) + + # Check 1: In default netns, verify dev-get has correct ifindex and assoc-list + dev_info = cfg.pspnl.dev_get({'id': psp_dev_id_for_assoc}) + + # Verify the PSP device has the correct ifindex + ksft_eq(dev_info['ifindex'], cfg.psp_ifindex) + + # Verify assoc-list exists and contains the associated nk_guest with correct ifindex and nsid + ksft_true('assoc-list' in dev_info, "No assoc-list in dev_get() response after association") + found = False + for assoc in dev_info['assoc-list']: + if assoc['ifindex'] == nk_guest_ifindex and assoc['nsid'] == cfg.psp_dev_peer_nsid: + found = True + break + ksft_true(found, "Associated device not found in assoc-list with correct ifindex and nsid") + + # Check 2: In guest netns, verify dev-get has assoc-list with nk_guest device + with NetNSEnter(cfg.netns.name): + peer_pspnl = PSPFamily() + + # Dump all devices in the guest namespace + peer_devices = peer_pspnl.dev_get({}, dump=True) + + # Find the device with by-association flag + peer_dev = None + for dev in peer_devices: + if dev.get('by-association'): + peer_dev = dev + break + + ksft_not_none(peer_dev, "No PSP device found with by-association flag in guest netns") + + # Verify assoc-list contains the nk_guest device + ksft_true('assoc-list' in peer_dev and len(peer_dev['assoc-list']) > 0, + "Guest device should have assoc-list with local devices") + + # Verify the assoc-list contains nk_guest ifindex with nsid=-1 (same namespace) + found = False + for assoc in peer_dev['assoc-list']: + if assoc['ifindex'] == nk_guest_ifindex: + ksft_eq(assoc['nsid'], -1, + "nsid should be -1 (NETNSA_NSID_NOT_ASSIGNED) for same-namespace device") + found = True + break + ksft_true(found, "nk_guest ifindex not found in assoc-list") + + # Clean up + cfg.pspnl.dev_disassoc({'id': psp_dev_id_for_assoc, 'ifindex': nk_guest_ifindex, 'nsid': cfg.psp_dev_peer_nsid}) + + del cfg.psp_dev_id + del cfg.psp_info + + +def _psp_dev_assoc_cleanup_on_netkit_del(cfg): + """ Test that assoc-list is cleared when associated netkit interface is deleted """ + _init_psp_dev(cfg, True) + psp_dev_id_for_assoc = cfg.psp_dev_id + + # Associate PSP device with nk_guest interface (in guest namespace) + nk_guest_dev = ip(f"link show dev {cfg._nk_guest_ifname}", json=True, ns=cfg.netns)[0] + nk_guest_ifindex = nk_guest_dev['ifindex'] + + cfg.pspnl.dev_assoc({'id': psp_dev_id_for_assoc, 'ifindex': nk_guest_ifindex, 'nsid': cfg.psp_dev_peer_nsid}) + + # Verify assoc-list exists in default netns + dev_info = cfg.pspnl.dev_get({'id': psp_dev_id_for_assoc}) + ksft_true('assoc-list' in dev_info, "No assoc-list after association") + found = False + for assoc in dev_info['assoc-list']: + if assoc['ifindex'] == nk_guest_ifindex and assoc['nsid'] == cfg.psp_dev_peer_nsid: + found = True + break + ksft_true(found, "Associated device not found in assoc-list") + + # Delete the netkit interface in the guest namespace + ip(f"link del {cfg._nk_guest_ifname}", ns=cfg.netns) + + # Mark netkit as already deleted so cleanup won't try to delete it again + # (deleting nk_guest also removes nk_host since they're a pair) + cfg._nk_host_ifname = None + cfg._nk_guest_ifname = None + + # Verify assoc-list is gone in default netns after netkit deletion + dev_info = cfg.pspnl.dev_get({'id': psp_dev_id_for_assoc}) + ksft_true('assoc-list' not in dev_info or len(dev_info['assoc-list']) == 0, + "assoc-list should be empty after netkit deletion") + + del cfg.psp_dev_id + del cfg.psp_info + + def __bad_xfer_do(cfg, s, tx, version='hdr0-aes-gcm-128'): # Make sure we accept the ACK for the SPI before we seal with the bad assoc _check_data_outq(s, 0) @@ -591,13 +852,92 @@ def ipver_test_builder(name, test_func, ipver): return test_case +def _get_nsid(ns_name): + """Get the nsid for a namespace.""" + for entry in ip("netns list-id", json=True): + if entry.get("name") == str(ns_name): + return entry["nsid"] + raise KsftSkipEx(f"nsid not found for namespace {ns_name}") + + +def _setup_psp_attributes(cfg): + """ + Set up PSP-specific attributes on the environment. + + This sets attributes needed for PSP tests based on whether we're using + netdevsim or a real NIC. + """ + if cfg._ns is not None: + # netdevsim case: PSP device is the local dev (in host namespace) + cfg.psp_dev = cfg._ns.nsims[0].dev + cfg.psp_ifname = cfg.psp_dev['ifname'] + cfg.psp_ifindex = cfg.psp_dev['ifindex'] + + # PSP peer device is the remote dev (in _netns, where psp_responder runs) + cfg.psp_dev_peer = cfg._ns_peer.nsims[0].dev + cfg.psp_dev_peer_ifname = cfg.psp_dev_peer['ifname'] + cfg.psp_dev_peer_ifindex = cfg.psp_dev_peer['ifindex'] + else: + # Real NIC case: PSP device is the local interface + cfg.psp_dev = cfg.dev + cfg.psp_ifname = cfg.ifname + cfg.psp_ifindex = cfg.ifindex + + # PSP peer device is the remote interface + cfg.psp_dev_peer = cfg.remote_dev + cfg.psp_dev_peer_ifname = cfg.remote_ifname + cfg.psp_dev_peer_ifindex = cfg.remote_ifindex + + # Get nsid for the guest namespace (netns) where nk_guest is + cfg.psp_dev_peer_nsid = _get_nsid(cfg.netns.name) + + +def _setup_psp_routes(cfg): + """ + Set up routes for cross-namespace connectivity. + + Traffic flows: + 1. remote (_netns) -> nk_guest (netns): + psp_dev_peer -> psp_dev_local -> BPF redirect -> nk_host -> nk_guest + Needs: route in _netns to nk_v6_pfx/64 via psp_dev_local + + 2. nk_guest (netns) -> remote (_netns): + nk_guest -> nk_host -> psp_dev_local -> psp_dev_peer + Needs: route in netns to dev_v6_pfx/64 via nk_host + """ + # In _netns (remote namespace): add route to nk_guest prefix via psp_dev_local + # psp_dev_peer can reach psp_dev_local via the link, then traffic goes through BPF + ip(f"-6 route add {cfg.nk_v6_pfx}/64 via {cfg.nsim_v6_pfx}1 dev {cfg.psp_dev_peer_ifname}", + ns=cfg._netns) + + # In netns (guest namespace): add route to remote peer prefix + # nk_guest default route goes to nk_host, but we need explicit route to dev_v6_pfx/64 + ip(f"-6 route add {cfg.nsim_v6_pfx}/64 via fe80::1 dev {cfg._nk_guest_ifname}", + ns=cfg.netns) + + def main() -> None: """ Ksft boiler plate main """ - with NetDrvEpEnv(__file__) as cfg: + # Use a different prefix for netkit guest to avoid conflict with dev prefix + nk_v6_pfx = "2001:db9::" + + # Set LOCAL_PREFIX_V6 to a DIFFERENT prefix than the dev prefix to avoid BPF + # redirecting psp_responder traffic. The BPF only redirects traffic + # matching LOCAL_PREFIX_V6, so dev traffic (2001:db8::) won't be affected. + if "LOCAL_PREFIX_V6" not in os.environ: + os.environ["LOCAL_PREFIX_V6"] = nk_v6_pfx + + with NetDrvContEnv(__file__, install_tx_redirect_bpf=True) as cfg: cfg.pspnl = PSPFamily() + cfg.nk_v6_pfx = nk_v6_pfx + + # Set up PSP-specific attributes and routes + _setup_psp_attributes(cfg) + _setup_psp_routes(cfg) # Set up responder and communication sock + # psp_responder runs in _netns (remote namespace with psp_dev_peer) responder = cfg.remote.deploy("psp_responder") cfg.comm_port = rand_port() @@ -622,9 +962,42 @@ def main() -> None: ipver_test_builder("data_mss_adjust", _data_mss_adjust, ipver) for ipver in ("4", "6") ] + cases += [ + psp_ip_ver_test_builder( + "data_basic_send_netkit_psp_assoc", + _data_basic_send_netkit_psp_assoc, version, "6" + ) + for version in range(0, 4) + ] + cases += [ + psp_ip_ver_test_builder( + "key_rotation_notify_multi_ns_netkit", + _key_rotation_notify_multi_ns_netkit, version, "6" + ) + for version in range(0, 4) + ] + cases += [ + psp_ip_ver_test_builder( + "dev_change_notify_multi_ns_netkit", + _dev_change_notify_multi_ns_netkit, version, "6" + ) + for version in range(0, 4) + ] + cases += [ + psp_ip_ver_test_builder( + "psp_dev_get_check_netkit_psp_assoc", + _psp_dev_get_check_netkit_psp_assoc, version, "6" + ) + for version in range(0, 4) + ] + # Run netkit deletion test only once at the end since it destroys the netkit + def psp_dev_assoc_cleanup_on_netkit_del_test(cfg): + _psp_dev_assoc_cleanup_on_netkit_del(cfg) + psp_dev_assoc_cleanup_on_netkit_del_test.__name__ = "psp_dev_assoc_cleanup_on_netkit_del" + cases.append(psp_dev_assoc_cleanup_on_netkit_del_test) ksft_run(cases=cases, globs=globals(), - case_pfx={"dev_", "data_", "assoc_", "removal_"}, + case_pfx={"dev_", "data_", "assoc_", "removal_", "key_", "psp_dev_"}, args=(cfg, )) cfg.comm_sock.send(b"exit\0") -- 2.52.0