1#!/usr/bin/env python3 2# SPDX-License-Identifier: GPL-2.0 3 4"""MACsec tests.""" 5 6import os 7 8from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_raises 9from lib.py import ksft_variants, KsftNamedVariant 10from lib.py import CmdExitFailure, KsftSkipEx 11from lib.py import NetDrvEpEnv 12from lib.py import cmd, ip, defer, ethtool 13 14MACSEC_KEY = "12345678901234567890123456789012" 15MACSEC_VLAN_VID = 10 16 17# Unique prefix per run to avoid collisions in the shared netns. 18# Keep it short: IFNAMSIZ is 16 (incl. NUL), and VLAN names append ".<vid>". 19MACSEC_PFX = f"ms{os.getpid()}_" 20 21 22def _macsec_name(idx=0): 23 return f"{MACSEC_PFX}{idx}" 24 25 26def _get_macsec_offload(dev): 27 """Returns macsec offload mode string from ip -d link show.""" 28 info = ip(f"-d link show dev {dev}", json=True)[0] 29 return info.get("linkinfo", {}).get("info_data", {}).get("offload") 30 31 32def _get_features(dev): 33 """Returns ethtool features dict for a device.""" 34 return ethtool(f"-k {dev}", json=True)[0] 35 36 37def _require_ip_macsec(cfg): 38 """SKIP if iproute2 on local or remote lacks 'ip macsec' support.""" 39 for host in [None, cfg.remote]: 40 out = cmd("ip macsec help", fail=False, host=host) 41 if "Usage" not in out.stdout + out.stderr: 42 where = "remote" if host else "local" 43 raise KsftSkipEx(f"iproute2 too old on {where}," 44 " missing macsec support") 45 46 47def _require_ip_macsec_offload(): 48 """SKIP if local iproute2 doesn't understand 'ip macsec offload'.""" 49 out = cmd("ip macsec help", fail=False) 50 if "offload" not in out.stdout + out.stderr: 51 raise KsftSkipEx("iproute2 too old, missing macsec offload") 52 53 54def _require_macsec_offload(cfg): 55 """SKIP if local device doesn't support macsec-hw-offload.""" 56 _require_ip_macsec_offload() 57 try: 58 feat = ethtool(f"-k {cfg.ifname}", json=True)[0] 59 except (CmdExitFailure, IndexError) as e: 60 raise KsftSkipEx( 61 f"can't query features: {e}") from e 62 if not feat.get("macsec-hw-offload", {}).get("active"): 63 raise KsftSkipEx("macsec-hw-offload not supported") 64 65 66def _get_mac(ifname, host=None): 67 """Gets MAC address of an interface.""" 68 dev = ip(f"link show dev {ifname}", json=True, host=host) 69 return dev[0]["address"] 70 71 72def _setup_macsec_sa(cfg, name): 73 """Adds matching TX/RX SAs on both ends.""" 74 local_mac = _get_mac(name) 75 remote_mac = _get_mac(name, host=cfg.remote) 76 77 ip(f"macsec add {name} tx sa 0 pn 1 on key 01 {MACSEC_KEY}") 78 ip(f"macsec add {name} rx port 1 address {remote_mac}") 79 ip(f"macsec add {name} rx port 1 address {remote_mac} " 80 f"sa 0 pn 1 on key 02 {MACSEC_KEY}") 81 82 ip(f"macsec add {name} tx sa 0 pn 1 on key 02 {MACSEC_KEY}", 83 host=cfg.remote) 84 ip(f"macsec add {name} rx port 1 address {local_mac}", host=cfg.remote) 85 ip(f"macsec add {name} rx port 1 address {local_mac} " 86 f"sa 0 pn 1 on key 01 {MACSEC_KEY}", host=cfg.remote) 87 88 89def _setup_macsec_devs(cfg, name, offload): 90 """Creates macsec devices on both ends. 91 92 Only the local device gets HW offload; the remote always uses software 93 MACsec since it may not support offload at all. 94 """ 95 offload_arg = "mac" if offload else "off" 96 97 ip(f"link add link {cfg.ifname} {name} " 98 f"type macsec encrypt on offload {offload_arg}") 99 defer(ip, f"link del {name}") 100 ip(f"link add link {cfg.remote_ifname} {name} " 101 f"type macsec encrypt on", host=cfg.remote) 102 defer(ip, f"link del {name}", host=cfg.remote) 103 104 105def _set_offload(name, offload): 106 """Sets offload on the local macsec device only.""" 107 offload_arg = "mac" if offload else "off" 108 109 ip(f"link set {name} type macsec encrypt on offload {offload_arg}") 110 111 112def _setup_vlans(cfg, name, vid): 113 """Adds VLANs on top of existing macsec devs.""" 114 vlan_name = f"{name}.{vid}" 115 116 ip(f"link add link {name} {vlan_name} type vlan id {vid}") 117 defer(ip, f"link del {vlan_name}") 118 ip(f"link add link {name} {vlan_name} type vlan id {vid}", host=cfg.remote) 119 defer(ip, f"link del {vlan_name}", host=cfg.remote) 120 121 122def _setup_vlan_ips(cfg, name, vid): 123 """Adds VLANs and IPs and brings up the macsec + VLAN devices.""" 124 local_ip = "198.51.100.1" 125 remote_ip = "198.51.100.2" 126 vlan_name = f"{name}.{vid}" 127 128 ip(f"addr add {local_ip}/24 dev {vlan_name}") 129 ip(f"addr add {remote_ip}/24 dev {vlan_name}", host=cfg.remote) 130 ip(f"link set {name} up") 131 ip(f"link set {name} up", host=cfg.remote) 132 ip(f"link set {vlan_name} up") 133 ip(f"link set {vlan_name} up", host=cfg.remote) 134 135 return vlan_name, remote_ip 136 137 138def test_offload_api(cfg) -> None: 139 """MACsec offload API: create SecY, add SA/rx, toggle offload.""" 140 141 _require_macsec_offload(cfg) 142 ms0 = _macsec_name(0) 143 ms1 = _macsec_name(1) 144 ms2 = _macsec_name(2) 145 146 # Create 3 SecY with offload 147 ip(f"link add link {cfg.ifname} {ms0} type macsec " 148 f"port 4 encrypt on offload mac") 149 defer(ip, f"link del {ms0}") 150 151 ip(f"link add link {cfg.ifname} {ms1} type macsec " 152 f"address aa:bb:cc:dd:ee:ff port 5 encrypt on offload mac") 153 defer(ip, f"link del {ms1}") 154 155 ip(f"link add link {cfg.ifname} {ms2} type macsec " 156 f"sci abbacdde01020304 encrypt on offload mac") 157 defer(ip, f"link del {ms2}") 158 159 # Add TX SA 160 ip(f"macsec add {ms0} tx sa 0 pn 1024 on " 161 "key 01 12345678901234567890123456789012") 162 163 # Add RX SC + SA 164 ip(f"macsec add {ms0} rx port 1234 address 1c:ed:de:ad:be:ef") 165 ip(f"macsec add {ms0} rx port 1234 address 1c:ed:de:ad:be:ef " 166 "sa 0 pn 1 on key 00 0123456789abcdef0123456789abcdef") 167 168 # Can't disable offload when SAs are configured 169 with ksft_raises(CmdExitFailure): 170 ip(f"link set {ms0} type macsec offload off") 171 with ksft_raises(CmdExitFailure): 172 ip(f"macsec offload {ms0} off") 173 174 # Toggle offload via rtnetlink on SA-free device 175 ip(f"link set {ms2} type macsec offload off") 176 ip(f"link set {ms2} type macsec encrypt on offload mac") 177 178 # Toggle offload via genetlink 179 ip(f"macsec offload {ms2} off") 180 ip(f"macsec offload {ms2} mac") 181 182 183def test_max_secy(cfg) -> None: 184 """nsim-only test for max number of SecYs.""" 185 186 cfg.require_nsim() 187 _require_ip_macsec_offload() 188 ms0 = _macsec_name(0) 189 ms1 = _macsec_name(1) 190 ms2 = _macsec_name(2) 191 ms3 = _macsec_name(3) 192 193 ip(f"link add link {cfg.ifname} {ms0} type macsec " 194 f"port 4 encrypt on offload mac") 195 defer(ip, f"link del {ms0}") 196 197 ip(f"link add link {cfg.ifname} {ms1} type macsec " 198 f"address aa:bb:cc:dd:ee:ff port 5 encrypt on offload mac") 199 defer(ip, f"link del {ms1}") 200 201 ip(f"link add link {cfg.ifname} {ms2} type macsec " 202 f"sci abbacdde01020304 encrypt on offload mac") 203 defer(ip, f"link del {ms2}") 204 with ksft_raises(CmdExitFailure): 205 ip(f"link add link {cfg.ifname} {ms3} " 206 f"type macsec port 8 encrypt on offload mac") 207 208 209def test_max_sc(cfg) -> None: 210 """nsim-only test for max number of SCs.""" 211 212 cfg.require_nsim() 213 _require_ip_macsec_offload() 214 ms0 = _macsec_name(0) 215 216 ip(f"link add link {cfg.ifname} {ms0} type macsec " 217 f"port 4 encrypt on offload mac") 218 defer(ip, f"link del {ms0}") 219 ip(f"macsec add {ms0} rx port 1234 address 1c:ed:de:ad:be:ef") 220 with ksft_raises(CmdExitFailure): 221 ip(f"macsec add {ms0} rx port 1235 address 1c:ed:de:ad:be:ef") 222 223 224def test_offload_state(cfg) -> None: 225 """Offload state reflects configuration changes.""" 226 227 _require_macsec_offload(cfg) 228 ms0 = _macsec_name(0) 229 230 # Create with offload on 231 ip(f"link add link {cfg.ifname} {ms0} type macsec " 232 f"encrypt on offload mac") 233 cleanup = defer(ip, f"link del {ms0}") 234 235 ksft_eq(_get_macsec_offload(ms0), "mac", 236 "created with offload: should be mac") 237 feats_on_1 = _get_features(ms0) 238 239 ip(f"link set {ms0} type macsec offload off") 240 ksft_eq(_get_macsec_offload(ms0), "off", 241 "offload disabled: should be off") 242 feats_off_1 = _get_features(ms0) 243 244 ip(f"link set {ms0} type macsec encrypt on offload mac") 245 ksft_eq(_get_macsec_offload(ms0), "mac", 246 "offload re-enabled: should be mac") 247 ksft_eq(_get_features(ms0), feats_on_1, 248 "features should match first offload-on snapshot") 249 250 # Delete and recreate without offload 251 cleanup.exec() 252 ip(f"link add link {cfg.ifname} {ms0} type macsec") 253 defer(ip, f"link del {ms0}") 254 ksft_eq(_get_macsec_offload(ms0), "off", 255 "created without offload: should be off") 256 ksft_eq(_get_features(ms0), feats_off_1, 257 "features should match first offload-off snapshot") 258 259 ip(f"link set {ms0} type macsec encrypt on offload mac") 260 ksft_eq(_get_macsec_offload(ms0), "mac", 261 "offload enabled after create: should be mac") 262 ksft_eq(_get_features(ms0), feats_on_1, 263 "features should match first offload-on snapshot") 264 265 266def _check_nsim_vid(cfg, vid, expected) -> None: 267 """Checks if a VLAN is present. Only works on netdevsim.""" 268 269 nsim = cfg.get_local_nsim_dev() 270 if not nsim: 271 return 272 273 vlan_path = os.path.join(nsim.nsims[0].dfs_dir, "vlan") 274 with open(vlan_path, encoding="utf-8") as f: 275 vids = f.read() 276 found = f"ctag {vid}\n" in vids 277 ksft_eq(found, expected, 278 f"VLAN {vid} {'expected' if expected else 'not expected'}" 279 f" in debugfs") 280 281 282@ksft_variants([ 283 KsftNamedVariant("offloaded", True), 284 KsftNamedVariant("software", False), 285]) 286def test_vlan(cfg, offload) -> None: 287 """Ping through VLAN-over-macsec.""" 288 289 _require_ip_macsec(cfg) 290 if offload: 291 _require_macsec_offload(cfg) 292 else: 293 _require_ip_macsec_offload() 294 name = _macsec_name() 295 _setup_macsec_devs(cfg, name, offload=offload) 296 _setup_macsec_sa(cfg, name) 297 _setup_vlans(cfg, name, MACSEC_VLAN_VID) 298 vlan_name, remote_ip = _setup_vlan_ips(cfg, name, MACSEC_VLAN_VID) 299 _check_nsim_vid(cfg, MACSEC_VLAN_VID, offload) 300 # nsim doesn't handle the data path for offloaded macsec, so skip 301 # the ping when offloaded on nsim. 302 if not offload or not cfg.get_local_nsim_dev(): 303 cmd(f"ping -I {vlan_name} -c 1 -W 5 {remote_ip}") 304 305 306@ksft_variants([ 307 KsftNamedVariant("on_to_off", True), 308 KsftNamedVariant("off_to_on", False), 309]) 310def test_vlan_toggle(cfg, offload) -> None: 311 """Toggle offload: VLAN filters propagate/remove correctly.""" 312 313 _require_ip_macsec(cfg) 314 _require_macsec_offload(cfg) 315 name = _macsec_name() 316 _setup_macsec_devs(cfg, name, offload=offload) 317 _setup_vlans(cfg, name, MACSEC_VLAN_VID) 318 _check_nsim_vid(cfg, MACSEC_VLAN_VID, offload) 319 _set_offload(name, offload=not offload) 320 _check_nsim_vid(cfg, MACSEC_VLAN_VID, not offload) 321 vlan_name, remote_ip = _setup_vlan_ips(cfg, name, MACSEC_VLAN_VID) 322 _setup_macsec_sa(cfg, name) 323 # nsim doesn't handle the data path for offloaded macsec, so skip 324 # the ping when the final state is offloaded on nsim. 325 if offload or not cfg.get_local_nsim_dev(): 326 cmd(f"ping -I {vlan_name} -c 1 -W 5 {remote_ip}") 327 328 329def main() -> None: 330 """Main program.""" 331 with NetDrvEpEnv(__file__) as cfg: 332 ksft_run([test_offload_api, 333 test_max_secy, 334 test_max_sc, 335 test_offload_state, 336 test_vlan, 337 test_vlan_toggle, 338 ], args=(cfg,)) 339 ksft_exit() 340 341 342if __name__ == "__main__": 343 main() 344