#!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0 """Test suite for PSP capable drivers.""" import errno import fcntl import os import socket import struct import termios import time from contextlib import contextmanager 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 ksft_variants, KsftNamedVariant from lib.py import KsftSkipEx, KsftFailEx from lib.py import NetDrvEpEnv, NetDrvContEnv from lib.py import Netlink, NlError, PSPFamily, RtnlFamily from lib.py import NetNSEnter from lib.py import bkg, rand_port, wait_port_listen from lib.py import ip TCP_ULP = 31 def _get_outq(s): one = b'\0' * 4 outq = fcntl.ioctl(s.fileno(), termios.TIOCOUTQ, one) return struct.unpack("I", outq)[0] def _send_with_ack(cfg, msg): cfg.comm_sock.send(msg) response = cfg.comm_sock.recv(4) if response != b'ack\0': raise RuntimeError("Unexpected server response", response) def _remote_read_len(cfg): cfg.comm_sock.send(b'read len\0') return int(cfg.comm_sock.recv(1024)[:-1].decode('utf-8')) def _make_clr_conn(cfg, ipver=None): _send_with_ack(cfg, b'conn clr\0') remote_addr = cfg.remote_addr_v[ipver] if ipver else cfg.remote_addr s = socket.create_connection((remote_addr, cfg.comm_port), ) return s def _make_psp_conn(cfg, version=0, ipver=None): _send_with_ack(cfg, b'conn psp\0' + struct.pack('BB', version, version)) remote_addr = cfg.remote_addr_v[ipver] if ipver else cfg.remote_addr s = socket.create_connection((remote_addr, cfg.comm_port), ) return s @contextmanager def _make_lo_conn(): # After tx-assoc, the client's egress is dropped, since lo has no # psp_dev, so its FIN never reaches the server. Closing the server # resets the unaccepted child, and the client accepts the cleartext # RST because it hasn't received any PSP traffic yet. with socket.create_server(("localhost", 0)) as srv, \ socket.create_connection(srv.getsockname()[:2]) as s: yield s def _close_conn(cfg, s): _send_with_ack(cfg, b'data close\0') s.close() def _close_psp_conn(cfg, s): _close_conn(cfg, s) def _spi_xchg(s, rx): s.send(struct.pack('I', rx['spi']) + rx['key']) tx = s.recv(4 + len(rx['key'])) return { 'spi': struct.unpack('I', tx[:4])[0], 'key': tx[4:] } def _send_careful(cfg, s, rounds): data = b'0123456789' * 200 for i in range(rounds): n = 0 for _ in range(10): # allow 10 retries try: n += s.send(data[n:], socket.MSG_DONTWAIT) if n == len(data): break except BlockingIOError: time.sleep(0.05) else: rlen = _remote_read_len(cfg) outq = _get_outq(s) report = f'sent: {i * len(data) + n} remote len: {rlen} outq: {outq}' raise RuntimeError(report) return len(data) * rounds def _check_data_rx(cfg, exp_len): read_len = -1 for _ in range(30): cfg.comm_sock.send(b'read len\0') read_len = int(cfg.comm_sock.recv(1024)[:-1].decode('utf-8')) if read_len == exp_len: break time.sleep(0.01) ksft_eq(read_len, exp_len) def _check_data_outq(s, exp_len, force_wait=False): outq = 0 for _ in range(10): outq = _get_outq(s) if not force_wait and outq == exp_len: break time.sleep(0.01) ksft_eq(outq, exp_len) def _get_stat(cfg, key): return cfg.pspnl.get_stats({'dev-id': cfg.psp_dev_id})[key] # # Test case boiler plate # 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'] == target_ifindex: cfg.psp_info = dev cfg.psp_dev_id = cfg.psp_info['id'] break else: raise KsftSkipEx("No PSP devices found") # Enable PSP if necessary cap = cfg.psp_info['psp-versions-cap'] ena = cfg.psp_info['psp-versions-ena'] if cap != ena: cfg.pspnl.dev_set({'id': cfg.psp_dev_id, 'psp-versions-ena': cap}) defer(cfg.pspnl.dev_set, {'id': cfg.psp_dev_id, 'psp-versions-ena': ena }) # # Test cases # def dev_list_devices(cfg): """ Dump all devices """ _init_psp_dev(cfg) devices = cfg.pspnl.dev_get({}, dump=True) found = False for dev in devices: found |= dev['id'] == cfg.psp_dev_id ksft_true(found) def dev_get_device(cfg): """ Get the device we intend to use """ _init_psp_dev(cfg) dev = cfg.pspnl.dev_get({'id': cfg.psp_dev_id}) ksft_eq(dev['id'], cfg.psp_dev_id) def dev_get_device_bad(cfg): """ Test getting device which doesn't exist """ raised = False try: cfg.pspnl.dev_get({'id': 1234567}) except NlError as e: ksft_eq(e.nl_msg.error, -errno.ENODEV) raised = True ksft_true(raised) def dev_rotate(cfg): """ Test key rotation """ _init_psp_dev(cfg) prev_rotations = _get_stat(cfg, 'key-rotations') rot = cfg.pspnl.key_rotate({"id": cfg.psp_dev_id}) ksft_eq(rot['id'], cfg.psp_dev_id) rot = cfg.pspnl.key_rotate({"id": cfg.psp_dev_id}) ksft_eq(rot['id'], cfg.psp_dev_id) cur_rotations = _get_stat(cfg, 'key-rotations') ksft_eq(cur_rotations, prev_rotations + 2) def dev_rotate_spi(cfg): """ Test key rotation and SPI check """ _init_psp_dev(cfg) top_a = top_b = 0 with _make_lo_conn() as s: assoc_a = cfg.pspnl.rx_assoc({"version": 0, "dev-id": cfg.psp_dev_id, "sock-fd": s.fileno()}) top_a = assoc_a['rx-key']['spi'] >> 31 rot = cfg.pspnl.key_rotate({"id": cfg.psp_dev_id}) with _make_lo_conn() as s: ksft_eq(rot['id'], cfg.psp_dev_id) assoc_b = cfg.pspnl.rx_assoc({"version": 0, "dev-id": cfg.psp_dev_id, "sock-fd": s.fileno()}) top_b = assoc_b['rx-key']['spi'] >> 31 ksft_ne(top_a, top_b) def assoc_basic(cfg): """ Test creating associations """ _init_psp_dev(cfg) with _make_lo_conn() as s: assoc = cfg.pspnl.rx_assoc({"version": 0, "dev-id": cfg.psp_dev_id, "sock-fd": s.fileno()}) ksft_eq(assoc['dev-id'], cfg.psp_dev_id) ksft_gt(assoc['rx-key']['spi'], 0) ksft_eq(len(assoc['rx-key']['key']), 16) assoc = cfg.pspnl.tx_assoc({"dev-id": cfg.psp_dev_id, "version": 0, "tx-key": assoc['rx-key'], "sock-fd": s.fileno()}) ksft_eq(len(assoc), 0) def assoc_bad_dev(cfg): """ Test creating associations with bad device ID """ _init_psp_dev(cfg) with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as s: with ksft_raises(NlError) as cm: cfg.pspnl.rx_assoc({"version": 0, "dev-id": cfg.psp_dev_id + 1234567, "sock-fd": s.fileno()}) ksft_eq(cm.exception.nl_msg.error, -errno.ENODEV) def assoc_sk_only_conn(cfg): """ Test creating associations based on socket """ _init_psp_dev(cfg) with _make_clr_conn(cfg) as s: assoc = cfg.pspnl.rx_assoc({"version": 0, "sock-fd": s.fileno()}) ksft_eq(assoc['dev-id'], cfg.psp_dev_id) cfg.pspnl.tx_assoc({"version": 0, "tx-key": assoc['rx-key'], "sock-fd": s.fileno()}) _close_conn(cfg, s) def assoc_sk_only_mismatch(cfg): """ Test creating associations based on socket (dev mismatch) """ _init_psp_dev(cfg) with _make_clr_conn(cfg) as s: with ksft_raises(NlError) as cm: cfg.pspnl.rx_assoc({"version": 0, "dev-id": cfg.psp_dev_id + 1234567, "sock-fd": s.fileno()}) the_exception = cm.exception ksft_eq(the_exception.nl_msg.extack['bad-attr'], ".dev-id") ksft_eq(the_exception.nl_msg.error, -errno.EINVAL) _close_conn(cfg, s) def assoc_sk_only_mismatch_tx(cfg): """ Test creating associations based on socket (dev mismatch) """ _init_psp_dev(cfg) with _make_clr_conn(cfg) as s: with ksft_raises(NlError) as cm: assoc = cfg.pspnl.rx_assoc({"version": 0, "sock-fd": s.fileno()}) cfg.pspnl.tx_assoc({"version": 0, "tx-key": assoc['rx-key'], "dev-id": cfg.psp_dev_id + 1234567, "sock-fd": s.fileno()}) the_exception = cm.exception ksft_eq(the_exception.nl_msg.extack['bad-attr'], ".dev-id") ksft_eq(the_exception.nl_msg.error, -errno.EINVAL) _close_conn(cfg, s) def assoc_sk_only_unconn(cfg): """ Test creating associations based on socket (unconnected, should fail) """ _init_psp_dev(cfg) with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as s: with ksft_raises(NlError) as cm: cfg.pspnl.rx_assoc({"version": 0, "sock-fd": s.fileno()}) the_exception = cm.exception ksft_eq(the_exception.nl_msg.extack['miss-type'], "dev-id") ksft_eq(the_exception.nl_msg.error, -errno.EINVAL) def assoc_rx_unconnected(cfg): """ Test that an Rx assoc is rejected on an unconnected socket """ _init_psp_dev(cfg) with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as s: with ksft_raises(NlError) as cm: cfg.pspnl.rx_assoc({"version": 0, "dev-id": cfg.psp_dev_id, "sock-fd": s.fileno()}) ksft_eq(cm.exception.nl_msg.error, -errno.ENOTCONN) ksft_eq(cm.exception.nl_msg.extack['bad-attr'], ".sock-fd") def assoc_rx_listener(cfg): """ Test that an Rx assoc is rejected on a listening socket """ _init_psp_dev(cfg) with socket.create_server(("localhost", 0)) as s: with ksft_raises(NlError) as cm: cfg.pspnl.rx_assoc({"version": 0, "dev-id": cfg.psp_dev_id, "sock-fd": s.fileno()}) ksft_eq(cm.exception.nl_msg.error, -errno.ENOTCONN) ksft_eq(cm.exception.nl_msg.extack['bad-attr'], ".sock-fd") def assoc_version_mismatch(cfg): """ Test creating associations where Rx and Tx PSP versions do not match """ _init_psp_dev(cfg) versions = list(cfg.psp_info['psp-versions-cap']) if len(versions) < 2: raise KsftSkipEx("Not enough PSP versions supported by the device for the test") # Translate versions to integers versions = [cfg.pspnl.consts["version"].entries[v].value for v in versions] with _make_lo_conn() as s: rx = cfg.pspnl.rx_assoc({"version": versions[0], "dev-id": cfg.psp_dev_id, "sock-fd": s.fileno()}) for version in versions[1:]: with ksft_raises(NlError) as cm: cfg.pspnl.tx_assoc({"dev-id": cfg.psp_dev_id, "version": version, "tx-key": rx['rx-key'], "sock-fd": s.fileno()}) the_exception = cm.exception ksft_eq(the_exception.nl_msg.error, -errno.EINVAL) def _require_tls_ulp(): with socket.create_server(("localhost", 0)) as srv, \ socket.create_connection(srv.getsockname()) as s: try: s.setsockopt(socket.SOL_TCP, TCP_ULP, b"tls") except OSError as exc: raise KsftSkipEx("kTLS not available") from exc def assoc_psp_ulp_exclusive(cfg): """ Test that a TCP ULP cannot be attached to a PSP socket """ _init_psp_dev(cfg) _require_tls_ulp() with _make_clr_conn(cfg) as s: try: cfg.pspnl.rx_assoc({"version": 0, "dev-id": cfg.psp_dev_id, "sock-fd": s.fileno()}) with ksft_raises(OSError) as cm: s.setsockopt(socket.SOL_TCP, TCP_ULP, b"tls") ksft_eq(cm.exception.errno, errno.EINVAL) finally: _close_conn(cfg, s) def assoc_ulp_psp_exclusive(cfg): """ Test that a PSP assoc cannot be added to a socket with a TCP ULP """ _init_psp_dev(cfg) _require_tls_ulp() with _make_clr_conn(cfg) as s: try: s.setsockopt(socket.SOL_TCP, TCP_ULP, b"tls") with ksft_raises(NlError) as cm: cfg.pspnl.rx_assoc({"version": 0, "dev-id": cfg.psp_dev_id, "sock-fd": s.fileno()}) ksft_eq(cm.exception.nl_msg.error, -errno.EINVAL) ksft_eq(cm.exception.nl_msg.extack['bad-attr'], ".sock-fd") finally: _close_conn(cfg, s) def assoc_twice(cfg): """ Test reusing Tx assoc for two sockets """ _init_psp_dev(cfg) def rx_assoc_check(s): assoc = cfg.pspnl.rx_assoc({"version": 0, "dev-id": cfg.psp_dev_id, "sock-fd": s.fileno()}) ksft_eq(assoc['dev-id'], cfg.psp_dev_id) ksft_gt(assoc['rx-key']['spi'], 0) ksft_eq(len(assoc['rx-key']['key']), 16) return assoc with _make_lo_conn() as s: assoc = rx_assoc_check(s) tx = cfg.pspnl.tx_assoc({"dev-id": cfg.psp_dev_id, "version": 0, "tx-key": assoc['rx-key'], "sock-fd": s.fileno()}) ksft_eq(len(tx), 0) # Use the same Tx assoc second time with _make_lo_conn() as s2: rx_assoc_check(s2) tx = cfg.pspnl.tx_assoc({"dev-id": cfg.psp_dev_id, "version": 0, "tx-key": assoc['rx-key'], "sock-fd": s2.fileno()}) ksft_eq(len(tx), 0) def _data_basic_send(cfg, version, ipver): """ Test basic data send """ _init_psp_dev(cfg) # Version 0 is required by spec, don't let it skip if version: name = cfg.pspnl.consts["version"].entries_by_val[version].name if name not in cfg.psp_info['psp-versions-cap']: with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as s: with ksft_raises(NlError) as cm: cfg.pspnl.rx_assoc({"version": version, "dev-id": cfg.psp_dev_id, "sock-fd": s.fileno()}) ksft_eq(cm.exception.nl_msg.error, -errno.EOPNOTSUPP) raise KsftSkipEx("PSP version not supported", name) 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) 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) 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, 20) _check_data_outq(s, data_len, force_wait=True) _check_data_rx(cfg, 0) _close_psp_conn(cfg, s) def data_send_bad_key(cfg): """ Test send data with bad key """ _init_psp_dev(cfg) s = _make_psp_conn(cfg) rx_assoc = cfg.pspnl.rx_assoc({"version": 0, "dev-id": cfg.psp_dev_id, "sock-fd": s.fileno()}) rx = rx_assoc['rx-key'] tx = _spi_xchg(s, rx) tx['key'] = (tx['key'][0] ^ 0xff).to_bytes(1, 'little') + tx['key'][1:] __bad_xfer_do(cfg, s, tx) def data_send_disconnect(cfg): """ Test socket close after sending data """ _init_psp_dev(cfg) with _make_psp_conn(cfg) as s: assoc = cfg.pspnl.rx_assoc({"version": 0, "sock-fd": s.fileno()}) tx = _spi_xchg(s, assoc['rx-key']) cfg.pspnl.tx_assoc({"version": 0, "tx-key": tx, "sock-fd": s.fileno()}) data_len = _send_careful(cfg, s, 100) _check_data_rx(cfg, data_len) s.shutdown(socket.SHUT_RDWR) s.close() def _data_mss_adjust(cfg, ipver): _init_psp_dev(cfg) # First figure out what the MSS would be without any adjustments s = _make_clr_conn(cfg, ipver) s.send(b"0123456789abcdef" * 1024) _check_data_rx(cfg, 16 * 1024) mss = s.getsockopt(socket.IPPROTO_TCP, socket.TCP_MAXSEG) _close_conn(cfg, s) s = _make_psp_conn(cfg, 0, ipver) try: rx_assoc = cfg.pspnl.rx_assoc({"version": 0, "dev-id": cfg.psp_dev_id, "sock-fd": s.fileno()}) rx = rx_assoc['rx-key'] tx = _spi_xchg(s, rx) rxmss = s.getsockopt(socket.IPPROTO_TCP, socket.TCP_MAXSEG) ksft_eq(mss, rxmss) cfg.pspnl.tx_assoc({"dev-id": cfg.psp_dev_id, "version": 0, "tx-key": tx, "sock-fd": s.fileno()}) txmss = s.getsockopt(socket.IPPROTO_TCP, socket.TCP_MAXSEG) ksft_eq(mss, txmss + 40) data_len = _send_careful(cfg, s, 100) _check_data_rx(cfg, data_len) _check_data_outq(s, 0) txmss = s.getsockopt(socket.IPPROTO_TCP, socket.TCP_MAXSEG) ksft_eq(mss, txmss + 40) finally: _close_psp_conn(cfg, s) def data_stale_key(cfg): """ Test send on a double-rotated key """ _init_psp_dev(cfg) prev_stale = _get_stat(cfg, 'stale-events') s = _make_psp_conn(cfg) try: rx_assoc = cfg.pspnl.rx_assoc({"version": 0, "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": 0, "tx-key": tx, "sock-fd": s.fileno()}) data_len = _send_careful(cfg, s, 100) _check_data_rx(cfg, data_len) _check_data_outq(s, 0) cfg.pspnl.key_rotate({"id": cfg.psp_dev_id}) cfg.pspnl.key_rotate({"id": cfg.psp_dev_id}) cur_stale = _get_stat(cfg, 'stale-events') ksft_gt(cur_stale, prev_stale) s.send(b'0123456789' * 200) _check_data_outq(s, 2000, force_wait=True) finally: _close_psp_conn(cfg, s) def __nsim_psp_rereg(cfg): # The PSP dev ID will change, remember what was there before before = {x['id'] for x in cfg.pspnl.dev_get({}, dump=True)} cfg._ns.nsims[0].dfs_write('psp_rereg', '1') after = {x['id'] for x in cfg.pspnl.dev_get({}, dump=True)} new_devs = list(after - before) ksft_eq(len(new_devs), 1) cfg.psp_dev_id = list(after - before)[0] def removal_device_rx(cfg): """ Test removing a netdev / PSD with active Rx assoc """ # We could technically devlink reload real devices, too # but that kills the control socket. So test this on # netdevsim only for now cfg.require_nsim() s = _make_clr_conn(cfg) try: rx_assoc = cfg.pspnl.rx_assoc({"version": 0, "dev-id": cfg.psp_dev_id, "sock-fd": s.fileno()}) ksft_not_none(rx_assoc) __nsim_psp_rereg(cfg) finally: _close_conn(cfg, s) def removal_device_bi(cfg): """ Test removing a netdev / PSD with active Rx/Tx assoc """ # We could technically devlink reload real devices, too # but that kills the control socket. So test this on # netdevsim only for now cfg.require_nsim() s = _make_clr_conn(cfg) try: rx_assoc = cfg.pspnl.rx_assoc({"version": 0, "dev-id": cfg.psp_dev_id, "sock-fd": s.fileno()}) cfg.pspnl.tx_assoc({"dev-id": cfg.psp_dev_id, "version": 0, "tx-key": rx_assoc['rx-key'], "sock-fd": s.fileno()}) __nsim_psp_rereg(cfg) finally: _close_conn(cfg, s) def _get_psp_ver_ip_variants(): for ver in range(4): for ipv in ("4", "6"): yield KsftNamedVariant(f"v{ver}_ip{ipv}", ver, ipv) def _get_ip_variants(): for ipv in ("4", "6"): yield KsftNamedVariant(f"ip{ipv}", ipv) @ksft_variants(_get_psp_ver_ip_variants()) def data_basic_send(cfg, version, ipver): """Test basic PSP data send.""" cfg.require_ipver(ipver) _data_basic_send(cfg, version, ipver) @ksft_variants(_get_ip_variants()) def data_mss_adjust(cfg, ipver): """Test MSS adjustment with PSP.""" cfg.require_ipver(ipver) _data_mss_adjust(cfg, ipver) def _check_assoc_list(cfg, psp_dev_id, ifindex, nsid=None): """Verify assoc-list contains device with given ifindex, no duplicates.""" dev_info = cfg.pspnl.dev_get({'id': psp_dev_id}) 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'] != ifindex: continue if nsid is not None and assoc['nsid'] != nsid: continue ksft_eq(found, False, "Duplicate assoc entry found") found = True ksft_eq(found, True, "Associated device not found in dev_get() response") def _data_basic_send_netkit_psp_assoc(cfg, version, ipver): """ Test basic data send with netkit interface associated with PSP dev. """ _assoc_nk_guest(cfg) # Enter guest namespace (netns) to run PSP test with NetNSEnter(cfg.netns.name): cfg.pspnl = PSPFamily() sock = _make_psp_conn(cfg, version, ipver) rx_assoc = cfg.pspnl.rx_assoc({"version": version, "dev-id": cfg.psp_dev_id, "sock-fd": sock.fileno()}) rx_key = rx_assoc['rx-key'] tx_key = _spi_xchg(sock, rx_key) cfg.pspnl.tx_assoc({"dev-id": cfg.psp_dev_id, "version": version, "tx-key": tx_key, "sock-fd": sock.fileno()}) data_len = _send_careful(cfg, sock, 100) _check_data_rx(cfg, data_len) _close_psp_conn(cfg, sock) def _assoc_check_list(cfg): """Test that assoc-list is correctly populated after dev-assoc.""" _assoc_nk_guest(cfg) _check_assoc_list(cfg, cfg.psp_dev_id, cfg.nk_guest_ifindex, cfg.psp_dev_peer_nsid) def _get_psp_ver_ip6_variants(): for ver in range(4): yield KsftNamedVariant(f"v{ver}_ip6", ver, "6") @ksft_variants(_get_psp_ver_ip6_variants()) def data_basic_send_netkit_psp_assoc(cfg, version, ipver): """Test PSP data send via netkit with dev-assoc.""" cfg.require_ipver(ipver) _data_basic_send_netkit_psp_assoc(cfg, version, ipver) def _key_rotation_notify_multi_ns_netkit(cfg): """ Test key rotation notifications across multiple namespaces using netkit """ _assoc_nk_guest(cfg) # 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": cfg.psp_dev_id}) # Poll both sockets from main thread for pspnl, label in [(main_pspnl, "main"), (peer_pspnl, "guest")]: for ntf in pspnl.poll_ntf(duration=10): if ntf['msg'].get('id') == cfg.psp_dev_id: break else: raise KsftFailEx( f"No key rotation notification received" f" in {label} namespace") def _dev_change_notify_multi_ns_netkit(cfg): """ Test dev_change notifications across multiple namespaces using netkit """ _assoc_nk_guest(cfg) # 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': cfg.psp_dev_id, '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 ntf in pspnl.poll_ntf(duration=10): if ntf['msg'].get('id') == cfg.psp_dev_id: break else: raise KsftFailEx( f"No dev_change notification received" f" in {label} namespace") def _subscribe_mgmt(cfg): """Listen on the mgmt group in the guest and in the main namespace.""" # Listener in the guest namespace; socket stays bound to that ns with NetNSEnter(cfg.netns.name): peer_pspnl = PSPFamily() peer_pspnl.ntf_subscribe('mgmt') main_pspnl = PSPFamily() main_pspnl.ntf_subscribe('mgmt') return main_pspnl, peer_pspnl def _get_dev_ntf(cfg, pspnl, label): """Wait for the next notification about the PSP device under test.""" for ntf in pspnl.poll_ntf(duration=10): if ntf['msg'].get('id') == cfg.psp_dev_id: return ntf raise KsftFailEx(f"No notification received in the {label} namespace") def _check_disassoc_ntf(cfg, main_pspnl, peer_pspnl, ifindex): """Check the notifications for a netns losing its last association.""" ntf = _get_dev_ntf(cfg, main_pspnl, "main") ksft_eq(ntf['name'], 'dev-change-ntf') for assoc in ntf['msg'].get('assoc-list', []): if assoc['nsid'] != cfg.psp_dev_peer_nsid: continue ksft_ne(assoc['ifindex'], ifindex, "Disassociated device still listed in the notification") # The device is gone as far as the disassociated namespace is concerned ntf = _get_dev_ntf(cfg, peer_pspnl, "guest") ksft_eq(ntf['name'], 'dev-del-ntf') ksft_true('ifindex' not in ntf['msg'], "ifindex reported to an associated namespace") def _dev_disassoc_notify_multi_ns_netkit(cfg): """ Test the notifications dev-disassoc generates in both namespaces """ _init_psp_dev(cfg, True) defer(delattr, cfg, 'psp_dev_id') defer(delattr, cfg, 'psp_info') cfg.pspnl.dev_assoc({'id': cfg.psp_dev_id, 'ifindex': cfg.nk_guest_ifindex, 'nsid': cfg.psp_dev_peer_nsid}) defer(_try_disassoc, cfg, cfg.psp_dev_id, cfg.nk_guest_ifindex, cfg.psp_dev_peer_nsid) main_pspnl, peer_pspnl = _subscribe_mgmt(cfg) cfg.pspnl.dev_disassoc({'id': cfg.psp_dev_id, 'ifindex': cfg.nk_guest_ifindex, 'nsid': cfg.psp_dev_peer_nsid}) _check_disassoc_ntf(cfg, main_pspnl, peer_pspnl, cfg.nk_guest_ifindex) def _dev_disassoc_notify_one_of_two_netkit(cfg): """Test the notifications with two netkits associated in one netns. Disassociating the first netkit leaves the PSP device visible in the guest namespace, generates a dev-change-ntf. Disassociating the second one takes the device out of its view, generates 'dev-del-ntf'. """ _init_psp_dev(cfg, True) defer(delattr, cfg, 'psp_dev_id') defer(delattr, cfg, 'psp_info') tmp_ifindex, _ = _add_netkit_guest(cfg, "tmp_nk_host", "tmp_nk_guest") for ifindex in [cfg.nk_guest_ifindex, tmp_ifindex]: cfg.pspnl.dev_assoc({'id': cfg.psp_dev_id, 'ifindex': ifindex, 'nsid': cfg.psp_dev_peer_nsid}) defer(_try_disassoc, cfg, cfg.psp_dev_id, ifindex, cfg.psp_dev_peer_nsid) main_pspnl, peer_pspnl = _subscribe_mgmt(cfg) # One of the two goes away, the device stays visible in the guest netns cfg.pspnl.dev_disassoc({'id': cfg.psp_dev_id, 'ifindex': tmp_ifindex, 'nsid': cfg.psp_dev_peer_nsid}) ntf = _get_dev_ntf(cfg, main_pspnl, "main") ksft_eq(ntf['name'], 'dev-change-ntf') ntf = _get_dev_ntf(cfg, peer_pspnl, "guest") ksft_eq(ntf['name'], 'dev-change-ntf') found = False for assoc in ntf['msg'].get('assoc-list', []): ksft_ne(assoc['ifindex'], tmp_ifindex, "Disassociated device still listed in the notification") found |= assoc['ifindex'] == cfg.nk_guest_ifindex ksft_true(found, "Remaining association missing from the notification") # And now the last one, the device disappears from the guest netns cfg.pspnl.dev_disassoc({'id': cfg.psp_dev_id, 'ifindex': cfg.nk_guest_ifindex, 'nsid': cfg.psp_dev_peer_nsid}) _check_disassoc_ntf(cfg, main_pspnl, peer_pspnl, cfg.nk_guest_ifindex) def _psp_dev_get_check_netkit_psp_assoc(cfg): """ Check psp dev-get output with netkit interface associated with PSP dev """ _assoc_nk_guest(cfg) # Check 1: In default netns, verify dev-get has correct ifindex and assoc-list dev_info = cfg.pspnl.dev_get({'id': cfg.psp_dev_id}) ksft_eq(dev_info['ifindex'], cfg.psp_ifindex) _check_assoc_list(cfg, cfg.psp_dev_id, cfg.nk_guest_ifindex, cfg.psp_dev_peer_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") # ifindex of the main netdevice means nothing in this namespace ksft_true('ifindex' not in peer_dev, "ifindex reported to an associated namespace") # 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'] == cfg.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") def _dev_assoc_no_nsid(cfg): """ Test dev-assoc and dev-disassoc without nsid attribute """ _init_psp_dev(cfg, True) # Associate without nsid - should look up ifindex in caller's netns cfg.pspnl.dev_assoc({'id': cfg.psp_dev_id, 'ifindex': cfg.nk_host_ifindex}) defer(_try_disassoc, cfg, cfg.psp_dev_id, cfg.nk_host_ifindex) defer(delattr, cfg, 'psp_dev_id') defer(delattr, cfg, 'psp_info') # Verify assoc-list contains the device (match by ifindex only) _check_assoc_list(cfg, cfg.psp_dev_id, cfg.nk_host_ifindex) # Disassociate without nsid - should also use caller's netns cfg.pspnl.dev_disassoc({'id': cfg.psp_dev_id, 'ifindex': cfg.nk_host_ifindex}) # Verify assoc-list no longer contains the device dev_info = cfg.pspnl.dev_get({'id': cfg.psp_dev_id}) found = False if 'assoc-list' in dev_info: for assoc in dev_info['assoc-list']: if assoc['ifindex'] == cfg.nk_host_ifindex: found = True break ksft_true(not found, "Device should not be in assoc-list after disassociation") def _add_netkit_guest(cfg, host_name, guest_name): """Create a netkit pair and move its peer into the test namespace. Returns the peer's ifindex there and the defer() deleting the pair. """ existing = {link['ifindex'] for link in ip("-d link show", json=True) if link.get('linkinfo', {}).get('info_kind') == 'netkit'} rtnl = RtnlFamily() rtnl.newlink( { "ifname": host_name, "linkinfo": { "kind": "netkit", "data": { "mode": "l2", "policy": "forward", "peer-policy": "forward", }, }, }, flags=[Netlink.NLM_F_CREATE, Netlink.NLM_F_EXCL], ) cleanup = defer(ip, f"link del {host_name}") # Find the peer by diffing against existing netkit ifindexes all_links = ip("-d link show", json=True) peer = [link for link in all_links if link.get('linkinfo', {}).get('info_kind') == 'netkit' and link['ifindex'] not in existing and link['ifname'] != host_name] ksft_eq(len(peer), 1, "Failed to find the new netkit peer") # Rename and move guest end into the test namespace ip(f"link set dev {peer[0]['ifname']} name {guest_name}") ip(f"link set dev {guest_name} netns {cfg.netns.name}") guest_dev = ip(f"link show dev {guest_name}", json=True, ns=cfg.netns)[0] ip(f"link set dev {guest_name} up", ns=cfg.netns) return guest_dev['ifindex'], cleanup def _psp_dev_assoc_cleanup_on_netkit_del(cfg): """Test that assoc-list is cleared when associated netkit is deleted. Creates a disposable netkit pair for this test to avoid destroying the shared environment. """ _init_psp_dev(cfg, True) defer(delattr, cfg, 'psp_dev_id') defer(delattr, cfg, 'psp_info') tmp_host_name = "tmp_nk_host" tmp_guest_ifindex, cleanup_netkit = _add_netkit_guest(cfg, tmp_host_name, "tmp_nk_guest") # Associate PSP device with the temporary guest interface cfg.pspnl.dev_assoc({'id': cfg.psp_dev_id, 'ifindex': tmp_guest_ifindex, 'nsid': cfg.psp_dev_peer_nsid}) # Verify assoc-list contains the temporary device _check_assoc_list(cfg, cfg.psp_dev_id, tmp_guest_ifindex, cfg.psp_dev_peer_nsid) # Removing the netdevice is notified like a disassociation main_pspnl, peer_pspnl = _subscribe_mgmt(cfg) # Delete the temporary netkit pair (deleting one end removes both) ip(f"link del {tmp_host_name}") cleanup_netkit.cancel() # Verify assoc-list is cleared after netkit deletion dev_info = cfg.pspnl.dev_get({'id': cfg.psp_dev_id}) ksft_true('assoc-list' not in dev_info or len(dev_info['assoc-list']) == 0, "assoc-list should be empty after netkit deletion") _check_disassoc_ntf(cfg, main_pspnl, peer_pspnl, tmp_guest_ifindex) def _try_disassoc(cfg, psp_dev_id, ifindex, nsid=None): """Best-effort disassociate, ignoring errors if already removed.""" try: params = {'id': psp_dev_id, 'ifindex': ifindex} if nsid is not None: params['nsid'] = nsid cfg.pspnl.dev_disassoc(params) except NlError: pass def _assoc_nk_guest(cfg): """Associate nk_guest with PSP device and register cleanup via defer().""" _init_psp_dev(cfg, True) cfg.pspnl.dev_assoc({'id': cfg.psp_dev_id, 'ifindex': cfg.nk_guest_ifindex, 'nsid': cfg.psp_dev_peer_nsid}) defer(_disassoc_nk_guest, cfg, cfg.psp_dev_id, cfg.nk_guest_ifindex) def _disassoc_nk_guest(cfg, psp_dev_id, nk_guest_ifindex): """Disassociate nk_guest and reset cfg PSP state.""" pspnl = PSPFamily() pspnl.dev_disassoc({'id': psp_dev_id, 'ifindex': nk_guest_ifindex, 'nsid': cfg.psp_dev_peer_nsid}) cfg.pspnl = pspnl del cfg.psp_dev_id del cfg.psp_info 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): # pylint: disable=protected-access """ 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 main() -> None: """ Ksft boiler plate main """ # Make sure LOCAL_PREFIX_V6 is set if "LOCAL_PREFIX_V6" not in os.environ: os.environ["LOCAL_PREFIX_V6"] = "2001:db8:2::" try: env = NetDrvContEnv(__file__, primary_rx_redirect=True) has_cont = True except KsftSkipEx: env = NetDrvEpEnv(__file__) has_cont = False with env as cfg: cfg.pspnl = PSPFamily() if has_cont: _setup_psp_attributes(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() srv = None try: with bkg(responder + f" -p {cfg.comm_port} -i {cfg.remote_ifindex}", host=cfg.remote, exit_wait=True) as srv: wait_port_listen(cfg.comm_port, host=cfg.remote) cfg.comm_sock = socket.create_connection((cfg.remote_addr, cfg.comm_port), timeout=1) cases = [data_basic_send, data_mss_adjust] if has_cont: cases += [ _assoc_check_list, data_basic_send_netkit_psp_assoc, _key_rotation_notify_multi_ns_netkit, _dev_change_notify_multi_ns_netkit, _dev_disassoc_notify_multi_ns_netkit, _dev_disassoc_notify_one_of_two_netkit, _psp_dev_get_check_netkit_psp_assoc, _dev_assoc_no_nsid, _psp_dev_assoc_cleanup_on_netkit_del, ] ksft_run(cases=cases, globs=globals(), case_pfx={"dev_", "data_", "assoc_", "removal_"}, args=(cfg, )) cfg.comm_sock.send(b"exit\0") cfg.comm_sock.close() finally: if srv and (srv.stdout or srv.stderr): ksft_pr("") ksft_pr(f"Responder logs ({srv.ret}):") if srv and srv.stdout: ksft_pr("STDOUT:\n# " + srv.stdout.strip().replace("\n", "\n# ")) if srv and srv.stderr: ksft_pr("STDERR:\n# " + srv.stderr.strip().replace("\n", "\n# ")) ksft_exit() if __name__ == "__main__": main()