Files
linux/tools/testing/selftests/drivers/net/macsec.py
Cosmin Ratiu 26555673bc selftests: Add MACsec VLAN propagation traffic test
Add VLAN filter propagation tests through offloaded MACsec devices via
actual traffic.

The tests create MACsec tunnels with matching SAs on both endpoints,
stack VLANs on top, and verify connectivity with ping. Covered:
- Offloaded MACsec with VLAN (filters propagate to HW)
- Software MACsec with VLAN (no HW filter propagation)
- Offload on/off toggle and verifying traffic still works

On netdevsim this makes use of the VLAN filter debugfs file to actually
validate that filters are applied/removed correctly.
On real hardware the traffic should validate actual VLAN filter
propagation.

Signed-off-by: Cosmin Ratiu <cratiu@nvidia.com>
Reviewed-by: Sabrina Dubroca <sd@queasysnail.net>
Link: https://patch.msgid.link/20260408115240.1636047-4-cratiu@nvidia.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
2026-04-09 19:38:42 -07:00

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()