xref: /linux/tools/testing/selftests/drivers/net/lib/py/env.py (revision 96fda937b4c170ae6164419f33434830423ce0e2)
1# SPDX-License-Identifier: GPL-2.0
2
3import ipaddress
4import os
5import sys
6import time
7import json
8from pathlib import Path
9from lib.py import KsftSkipEx, KsftXfailEx
10from lib.py import ksft_setup, wait_file
11from lib.py import cmd, ethtool, ip, CmdExitFailure
12from lib.py import NetNS, NetdevSimDev, UserNetNS
13from .remote import Remote
14from . import bpftool, RtnlFamily, Netlink
15
16
17class NetDrvEnvBase:
18    """
19    Base class for a NIC / host environments
20
21    Attributes:
22      test_dir: Path to the source directory of the test
23      net_lib_dir: Path to the net/lib directory
24    """
25    def __init__(self, src_path):
26        self.src_path = Path(src_path)
27        self.test_dir = self.src_path.parent.resolve()
28        self.net_lib_dir = (Path(__file__).parent / "../../../../net/lib").resolve()
29
30        self.env = self._load_env_file()
31
32        # Following attrs must be set be inheriting classes
33        self.dev = None
34
35    def _load_env_file(self):
36        env = os.environ.copy()
37
38        src_dir = Path(self.src_path).parent.resolve()
39        if not (src_dir / "net.config").exists():
40            return ksft_setup(env)
41
42        with open((src_dir / "net.config").as_posix(), 'r') as fp:
43            for line in fp.readlines():
44                full_file = line
45                # Strip comments
46                pos = line.find("#")
47                if pos >= 0:
48                    line = line[:pos]
49                line = line.strip()
50                if not line:
51                    continue
52                pair = line.split('=', maxsplit=1)
53                if len(pair) != 2:
54                    raise Exception("Can't parse configuration line:", full_file)
55                env[pair[0]] = pair[1]
56        return ksft_setup(env)
57
58    def __del__(self):
59        pass
60
61    def __enter__(self):
62        ip(f"link set dev {self.dev['ifname']} up")
63        wait_file(f"/sys/class/net/{self.dev['ifname']}/carrier",
64                  lambda x: x.strip() == "1")
65
66        return self
67
68    def __exit__(self, ex_type, ex_value, ex_tb):
69        """
70        __exit__ gets called at the end of a "with" block.
71        """
72        self.__del__()
73
74
75class NetDrvEnv(NetDrvEnvBase):
76    """
77    Class for a single NIC / host env, with no remote end
78    """
79    def __init__(self, src_path, nsim_test=None, **kwargs):
80        super().__init__(src_path)
81
82        self._ns = None
83
84        if 'NETIF' in self.env:
85            if nsim_test is True:
86                raise KsftXfailEx("Test only works on netdevsim")
87
88            self.dev = ip("-d link show dev " + self.env['NETIF'], json=True)[0]
89        else:
90            if nsim_test is False:
91                raise KsftXfailEx("Test does not work on netdevsim")
92
93            self._ns = NetdevSimDev(**kwargs)
94            self.dev = self._ns.nsims[0].dev
95        self.ifname = self.dev['ifname']
96        self.ifindex = self.dev['ifindex']
97
98    def __del__(self):
99        if self._ns:
100            self._ns.remove()
101            self._ns = None
102
103
104class NetDrvEpEnv(NetDrvEnvBase):
105    """
106    Class for an environment with a local device and "remote endpoint"
107    which can be used to send traffic in.
108
109    For local testing it creates two network namespaces and a pair
110    of netdevsim devices.
111    """
112
113    # Network prefixes used for local tests
114    nsim_v4_pfx = "192.0.2."
115    nsim_v6_pfx = "2001:db8::"
116
117    def __init__(self, src_path, nsim_test=None):
118        super().__init__(src_path)
119
120        self._stats_settle_time = None
121
122        # Things we try to destroy
123        self.remote = None
124        # These are for local testing state
125        self._netns = None
126        self._ns = None
127        self._ns_peer = None
128
129        self.addr_v        = { "4": None, "6": None }
130        self.remote_addr_v = { "4": None, "6": None }
131
132        if "NETIF" in self.env:
133            if nsim_test is True:
134                raise KsftXfailEx("Test only works on netdevsim")
135            self._check_env()
136
137            self.dev = ip("-d link show dev " + self.env['NETIF'], json=True)[0]
138
139            self.addr_v["4"] = self.env.get("LOCAL_V4")
140            self.addr_v["6"] = self.env.get("LOCAL_V6")
141            self.remote_addr_v["4"] = self.env.get("REMOTE_V4")
142            self.remote_addr_v["6"] = self.env.get("REMOTE_V6")
143            kind = self.env["REMOTE_TYPE"]
144            args = self.env["REMOTE_ARGS"]
145        else:
146            if nsim_test is False:
147                raise KsftXfailEx("Test does not work on netdevsim")
148
149            self.create_local()
150
151            self.dev = self._ns.nsims[0].dev
152
153            self.addr_v["4"] = self.nsim_v4_pfx + "1"
154            self.addr_v["6"] = self.nsim_v6_pfx + "1"
155            self.remote_addr_v["4"] = self.nsim_v4_pfx + "2"
156            self.remote_addr_v["6"] = self.nsim_v6_pfx + "2"
157            kind = "netns"
158            args = self._netns.name
159
160        self.remote = Remote(kind, args, src_path)
161
162        self.addr_ipver = "6" if self.addr_v["6"] else "4"
163        self.addr = self.addr_v[self.addr_ipver]
164        self.remote_addr = self.remote_addr_v[self.addr_ipver]
165
166        # Bracketed addresses, some commands need IPv6 to be inside []
167        self.baddr = f"[{self.addr_v['6']}]" if self.addr_v["6"] else self.addr_v["4"]
168        self.remote_baddr = f"[{self.remote_addr_v['6']}]" if self.remote_addr_v["6"] else self.remote_addr_v["4"]
169
170        self.ifname = self.dev['ifname']
171        self.ifindex = self.dev['ifindex']
172
173        # resolve remote interface name
174        self.remote_ifname = self.resolve_remote_ifc()
175        self.remote_dev = ip("-d link show dev " + self.remote_ifname,
176                             host=self.remote, json=True)[0]
177        self.remote_ifindex = self.remote_dev['ifindex']
178
179        self._required_cmd = {}
180
181    def create_local(self):
182        self._netns = NetNS()
183        self._ns = NetdevSimDev()
184        self._ns_peer = NetdevSimDev(ns=self._netns)
185
186        with open("/proc/self/ns/net") as nsfd0, \
187             open("/var/run/netns/" + self._netns.name) as nsfd1:
188            ifi0 = self._ns.nsims[0].ifindex
189            ifi1 = self._ns_peer.nsims[0].ifindex
190            NetdevSimDev.ctrl_write('link_device',
191                                    f'{nsfd0.fileno()}:{ifi0} {nsfd1.fileno()}:{ifi1}')
192
193        ip(f"   addr add dev {self._ns.nsims[0].ifname} {self.nsim_v4_pfx}1/24")
194        ip(f"-6 addr add dev {self._ns.nsims[0].ifname} {self.nsim_v6_pfx}1/64 nodad")
195        ip(f"   link set dev {self._ns.nsims[0].ifname} up")
196
197        ip(f"   addr add dev {self._ns_peer.nsims[0].ifname} {self.nsim_v4_pfx}2/24", ns=self._netns)
198        ip(f"-6 addr add dev {self._ns_peer.nsims[0].ifname} {self.nsim_v6_pfx}2/64 nodad", ns=self._netns)
199        ip(f"   link set dev {self._ns_peer.nsims[0].ifname} up", ns=self._netns)
200
201    def _check_env(self):
202        vars_needed = [
203            ["LOCAL_V4", "LOCAL_V6"],
204            ["REMOTE_V4", "REMOTE_V6"],
205            ["REMOTE_TYPE"],
206            ["REMOTE_ARGS"]
207        ]
208        missing = []
209
210        for choice in vars_needed:
211            for entry in choice:
212                if entry in self.env:
213                    break
214            else:
215                missing.append(choice)
216        # Make sure v4 / v6 configs are symmetric
217        if ("LOCAL_V6" in self.env) != ("REMOTE_V6" in self.env):
218            missing.append(["LOCAL_V6", "REMOTE_V6"])
219        if ("LOCAL_V4" in self.env) != ("REMOTE_V4" in self.env):
220            missing.append(["LOCAL_V4", "REMOTE_V4"])
221        if missing:
222            raise Exception("Invalid environment, missing configuration:", missing,
223                            "Please see tools/testing/selftests/drivers/net/README.rst")
224
225    def resolve_remote_ifc(self):
226        v4 = v6 = None
227        if self.remote_addr_v["4"]:
228            v4 = ip("addr show to " + self.remote_addr_v["4"], json=True, host=self.remote)
229        if self.remote_addr_v["6"]:
230            v6 = ip("addr show to " + self.remote_addr_v["6"], json=True, host=self.remote)
231        if v4 and v6 and v4[0]["ifname"] != v6[0]["ifname"]:
232            raise Exception("Can't resolve remote interface name, v4 and v6 don't match")
233        if (v4 and len(v4) > 1) or (v6 and len(v6) > 1):
234            raise Exception("Can't resolve remote interface name, multiple interfaces match")
235        return v6[0]["ifname"] if v6 else v4[0]["ifname"]
236
237    def __del__(self):
238        if self._ns:
239            self._ns.remove()
240            self._ns = None
241        if self._ns_peer:
242            self._ns_peer.remove()
243            self._ns_peer = None
244        if self._netns:
245            del self._netns
246            self._netns = None
247        if self.remote:
248            del self.remote
249            self.remote = None
250
251    def require_ipver(self, ipver):
252        if not self.addr_v[ipver] or not self.remote_addr_v[ipver]:
253            raise KsftSkipEx(f"Test requires IPv{ipver} connectivity")
254
255    def require_nsim(self, nsim_test=True):
256        """Require or exclude netdevsim for this test"""
257        if nsim_test and self._ns is None:
258            raise KsftXfailEx("Test only works on netdevsim")
259        if nsim_test is False and self._ns is not None:
260            raise KsftXfailEx("Test does not work on netdevsim")
261
262    def get_local_nsim_dev(self):
263        """Returns the local netdevsim device or None.
264           Using this method is discouraged, as it makes tests nsim-specific.
265           Standard interfaces available on all HW should ideally be used.
266           This method is intended for the few cases where nsim-specific
267           assertions need to be verified which cannot be verified otherwise.
268        """
269        return self._ns
270
271    def _require_cmd(self, comm, key, host=None):
272        cached = self._required_cmd.get(comm, {})
273        if cached.get(key) is None:
274            cached[key] = cmd("command -v -- " + comm, fail=False,
275                              shell=True, host=host).ret == 0
276        self._required_cmd[comm] = cached
277        return cached[key]
278
279    def require_cmd(self, comm, local=True, remote=False):
280        if local:
281            if not self._require_cmd(comm, "local"):
282                raise KsftSkipEx("Test requires command: " + comm)
283        if remote:
284            if not self._require_cmd(comm, "remote", host=self.remote):
285                raise KsftSkipEx("Test requires (remote) command: " + comm)
286
287    def wait_hw_stats_settle(self):
288        """
289        Wait for HW stats to become consistent, some devices DMA HW stats
290        periodically so events won't be reflected until next sync.
291        Good drivers will tell us via ethtool what their sync period is.
292        """
293        if self._stats_settle_time is None:
294            data = {}
295            try:
296                data = ethtool("-c " + self.ifname, json=True)[0]
297            except CmdExitFailure as e:
298                if "Operation not supported" not in e.cmd.stderr:
299                    raise
300
301            self._stats_settle_time = \
302                1.25 * data.get('stats-block-usecs', 20000) / 1000 / 1000
303
304        time.sleep(self._stats_settle_time)
305
306
307class NetDrvContEnv(NetDrvEpEnv):
308    """
309    Class for an environment with a netkit pair setup for forwarding traffic
310    between the physical interface and a network namespace.
311      NETIF           = "eth0"
312      LOCAL_V6        = "2001:db8:1::1"
313      REMOTE_V6       = "2001:db8:1::2"
314      LOCAL_PREFIX_V6 = "2001:db8:2::0/64"
315
316              +-----------------------------+        +------------------------------+
317      dst     | INIT NS                     |        | TEST NS                      |
318      2001:   | +---------------+           |        |                              |
319      db8:2::2| | NETIF         |           |  bpf   |                              |
320          +---|>| 2001:db8:1::1 |           |redirect| +-------------------------+  |
321          |   | |               |-----------|--------|>| Netkit                  |  |
322          |   | +---------------+           | _peer  | | nk_guest                |  |
323          |   | +-------------+ Netkit pair |        | | fe80::2/64              |  |
324          |   | | Netkit      |.............|........|>| 2001:db8:2::2/64        |  |
325          |   | | nk_host     |             |        | +-------------------------+  |
326          |   | | fe80::1/64  |             |        |                              |
327          |   | +-------------+             |        | route:                       |
328          |   |                             |        |   default                    |
329          |   | route:                      |        |     via fe80::1 dev nk_guest |
330          |   |   2001:db8:2::2/128         |        +------------------------------+
331          |   |     via fe80::2 dev nk_host |
332          |   +-----------------------------+
333          |
334          |   +---------------+
335          |   | REMOTE        |
336          +---| 2001:db8:1::2 |
337              +---------------+
338    """
339
340    def __init__(self, src_path, rxqueues=1, primary_rx_redirect=False,
341                 userns=False, **kwargs):
342        self.netns = None
343        self._userns = userns
344        self.nk_host_ifname = None
345        self.nk_guest_ifname = None
346        self._tc_clsact_added = False
347        self._tc_attached = False
348        self._primary_rx_redirect_attached = False
349        self._primary_rx_redirect_clsact_added = False
350        self._bpf_prog_pref = None
351        self._bpf_prog_id = None
352        self._init_ns_attached = False
353        self._remote_route_added = False
354        self._old_fwd = None
355        self._old_accept_ra = None
356
357        super().__init__(src_path, **kwargs)
358
359        self.require_ipver("6")
360        local_prefix = self.env.get("LOCAL_PREFIX_V6")
361        if not local_prefix:
362            raise KsftSkipEx("LOCAL_PREFIX_V6 required")
363
364        net = ipaddress.IPv6Network(local_prefix, strict=False)
365        self.ipv6_prefix = str(net.network_address)
366        self.nk_host_ipv6 = f"{self.ipv6_prefix}2:1"
367        self.nk_guest_ipv6 = f"{self.ipv6_prefix}2:2"
368
369        local_v6 = ipaddress.IPv6Address(self.addr_v["6"])
370        if local_v6 in net:
371            raise KsftSkipEx("LOCAL_V6 must not fall within LOCAL_PREFIX_V6")
372
373        rtnl = RtnlFamily()
374        rtnl.newlink(
375            {
376                "linkinfo": {
377                    "kind": "netkit",
378                    "data": {
379                        "mode": "l2",
380                        "policy": "forward",
381                        "peer-policy": "forward",
382                    },
383                },
384                "num-rx-queues": rxqueues,
385            },
386            flags=[Netlink.NLM_F_CREATE, Netlink.NLM_F_EXCL],
387        )
388
389        all_links = ip("-d link show", json=True)
390        netkit_links = [link for link in all_links
391                        if link.get('linkinfo', {}).get('info_kind') == 'netkit'
392                        and 'UP' not in link.get('flags', [])]
393
394        if len(netkit_links) != 2:
395            raise KsftSkipEx("Failed to create netkit pair")
396
397        netkit_links.sort(key=lambda x: x['ifindex'])
398        self.nk_host_ifname = netkit_links[1]['ifname']
399        self.nk_guest_ifname = netkit_links[0]['ifname']
400        self.nk_host_ifindex = netkit_links[1]['ifindex']
401        self.nk_guest_ifindex = netkit_links[0]['ifindex']
402
403        self._setup_ns()
404        self.attach_bpf()
405        if primary_rx_redirect:
406            self._attach_primary_rx_redirect_bpf()
407
408    def __del__(self):
409        if self._primary_rx_redirect_attached:
410            cmd(f"tc filter del dev {self.nk_host_ifname} ingress", fail=False)
411            self._primary_rx_redirect_attached = False
412
413        if self._primary_rx_redirect_clsact_added:
414            cmd(f"tc qdisc del dev {self.nk_host_ifname} clsact", fail=False)
415            self._primary_rx_redirect_clsact_added = False
416
417        if self._tc_attached:
418            cmd(f"tc filter del dev {self.ifname} ingress pref {self._bpf_prog_pref}")
419            self._tc_attached = False
420
421        if self._tc_clsact_added:
422            cmd(f"tc qdisc del dev {self.ifname} clsact")
423            self._tc_clsact_added = False
424
425        if self._remote_route_added:
426            cmd(f"ip -6 route del {self.nk_guest_ipv6}/128",
427                host=self.remote, fail=False)
428            self._remote_route_added = False
429
430        if self.nk_host_ifname:
431            cmd(f"ip link del dev {self.nk_host_ifname}")
432            self.nk_host_ifname = None
433            self.nk_guest_ifname = None
434
435        if self._init_ns_attached:
436            cmd("ip netns del init", fail=False)
437            self._init_ns_attached = False
438
439        if self.netns:
440            del self.netns
441            self.netns = None
442
443        if self._old_fwd is not None:
444            with open("/proc/sys/net/ipv6/conf/all/forwarding", "w",
445                      encoding="utf-8") as f:
446                f.write(self._old_fwd)
447            self._old_fwd = None
448        if self._old_accept_ra is not None:
449            with open("/proc/sys/net/ipv6/conf/all/accept_ra", "w",
450                      encoding="utf-8") as f:
451                f.write(self._old_accept_ra)
452            self._old_accept_ra = None
453
454        super().__del__()
455
456    def _setup_ns(self):
457        fwd_path = "/proc/sys/net/ipv6/conf/all/forwarding"
458        ra_path = "/proc/sys/net/ipv6/conf/all/accept_ra"
459        with open(fwd_path, encoding="utf-8") as f:
460            self._old_fwd = f.read().strip()
461        with open(ra_path, encoding="utf-8") as f:
462            self._old_accept_ra = f.read().strip()
463        with open(fwd_path, "w", encoding="utf-8") as f:
464            f.write("1")
465        with open(ra_path, "w", encoding="utf-8") as f:
466            f.write("2")
467
468        self.netns = UserNetNS() if self._userns else NetNS()
469        cmd("ip netns attach init 1")
470        self._init_ns_attached = True
471        ip("netns set init 0", ns=self.netns)
472        ip(f"link set dev {self.nk_guest_ifname} netns {self.netns.name}")
473        nk_guest_dev = ip(f"link show dev {self.nk_guest_ifname}",
474                          json=True, ns=self.netns)[0]
475        self.nk_guest_ifindex = nk_guest_dev['ifindex']
476        ip(f"link set dev {self.nk_host_ifname} up")
477        ip(f"-6 addr add fe80::1/64 dev {self.nk_host_ifname} nodad")
478        ip(f"-6 route add {self.nk_guest_ipv6}/128 via fe80::2 dev {self.nk_host_ifname}")
479
480        ip("link set lo up", ns=self.netns)
481        ip(f"link set dev {self.nk_guest_ifname} up", ns=self.netns)
482        ip(f"-6 addr add fe80::2/64 dev {self.nk_guest_ifname}", ns=self.netns)
483        ip(f"-6 addr add {self.nk_guest_ipv6}/64 dev {self.nk_guest_ifname} nodad", ns=self.netns)
484        ip(f"-6 route add default via fe80::1 dev {self.nk_guest_ifname}", ns=self.netns)
485
486    def _tc_ensure_clsact(self, ifname=None):
487        """Ensure a clsact qdisc exists on @ifname.
488
489        Returns True if this call added the qdisc, otherwise returns False.
490        """
491        if ifname is None:
492            ifname = self.ifname
493        qdisc = json.loads(cmd(f"tc -j qdisc show dev {ifname}").stdout)
494        for q in qdisc:
495            if q['kind'] == 'clsact':
496                return False
497        cmd(f"tc qdisc add dev {ifname} clsact")
498        return True
499
500    def _get_bpf_prog_ids(self):
501        filters = json.loads(cmd(f"tc -j filter show dev {self.ifname} ingress").stdout)
502        for bpf in filters:
503            if 'options' not in bpf:
504                continue
505            if bpf['options']['bpf_name'].startswith('nk_forward.bpf'):
506                return (bpf['pref'], bpf['options']['prog']['id'])
507        raise Exception("Failed to get BPF prog ID")
508
509    def _find_bss_map_id(self, prog_id):
510        """Find the .bss map ID for a loaded BPF program."""
511        prog_info = bpftool(f"prog show id {prog_id}", json=True)
512        for map_id in prog_info.get("map_ids", []):
513            map_info = bpftool(f"map show id {map_id}", json=True)
514            if map_info.get("name", "").endswith("bss"):
515                return map_id
516        raise Exception(f"Failed to find .bss map for prog {prog_id}")
517
518    def _find_bpf_obj(self, name):
519        bpf_obj = self.test_dir / name
520        if bpf_obj.exists():
521            return bpf_obj
522        bpf_obj = self.test_dir / "hw" / name
523        if bpf_obj.exists():
524            return bpf_obj
525        return None
526
527    def detach_bpf(self):
528        if self._tc_attached:
529            cmd(f"tc filter del dev {self.ifname} ingress pref "
530                f"{self._bpf_prog_pref}", fail=False)
531            self._tc_attached = False
532
533    def attach_bpf(self):
534        bpf_obj = self._find_bpf_obj("nk_forward.bpf.o")
535        if not bpf_obj:
536            raise KsftSkipEx("BPF prog nk_forward.bpf.o not found")
537
538        if self._tc_ensure_clsact():
539            self._tc_clsact_added = True
540        cmd(f"tc filter add dev {self.ifname} ingress bpf obj {bpf_obj}"
541            " sec tc/ingress direct-action")
542        self._tc_attached = True
543
544        (self._bpf_prog_pref, self._bpf_prog_id) = self._get_bpf_prog_ids()
545        bss_map_id = self._find_bss_map_id(self._bpf_prog_id)
546
547        ipv6_addr = ipaddress.IPv6Address(self.ipv6_prefix)
548        ipv6_bytes = ipv6_addr.packed
549        ifindex_bytes = self.nk_host_ifindex.to_bytes(4, byteorder='little')
550        value = ipv6_bytes + ifindex_bytes
551        value_hex = ' '.join(f'{b:02x}' for b in value)
552        bpftool(f"map update id {bss_map_id} key hex 00 00 00 00 value hex {value_hex}")
553
554    def _attach_primary_rx_redirect_bpf(self):
555        """Attach BPF redirect program on the primary netkit ingress."""
556        bpf_obj = self._find_bpf_obj("nk_primary_rx_redirect.bpf.o")
557        if not bpf_obj:
558            raise KsftSkipEx("nk_primary_rx_redirect.bpf.o not found")
559
560        if self._tc_ensure_clsact(self.nk_host_ifname):
561            self._primary_rx_redirect_clsact_added = True
562        cmd(f"tc filter add dev {self.nk_host_ifname} ingress"
563            f" bpf obj {bpf_obj} sec tc/ingress direct-action")
564        self._primary_rx_redirect_attached = True
565
566        ip(f"-6 route add {self.nk_guest_ipv6}/128 via {self.addr_v['6']}",
567           host=self.remote)
568        self._remote_route_added = True
569
570        filters = json.loads(
571            cmd(f"tc -j filter show dev {self.nk_host_ifname} ingress").stdout)
572        redirect_prog_id = None
573        for bpf in filters:
574            if 'options' not in bpf:
575                continue
576            if bpf['options']['bpf_name'].startswith('nk_primary_rx_redirect'):
577                redirect_prog_id = bpf['options']['prog']['id']
578                break
579        if redirect_prog_id is None:
580            raise Exception("Failed to get primary RX redirect BPF prog ID")
581
582        bss_map_id = self._find_bss_map_id(redirect_prog_id)
583        phys_ifindex_bytes = self.ifindex.to_bytes(4, byteorder=sys.byteorder)
584        value_hex = ' '.join(f'{b:02x}' for b in phys_ifindex_bytes)
585        bpftool(f"map update id {bss_map_id} key hex 00 00 00 00 value hex {value_hex}")
586