blob: 06559ef49b9a5ed581cc7a9ecfdec15d06d4dea7 [file] [log] [blame]
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""Test suite for PSP capable drivers."""
import errno
import fcntl
import socket
import struct
import termios
import time
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
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
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):
if not hasattr(cfg, 'psp_dev_id'):
# Figure out which local device we are testing against
for dev in cfg.pspnl.dev_get({}, dump=True):
if dev['ifindex'] == cfg.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 socket.socket(socket.AF_INET6, socket.SOCK_STREAM) 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
s.close()
rot = cfg.pspnl.key_rotate({"id": cfg.psp_dev_id})
with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) 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
s.close()
ksft_ne(top_a, top_b)
def assoc_basic(cfg):
""" Test creating associations """
_init_psp_dev(cfg)
with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) 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)
s.close()
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)
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)
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_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 socket.socket(socket.AF_INET6, socket.SOCK_STREAM) 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 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 socket.socket(socket.AF_INET6, socket.SOCK_STREAM) 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 socket.socket(socket.AF_INET6, socket.SOCK_STREAM) 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)
s.close()
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 = set([x['id'] for x in cfg.pspnl.dev_get({}, dump=True)])
cfg._ns.nsims[0].dfs_write('psp_rereg', '1')
after = set([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 psp_ip_ver_test_builder(name, test_func, psp_ver, ipver):
"""Build test cases for each combo of PSP version and IP version"""
def test_case(cfg):
cfg.require_ipver(ipver)
test_case.__name__ = f"{name}_v{psp_ver}_ip{ipver}"
test_func(cfg, psp_ver, ipver)
return test_case
def ipver_test_builder(name, test_func, ipver):
"""Build test cases for each IP version"""
def test_case(cfg):
cfg.require_ipver(ipver)
test_case.__name__ = f"{name}_ip{ipver}"
test_func(cfg, ipver)
return test_case
def main() -> None:
""" Ksft boiler plate main """
with NetDrvEpEnv(__file__) as cfg:
cfg.pspnl = PSPFamily()
# Set up responder and communication sock
responder = cfg.remote.deploy("psp_responder")
cfg.comm_port = rand_port()
srv = None
try:
with bkg(responder + f" -p {cfg.comm_port}", 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 = [
psp_ip_ver_test_builder(
"data_basic_send", _data_basic_send, version, ipver
)
for version in range(0, 4)
for ipver in ("4", "6")
]
cases += [
ipver_test_builder("data_mss_adjust", _data_mss_adjust, ipver)
for ipver in ("4", "6")
]
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()