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