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=None): 38 """SKIP if iproute2 lacks MACsec locally or on cfg.remote, when given.""" 39 hosts = [None, cfg.remote] if cfg is not None else [None] 40 for host in hosts: 41 out = cmd("ip macsec help", fail=False, host=host) 42 if "Usage" not in out.stdout + out.stderr: 43 where = "remote" if host else "local" 44 raise KsftSkipEx(f"iproute2 too old on {where}," 45 " missing macsec support") 46 47 48def _require_ip_macsec_offload(): 49 """SKIP if local iproute2 doesn't understand 'ip macsec offload'.""" 50 out = cmd("ip macsec help", fail=False) 51 if "offload" not in out.stdout + out.stderr: 52 raise KsftSkipEx("iproute2 too old, missing macsec offload") 53 54 55def _require_macsec_offload(cfg): 56 """SKIP if local device doesn't support macsec-hw-offload.""" 57 _require_ip_macsec_offload() 58 try: 59 feat = ethtool(f"-k {cfg.ifname}", json=True)[0] 60 except (CmdExitFailure, IndexError) as e: 61 raise KsftSkipEx( 62 f"can't query features: {e}") from e 63 if not feat.get("macsec-hw-offload", {}).get("active"): 64 raise KsftSkipEx("macsec-hw-offload not supported") 65 66 67def _get_mac(ifname, host=None): 68 """Gets MAC address of an interface.""" 69 dev = ip(f"link show dev {ifname}", json=True, host=host) 70 return dev[0]["address"] 71 72 73def _setup_macsec_sa(cfg, name): 74 """Adds matching TX/RX SAs on both ends.""" 75 local_mac = _get_mac(name) 76 remote_mac = _get_mac(name, host=cfg.remote) 77 78 ip(f"macsec add {name} tx sa 0 pn 1 on key 01 {MACSEC_KEY}") 79 ip(f"macsec add {name} rx port 1 address {remote_mac}") 80 ip(f"macsec add {name} rx port 1 address {remote_mac} " 81 f"sa 0 pn 1 on key 02 {MACSEC_KEY}") 82 83 ip(f"macsec add {name} tx sa 0 pn 1 on key 02 {MACSEC_KEY}", 84 host=cfg.remote) 85 ip(f"macsec add {name} rx port 1 address {local_mac}", host=cfg.remote) 86 ip(f"macsec add {name} rx port 1 address {local_mac} " 87 f"sa 0 pn 1 on key 01 {MACSEC_KEY}", host=cfg.remote) 88 89 90def _setup_macsec_devs(cfg, name, offload): 91 """Creates macsec devices on both ends. 92 93 Only the local device gets HW offload; the remote always uses software 94 MACsec since it may not support offload at all. 95 """ 96 offload_arg = "mac" if offload else "off" 97 98 ip(f"link add link {cfg.ifname} {name} " 99 f"type macsec encrypt on offload {offload_arg}") 100 defer(ip, f"link del {name}") 101 ip(f"link add link {cfg.remote_ifname} {name} " 102 f"type macsec encrypt on", host=cfg.remote) 103 defer(ip, f"link del {name}", host=cfg.remote) 104 105 106def _set_offload(name, offload): 107 """Sets offload on the local macsec device only.""" 108 offload_arg = "mac" if offload else "off" 109 110 ip(f"link set {name} type macsec encrypt on offload {offload_arg}") 111 112 113def _setup_vlans(cfg, name, vid): 114 """Adds VLANs on top of existing macsec devs.""" 115 vlan_name = f"{name}.{vid}" 116 117 ip(f"link add link {name} {vlan_name} type vlan id {vid}") 118 defer(ip, f"link del {vlan_name}") 119 ip(f"link add link {name} {vlan_name} type vlan id {vid}", host=cfg.remote) 120 defer(ip, f"link del {vlan_name}", host=cfg.remote) 121 122 123def _setup_vlan_ips(cfg, name, vid): 124 """Adds VLANs and IPs and brings up the macsec + VLAN devices.""" 125 local_ip = "198.51.100.1" 126 remote_ip = "198.51.100.2" 127 vlan_name = f"{name}.{vid}" 128 129 ip(f"addr add {local_ip}/24 dev {vlan_name}") 130 ip(f"addr add {remote_ip}/24 dev {vlan_name}", host=cfg.remote) 131 ip(f"link set {name} up") 132 ip(f"link set {name} up", host=cfg.remote) 133 ip(f"link set {vlan_name} up") 134 ip(f"link set {vlan_name} up", host=cfg.remote) 135 136 return vlan_name, remote_ip 137 138 139def test_offload_api(cfg) -> None: 140 """MACsec offload API: create SecY, add SA/rx, toggle offload.""" 141 142 _require_macsec_offload(cfg) 143 ms0 = _macsec_name(0) 144 ms1 = _macsec_name(1) 145 ms2 = _macsec_name(2) 146 147 # Create 3 SecY with offload 148 ip(f"link add link {cfg.ifname} {ms0} type macsec " 149 f"port 4 encrypt on offload mac") 150 defer(ip, f"link del {ms0}") 151 152 ip(f"link add link {cfg.ifname} {ms1} type macsec " 153 f"address aa:bb:cc:dd:ee:ff port 5 encrypt on offload mac") 154 defer(ip, f"link del {ms1}") 155 156 ip(f"link add link {cfg.ifname} {ms2} type macsec " 157 f"sci abbacdde01020304 encrypt on offload mac") 158 defer(ip, f"link del {ms2}") 159 160 # Add TX SA 161 ip(f"macsec add {ms0} tx sa 0 pn 1024 on " 162 "key 01 12345678901234567890123456789012") 163 164 # Add RX SC + SA 165 ip(f"macsec add {ms0} rx port 1234 address 1c:ed:de:ad:be:ef") 166 ip(f"macsec add {ms0} rx port 1234 address 1c:ed:de:ad:be:ef " 167 "sa 0 pn 1 on key 00 0123456789abcdef0123456789abcdef") 168 169 # Can't disable offload when SAs are configured 170 with ksft_raises(CmdExitFailure): 171 ip(f"link set {ms0} type macsec offload off") 172 with ksft_raises(CmdExitFailure): 173 ip(f"macsec offload {ms0} off") 174 175 # Toggle offload via rtnetlink on SA-free device 176 ip(f"link set {ms2} type macsec offload off") 177 ip(f"link set {ms2} type macsec encrypt on offload mac") 178 179 # Toggle offload via genetlink 180 ip(f"macsec offload {ms2} off") 181 ip(f"macsec offload {ms2} mac") 182 183 184def test_max_secy(cfg) -> None: 185 """nsim-only test for max number of SecYs.""" 186 187 cfg.require_nsim() 188 _require_ip_macsec_offload() 189 ms0 = _macsec_name(0) 190 ms1 = _macsec_name(1) 191 ms2 = _macsec_name(2) 192 ms3 = _macsec_name(3) 193 194 ip(f"link add link {cfg.ifname} {ms0} type macsec " 195 f"port 4 encrypt on offload mac") 196 defer(ip, f"link del {ms0}") 197 198 ip(f"link add link {cfg.ifname} {ms1} type macsec " 199 f"address aa:bb:cc:dd:ee:ff port 5 encrypt on offload mac") 200 defer(ip, f"link del {ms1}") 201 202 ip(f"link add link {cfg.ifname} {ms2} type macsec " 203 f"sci abbacdde01020304 encrypt on offload mac") 204 defer(ip, f"link del {ms2}") 205 with ksft_raises(CmdExitFailure): 206 ip(f"link add link {cfg.ifname} {ms3} " 207 f"type macsec port 8 encrypt on offload mac") 208 209 210def test_max_sc(cfg) -> None: 211 """nsim-only test for max number of SCs.""" 212 213 cfg.require_nsim() 214 _require_ip_macsec_offload() 215 ms0 = _macsec_name(0) 216 217 ip(f"link add link {cfg.ifname} {ms0} type macsec " 218 f"port 4 encrypt on offload mac") 219 defer(ip, f"link del {ms0}") 220 ip(f"macsec add {ms0} rx port 1234 address 1c:ed:de:ad:be:ef") 221 with ksft_raises(CmdExitFailure): 222 ip(f"macsec add {ms0} rx port 1235 address 1c:ed:de:ad:be:ef") 223 224 225def test_offload_state(cfg) -> None: 226 """Offload state reflects configuration changes.""" 227 228 _require_macsec_offload(cfg) 229 ms0 = _macsec_name(0) 230 231 # Create with offload on 232 ip(f"link add link {cfg.ifname} {ms0} type macsec " 233 f"encrypt on offload mac") 234 cleanup = defer(ip, f"link del {ms0}") 235 236 ksft_eq(_get_macsec_offload(ms0), "mac", 237 "created with offload: should be mac") 238 feats_on_1 = _get_features(ms0) 239 240 ip(f"link set {ms0} type macsec offload off") 241 ksft_eq(_get_macsec_offload(ms0), "off", 242 "offload disabled: should be off") 243 feats_off_1 = _get_features(ms0) 244 245 ip(f"link set {ms0} type macsec encrypt on offload mac") 246 ksft_eq(_get_macsec_offload(ms0), "mac", 247 "offload re-enabled: should be mac") 248 ksft_eq(_get_features(ms0), feats_on_1, 249 "features should match first offload-on snapshot") 250 251 # Delete and recreate without offload 252 cleanup.exec() 253 ip(f"link add link {cfg.ifname} {ms0} type macsec") 254 defer(ip, f"link del {ms0}") 255 ksft_eq(_get_macsec_offload(ms0), "off", 256 "created without offload: should be off") 257 ksft_eq(_get_features(ms0), feats_off_1, 258 "features should match first offload-off snapshot") 259 260 ip(f"link set {ms0} type macsec encrypt on offload mac") 261 ksft_eq(_get_macsec_offload(ms0), "mac", 262 "offload enabled after create: should be mac") 263 ksft_eq(_get_features(ms0), feats_on_1, 264 "features should match first offload-on snapshot") 265 266 267@ksft_variants([ 268 KsftNamedVariant("default", "", ""), 269 KsftNamedVariant("explicit", "", "sci {sci}"), 270 KsftNamedVariant("undefined", "", "sci ffffffffffffffff"), 271 KsftNamedVariant("undefined_default", "sci ffffffffffffffff", ""), 272 KsftNamedVariant("undefined_explicit", "sci ffffffffffffffff", "sci {sci}"), 273 KsftNamedVariant("undefined_twice", "sci ffffffffffffffff", 274 "sci ffffffffffffffff"), 275]) 276def test_duplicate_sci(cfg, first, second) -> None: 277 """Reject duplicate transmit SCIs, including the undefined-SCI fallback.""" 278 279 _require_ip_macsec() 280 ms0 = _macsec_name(0) 281 ms1 = _macsec_name(1) 282 sci = _get_mac(cfg.ifname).replace(":", "") + "0001" 283 284 ip(f"link add link {cfg.ifname} {ms0} type macsec {first}") 285 defer(ip, f"link del {ms0}") 286 with ksft_raises(CmdExitFailure): 287 ip(f"link add link {cfg.ifname} {ms1} type macsec " 288 f"{second.format(sci=sci)}") 289 # Clean up if the kernel incorrectly accepted the duplicate. 290 defer(ip, f"link del {ms1}") 291 292 293def test_undefined_sci(cfg) -> None: 294 """An undefined SCI still selects the default when it is available.""" 295 296 _require_ip_macsec() 297 ms0 = _macsec_name(0) 298 ms1 = _macsec_name(1) 299 sci = _get_mac(cfg.ifname).replace(":", "") + "0001" 300 301 # A different port on the same lower device must not block the fallback. 302 ip(f"link add link {cfg.ifname} {ms0} type macsec port 2") 303 defer(ip, f"link del {ms0}") 304 ip(f"link add link {cfg.ifname} {ms1} type macsec sci ffffffffffffffff") 305 cleanup = defer(ip, f"link del {ms1}") 306 info = ip(f"-d link show dev {ms1}", json=True)[0] 307 ksft_eq(info["linkinfo"]["info_data"]["sci"], sci) 308 309 # Deleting a device must make its SCI available again. 310 cleanup.exec() 311 ip(f"link add link {cfg.ifname} {ms1} type macsec sci ffffffffffffffff") 312 defer(ip, f"link del {ms1}") 313 314 315def _check_nsim_vid(cfg, vid, expected) -> None: 316 """Checks if a VLAN is present. Only works on netdevsim.""" 317 318 nsim = cfg.get_local_nsim_dev() 319 if not nsim: 320 return 321 322 vlan_path = os.path.join(nsim.nsims[0].dfs_dir, "vlan") 323 with open(vlan_path, encoding="utf-8") as f: 324 vids = f.read() 325 found = f"ctag {vid}\n" in vids 326 ksft_eq(found, expected, 327 f"VLAN {vid} {'expected' if expected else 'not expected'}" 328 f" in debugfs") 329 330 331@ksft_variants([ 332 KsftNamedVariant("offloaded", True), 333 KsftNamedVariant("software", False), 334]) 335def test_vlan(cfg, offload) -> None: 336 """Ping through VLAN-over-macsec.""" 337 338 _require_ip_macsec(cfg) 339 if offload: 340 _require_macsec_offload(cfg) 341 else: 342 _require_ip_macsec_offload() 343 name = _macsec_name() 344 _setup_macsec_devs(cfg, name, offload=offload) 345 _setup_macsec_sa(cfg, name) 346 _setup_vlans(cfg, name, MACSEC_VLAN_VID) 347 vlan_name, remote_ip = _setup_vlan_ips(cfg, name, MACSEC_VLAN_VID) 348 _check_nsim_vid(cfg, MACSEC_VLAN_VID, offload) 349 # nsim doesn't handle the data path for offloaded macsec, so skip 350 # the ping when offloaded on nsim. 351 if not offload or not cfg.get_local_nsim_dev(): 352 cmd(f"ping -I {vlan_name} -c 1 -W 5 {remote_ip}") 353 354 355@ksft_variants([ 356 KsftNamedVariant("on_to_off", True), 357 KsftNamedVariant("off_to_on", False), 358]) 359def test_vlan_toggle(cfg, offload) -> None: 360 """Toggle offload: VLAN filters propagate/remove correctly.""" 361 362 _require_ip_macsec(cfg) 363 _require_macsec_offload(cfg) 364 name = _macsec_name() 365 _setup_macsec_devs(cfg, name, offload=offload) 366 _setup_vlans(cfg, name, MACSEC_VLAN_VID) 367 _check_nsim_vid(cfg, MACSEC_VLAN_VID, offload) 368 _set_offload(name, offload=not offload) 369 _check_nsim_vid(cfg, MACSEC_VLAN_VID, not offload) 370 vlan_name, remote_ip = _setup_vlan_ips(cfg, name, MACSEC_VLAN_VID) 371 _setup_macsec_sa(cfg, name) 372 # nsim doesn't handle the data path for offloaded macsec, so skip 373 # the ping when the final state is offloaded on nsim. 374 if offload or not cfg.get_local_nsim_dev(): 375 cmd(f"ping -I {vlan_name} -c 1 -W 5 {remote_ip}") 376 377 378def main() -> None: 379 """Main program.""" 380 with NetDrvEpEnv(__file__) as cfg: 381 ksft_run([test_offload_api, 382 test_max_secy, 383 test_max_sc, 384 test_offload_state, 385 test_duplicate_sci, 386 test_undefined_sci, 387 test_vlan, 388 test_vlan_toggle, 389 ], args=(cfg,)) 390 ksft_exit() 391 392 393if __name__ == "__main__": 394 main() 395