344 lines
11 KiB
Python
Executable File
344 lines
11 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-2.0
|
|
|
|
"""MACsec tests."""
|
|
|
|
import os
|
|
|
|
from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_raises
|
|
from lib.py import ksft_variants, KsftNamedVariant
|
|
from lib.py import CmdExitFailure, KsftSkipEx
|
|
from lib.py import NetDrvEpEnv
|
|
from lib.py import cmd, ip, defer, ethtool
|
|
|
|
MACSEC_KEY = "12345678901234567890123456789012"
|
|
MACSEC_VLAN_VID = 10
|
|
|
|
# Unique prefix per run to avoid collisions in the shared netns.
|
|
# Keep it short: IFNAMSIZ is 16 (incl. NUL), and VLAN names append ".<vid>".
|
|
MACSEC_PFX = f"ms{os.getpid()}_"
|
|
|
|
|
|
def _macsec_name(idx=0):
|
|
return f"{MACSEC_PFX}{idx}"
|
|
|
|
|
|
def _get_macsec_offload(dev):
|
|
"""Returns macsec offload mode string from ip -d link show."""
|
|
info = ip(f"-d link show dev {dev}", json=True)[0]
|
|
return info.get("linkinfo", {}).get("info_data", {}).get("offload")
|
|
|
|
|
|
def _get_features(dev):
|
|
"""Returns ethtool features dict for a device."""
|
|
return ethtool(f"-k {dev}", json=True)[0]
|
|
|
|
|
|
def _require_ip_macsec(cfg):
|
|
"""SKIP if iproute2 on local or remote lacks 'ip macsec' support."""
|
|
for host in [None, cfg.remote]:
|
|
out = cmd("ip macsec help", fail=False, host=host)
|
|
if "Usage" not in out.stdout + out.stderr:
|
|
where = "remote" if host else "local"
|
|
raise KsftSkipEx(f"iproute2 too old on {where},"
|
|
" missing macsec support")
|
|
|
|
|
|
def _require_ip_macsec_offload():
|
|
"""SKIP if local iproute2 doesn't understand 'ip macsec offload'."""
|
|
out = cmd("ip macsec help", fail=False)
|
|
if "offload" not in out.stdout + out.stderr:
|
|
raise KsftSkipEx("iproute2 too old, missing macsec offload")
|
|
|
|
|
|
def _require_macsec_offload(cfg):
|
|
"""SKIP if local device doesn't support macsec-hw-offload."""
|
|
_require_ip_macsec_offload()
|
|
try:
|
|
feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
|
|
except (CmdExitFailure, IndexError) as e:
|
|
raise KsftSkipEx(
|
|
f"can't query features: {e}") from e
|
|
if not feat.get("macsec-hw-offload", {}).get("active"):
|
|
raise KsftSkipEx("macsec-hw-offload not supported")
|
|
|
|
|
|
def _get_mac(ifname, host=None):
|
|
"""Gets MAC address of an interface."""
|
|
dev = ip(f"link show dev {ifname}", json=True, host=host)
|
|
return dev[0]["address"]
|
|
|
|
|
|
def _setup_macsec_sa(cfg, name):
|
|
"""Adds matching TX/RX SAs on both ends."""
|
|
local_mac = _get_mac(name)
|
|
remote_mac = _get_mac(name, host=cfg.remote)
|
|
|
|
ip(f"macsec add {name} tx sa 0 pn 1 on key 01 {MACSEC_KEY}")
|
|
ip(f"macsec add {name} rx port 1 address {remote_mac}")
|
|
ip(f"macsec add {name} rx port 1 address {remote_mac} "
|
|
f"sa 0 pn 1 on key 02 {MACSEC_KEY}")
|
|
|
|
ip(f"macsec add {name} tx sa 0 pn 1 on key 02 {MACSEC_KEY}",
|
|
host=cfg.remote)
|
|
ip(f"macsec add {name} rx port 1 address {local_mac}", host=cfg.remote)
|
|
ip(f"macsec add {name} rx port 1 address {local_mac} "
|
|
f"sa 0 pn 1 on key 01 {MACSEC_KEY}", host=cfg.remote)
|
|
|
|
|
|
def _setup_macsec_devs(cfg, name, offload):
|
|
"""Creates macsec devices on both ends.
|
|
|
|
Only the local device gets HW offload; the remote always uses software
|
|
MACsec since it may not support offload at all.
|
|
"""
|
|
offload_arg = "mac" if offload else "off"
|
|
|
|
ip(f"link add link {cfg.ifname} {name} "
|
|
f"type macsec encrypt on offload {offload_arg}")
|
|
defer(ip, f"link del {name}")
|
|
ip(f"link add link {cfg.remote_ifname} {name} "
|
|
f"type macsec encrypt on", host=cfg.remote)
|
|
defer(ip, f"link del {name}", host=cfg.remote)
|
|
|
|
|
|
def _set_offload(name, offload):
|
|
"""Sets offload on the local macsec device only."""
|
|
offload_arg = "mac" if offload else "off"
|
|
|
|
ip(f"link set {name} type macsec encrypt on offload {offload_arg}")
|
|
|
|
|
|
def _setup_vlans(cfg, name, vid):
|
|
"""Adds VLANs on top of existing macsec devs."""
|
|
vlan_name = f"{name}.{vid}"
|
|
|
|
ip(f"link add link {name} {vlan_name} type vlan id {vid}")
|
|
defer(ip, f"link del {vlan_name}")
|
|
ip(f"link add link {name} {vlan_name} type vlan id {vid}", host=cfg.remote)
|
|
defer(ip, f"link del {vlan_name}", host=cfg.remote)
|
|
|
|
|
|
def _setup_vlan_ips(cfg, name, vid):
|
|
"""Adds VLANs and IPs and brings up the macsec + VLAN devices."""
|
|
local_ip = "198.51.100.1"
|
|
remote_ip = "198.51.100.2"
|
|
vlan_name = f"{name}.{vid}"
|
|
|
|
ip(f"addr add {local_ip}/24 dev {vlan_name}")
|
|
ip(f"addr add {remote_ip}/24 dev {vlan_name}", host=cfg.remote)
|
|
ip(f"link set {name} up")
|
|
ip(f"link set {name} up", host=cfg.remote)
|
|
ip(f"link set {vlan_name} up")
|
|
ip(f"link set {vlan_name} up", host=cfg.remote)
|
|
|
|
return vlan_name, remote_ip
|
|
|
|
|
|
def test_offload_api(cfg) -> None:
|
|
"""MACsec offload API: create SecY, add SA/rx, toggle offload."""
|
|
|
|
_require_macsec_offload(cfg)
|
|
ms0 = _macsec_name(0)
|
|
ms1 = _macsec_name(1)
|
|
ms2 = _macsec_name(2)
|
|
|
|
# Create 3 SecY with offload
|
|
ip(f"link add link {cfg.ifname} {ms0} type macsec "
|
|
f"port 4 encrypt on offload mac")
|
|
defer(ip, f"link del {ms0}")
|
|
|
|
ip(f"link add link {cfg.ifname} {ms1} type macsec "
|
|
f"address aa:bb:cc:dd:ee:ff port 5 encrypt on offload mac")
|
|
defer(ip, f"link del {ms1}")
|
|
|
|
ip(f"link add link {cfg.ifname} {ms2} type macsec "
|
|
f"sci abbacdde01020304 encrypt on offload mac")
|
|
defer(ip, f"link del {ms2}")
|
|
|
|
# Add TX SA
|
|
ip(f"macsec add {ms0} tx sa 0 pn 1024 on "
|
|
"key 01 12345678901234567890123456789012")
|
|
|
|
# Add RX SC + SA
|
|
ip(f"macsec add {ms0} rx port 1234 address 1c:ed:de:ad:be:ef")
|
|
ip(f"macsec add {ms0} rx port 1234 address 1c:ed:de:ad:be:ef "
|
|
"sa 0 pn 1 on key 00 0123456789abcdef0123456789abcdef")
|
|
|
|
# Can't disable offload when SAs are configured
|
|
with ksft_raises(CmdExitFailure):
|
|
ip(f"link set {ms0} type macsec offload off")
|
|
with ksft_raises(CmdExitFailure):
|
|
ip(f"macsec offload {ms0} off")
|
|
|
|
# Toggle offload via rtnetlink on SA-free device
|
|
ip(f"link set {ms2} type macsec offload off")
|
|
ip(f"link set {ms2} type macsec encrypt on offload mac")
|
|
|
|
# Toggle offload via genetlink
|
|
ip(f"macsec offload {ms2} off")
|
|
ip(f"macsec offload {ms2} mac")
|
|
|
|
|
|
def test_max_secy(cfg) -> None:
|
|
"""nsim-only test for max number of SecYs."""
|
|
|
|
cfg.require_nsim()
|
|
_require_ip_macsec_offload()
|
|
ms0 = _macsec_name(0)
|
|
ms1 = _macsec_name(1)
|
|
ms2 = _macsec_name(2)
|
|
ms3 = _macsec_name(3)
|
|
|
|
ip(f"link add link {cfg.ifname} {ms0} type macsec "
|
|
f"port 4 encrypt on offload mac")
|
|
defer(ip, f"link del {ms0}")
|
|
|
|
ip(f"link add link {cfg.ifname} {ms1} type macsec "
|
|
f"address aa:bb:cc:dd:ee:ff port 5 encrypt on offload mac")
|
|
defer(ip, f"link del {ms1}")
|
|
|
|
ip(f"link add link {cfg.ifname} {ms2} type macsec "
|
|
f"sci abbacdde01020304 encrypt on offload mac")
|
|
defer(ip, f"link del {ms2}")
|
|
with ksft_raises(CmdExitFailure):
|
|
ip(f"link add link {cfg.ifname} {ms3} "
|
|
f"type macsec port 8 encrypt on offload mac")
|
|
|
|
|
|
def test_max_sc(cfg) -> None:
|
|
"""nsim-only test for max number of SCs."""
|
|
|
|
cfg.require_nsim()
|
|
_require_ip_macsec_offload()
|
|
ms0 = _macsec_name(0)
|
|
|
|
ip(f"link add link {cfg.ifname} {ms0} type macsec "
|
|
f"port 4 encrypt on offload mac")
|
|
defer(ip, f"link del {ms0}")
|
|
ip(f"macsec add {ms0} rx port 1234 address 1c:ed:de:ad:be:ef")
|
|
with ksft_raises(CmdExitFailure):
|
|
ip(f"macsec add {ms0} rx port 1235 address 1c:ed:de:ad:be:ef")
|
|
|
|
|
|
def test_offload_state(cfg) -> None:
|
|
"""Offload state reflects configuration changes."""
|
|
|
|
_require_macsec_offload(cfg)
|
|
ms0 = _macsec_name(0)
|
|
|
|
# Create with offload on
|
|
ip(f"link add link {cfg.ifname} {ms0} type macsec "
|
|
f"encrypt on offload mac")
|
|
cleanup = defer(ip, f"link del {ms0}")
|
|
|
|
ksft_eq(_get_macsec_offload(ms0), "mac",
|
|
"created with offload: should be mac")
|
|
feats_on_1 = _get_features(ms0)
|
|
|
|
ip(f"link set {ms0} type macsec offload off")
|
|
ksft_eq(_get_macsec_offload(ms0), "off",
|
|
"offload disabled: should be off")
|
|
feats_off_1 = _get_features(ms0)
|
|
|
|
ip(f"link set {ms0} type macsec encrypt on offload mac")
|
|
ksft_eq(_get_macsec_offload(ms0), "mac",
|
|
"offload re-enabled: should be mac")
|
|
ksft_eq(_get_features(ms0), feats_on_1,
|
|
"features should match first offload-on snapshot")
|
|
|
|
# Delete and recreate without offload
|
|
cleanup.exec()
|
|
ip(f"link add link {cfg.ifname} {ms0} type macsec")
|
|
defer(ip, f"link del {ms0}")
|
|
ksft_eq(_get_macsec_offload(ms0), "off",
|
|
"created without offload: should be off")
|
|
ksft_eq(_get_features(ms0), feats_off_1,
|
|
"features should match first offload-off snapshot")
|
|
|
|
ip(f"link set {ms0} type macsec encrypt on offload mac")
|
|
ksft_eq(_get_macsec_offload(ms0), "mac",
|
|
"offload enabled after create: should be mac")
|
|
ksft_eq(_get_features(ms0), feats_on_1,
|
|
"features should match first offload-on snapshot")
|
|
|
|
|
|
def _check_nsim_vid(cfg, vid, expected) -> None:
|
|
"""Checks if a VLAN is present. Only works on netdevsim."""
|
|
|
|
nsim = cfg.get_local_nsim_dev()
|
|
if not nsim:
|
|
return
|
|
|
|
vlan_path = os.path.join(nsim.nsims[0].dfs_dir, "vlan")
|
|
with open(vlan_path, encoding="utf-8") as f:
|
|
vids = f.read()
|
|
found = f"ctag {vid}\n" in vids
|
|
ksft_eq(found, expected,
|
|
f"VLAN {vid} {'expected' if expected else 'not expected'}"
|
|
f" in debugfs")
|
|
|
|
|
|
@ksft_variants([
|
|
KsftNamedVariant("offloaded", True),
|
|
KsftNamedVariant("software", False),
|
|
])
|
|
def test_vlan(cfg, offload) -> None:
|
|
"""Ping through VLAN-over-macsec."""
|
|
|
|
_require_ip_macsec(cfg)
|
|
if offload:
|
|
_require_macsec_offload(cfg)
|
|
else:
|
|
_require_ip_macsec_offload()
|
|
name = _macsec_name()
|
|
_setup_macsec_devs(cfg, name, offload=offload)
|
|
_setup_macsec_sa(cfg, name)
|
|
_setup_vlans(cfg, name, MACSEC_VLAN_VID)
|
|
vlan_name, remote_ip = _setup_vlan_ips(cfg, name, MACSEC_VLAN_VID)
|
|
_check_nsim_vid(cfg, MACSEC_VLAN_VID, offload)
|
|
# nsim doesn't handle the data path for offloaded macsec, so skip
|
|
# the ping when offloaded on nsim.
|
|
if not offload or not cfg.get_local_nsim_dev():
|
|
cmd(f"ping -I {vlan_name} -c 1 -W 5 {remote_ip}")
|
|
|
|
|
|
@ksft_variants([
|
|
KsftNamedVariant("on_to_off", True),
|
|
KsftNamedVariant("off_to_on", False),
|
|
])
|
|
def test_vlan_toggle(cfg, offload) -> None:
|
|
"""Toggle offload: VLAN filters propagate/remove correctly."""
|
|
|
|
_require_ip_macsec(cfg)
|
|
_require_macsec_offload(cfg)
|
|
name = _macsec_name()
|
|
_setup_macsec_devs(cfg, name, offload=offload)
|
|
_setup_vlans(cfg, name, MACSEC_VLAN_VID)
|
|
_check_nsim_vid(cfg, MACSEC_VLAN_VID, offload)
|
|
_set_offload(name, offload=not offload)
|
|
_check_nsim_vid(cfg, MACSEC_VLAN_VID, not offload)
|
|
vlan_name, remote_ip = _setup_vlan_ips(cfg, name, MACSEC_VLAN_VID)
|
|
_setup_macsec_sa(cfg, name)
|
|
# nsim doesn't handle the data path for offloaded macsec, so skip
|
|
# the ping when the final state is offloaded on nsim.
|
|
if offload or not cfg.get_local_nsim_dev():
|
|
cmd(f"ping -I {vlan_name} -c 1 -W 5 {remote_ip}")
|
|
|
|
|
|
def main() -> None:
|
|
"""Main program."""
|
|
with NetDrvEpEnv(__file__) as cfg:
|
|
ksft_run([test_offload_api,
|
|
test_max_secy,
|
|
test_max_sc,
|
|
test_offload_state,
|
|
test_vlan,
|
|
test_vlan_toggle,
|
|
], args=(cfg,))
|
|
ksft_exit()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|