xref: /linux/tools/testing/selftests/drivers/net/macsec.py (revision 056d046bf530d44bff5ce13a0addbc50c14ac95f)
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