xref: /linux/tools/testing/selftests/drivers/net/lib/py/env.py (revision 6a20b34fe3b31b292078bc79ec18a2ab0d9f7719)
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
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, **kwargs):
341        self.netns = None
342        self._nk_host_ifname = None
343        self.nk_guest_ifname = None
344        self._tc_clsact_added = False
345        self._tc_attached = False
346        self._primary_rx_redirect_attached = False
347        self._primary_rx_redirect_clsact_added = False
348        self._bpf_prog_pref = None
349        self._bpf_prog_id = None
350        self._init_ns_attached = False
351        self._remote_route_added = False
352        self._old_fwd = None
353        self._old_accept_ra = None
354
355        super().__init__(src_path, **kwargs)
356
357        self.require_ipver("6")
358        local_prefix = self.env.get("LOCAL_PREFIX_V6")
359        if not local_prefix:
360            raise KsftSkipEx("LOCAL_PREFIX_V6 required")
361
362        net = ipaddress.IPv6Network(local_prefix, strict=False)
363        self.ipv6_prefix = str(net.network_address)
364        self.nk_host_ipv6 = f"{self.ipv6_prefix}2:1"
365        self.nk_guest_ipv6 = f"{self.ipv6_prefix}2:2"
366
367        local_v6 = ipaddress.IPv6Address(self.addr_v["6"])
368        if local_v6 in net:
369            raise KsftSkipEx("LOCAL_V6 must not fall within LOCAL_PREFIX_V6")
370
371        rtnl = RtnlFamily()
372        rtnl.newlink(
373            {
374                "linkinfo": {
375                    "kind": "netkit",
376                    "data": {
377                        "mode": "l2",
378                        "policy": "forward",
379                        "peer-policy": "forward",
380                    },
381                },
382                "num-rx-queues": rxqueues,
383            },
384            flags=[Netlink.NLM_F_CREATE, Netlink.NLM_F_EXCL],
385        )
386
387        all_links = ip("-d link show", json=True)
388        netkit_links = [link for link in all_links
389                        if link.get('linkinfo', {}).get('info_kind') == 'netkit'
390                        and 'UP' not in link.get('flags', [])]
391
392        if len(netkit_links) != 2:
393            raise KsftSkipEx("Failed to create netkit pair")
394
395        netkit_links.sort(key=lambda x: x['ifindex'])
396        self._nk_host_ifname = netkit_links[1]['ifname']
397        self.nk_guest_ifname = netkit_links[0]['ifname']
398        self.nk_host_ifindex = netkit_links[1]['ifindex']
399        self.nk_guest_ifindex = netkit_links[0]['ifindex']
400
401        self._setup_ns()
402        self._attach_bpf()
403        if primary_rx_redirect:
404            self._attach_primary_rx_redirect_bpf()
405
406    def __del__(self):
407        if self._primary_rx_redirect_attached:
408            cmd(f"tc filter del dev {self._nk_host_ifname} ingress", fail=False)
409            self._primary_rx_redirect_attached = False
410
411        if self._primary_rx_redirect_clsact_added:
412            cmd(f"tc qdisc del dev {self._nk_host_ifname} clsact", fail=False)
413            self._primary_rx_redirect_clsact_added = False
414
415        if self._tc_attached:
416            cmd(f"tc filter del dev {self.ifname} ingress pref {self._bpf_prog_pref}")
417            self._tc_attached = False
418
419        if self._tc_clsact_added:
420            cmd(f"tc qdisc del dev {self.ifname} clsact")
421            self._tc_clsact_added = False
422
423        if self._remote_route_added:
424            cmd(f"ip -6 route del {self.nk_guest_ipv6}/128",
425                host=self.remote, fail=False)
426            self._remote_route_added = False
427
428        if self._nk_host_ifname:
429            cmd(f"ip link del dev {self._nk_host_ifname}")
430            self._nk_host_ifname = None
431            self.nk_guest_ifname = None
432
433        if self._init_ns_attached:
434            cmd("ip netns del init", fail=False)
435            self._init_ns_attached = False
436
437        if self.netns:
438            del self.netns
439            self.netns = None
440
441        if self._old_fwd is not None:
442            with open("/proc/sys/net/ipv6/conf/all/forwarding", "w",
443                      encoding="utf-8") as f:
444                f.write(self._old_fwd)
445            self._old_fwd = None
446        if self._old_accept_ra is not None:
447            with open("/proc/sys/net/ipv6/conf/all/accept_ra", "w",
448                      encoding="utf-8") as f:
449                f.write(self._old_accept_ra)
450            self._old_accept_ra = None
451
452        super().__del__()
453
454    def _setup_ns(self):
455        fwd_path = "/proc/sys/net/ipv6/conf/all/forwarding"
456        ra_path = "/proc/sys/net/ipv6/conf/all/accept_ra"
457        with open(fwd_path, encoding="utf-8") as f:
458            self._old_fwd = f.read().strip()
459        with open(ra_path, encoding="utf-8") as f:
460            self._old_accept_ra = f.read().strip()
461        with open(fwd_path, "w", encoding="utf-8") as f:
462            f.write("1")
463        with open(ra_path, "w", encoding="utf-8") as f:
464            f.write("2")
465
466        self.netns = NetNS()
467        cmd("ip netns attach init 1")
468        self._init_ns_attached = True
469        ip("netns set init 0", ns=self.netns)
470        ip(f"link set dev {self.nk_guest_ifname} netns {self.netns.name}")
471        ip(f"link set dev {self._nk_host_ifname} up")
472        ip(f"-6 addr add fe80::1/64 dev {self._nk_host_ifname} nodad")
473        ip(f"-6 route add {self.nk_guest_ipv6}/128 via fe80::2 dev {self._nk_host_ifname}")
474
475        ip("link set lo up", ns=self.netns)
476        ip(f"link set dev {self.nk_guest_ifname} up", ns=self.netns)
477        ip(f"-6 addr add fe80::2/64 dev {self.nk_guest_ifname}", ns=self.netns)
478        ip(f"-6 addr add {self.nk_guest_ipv6}/64 dev {self.nk_guest_ifname} nodad", ns=self.netns)
479        ip(f"-6 route add default via fe80::1 dev {self.nk_guest_ifname}", ns=self.netns)
480
481    def _tc_ensure_clsact(self, ifname=None):
482        """Ensure a clsact qdisc exists on @ifname.
483
484        Returns True if this call added the qdisc, otherwise returns False.
485        """
486        if ifname is None:
487            ifname = self.ifname
488        qdisc = json.loads(cmd(f"tc -j qdisc show dev {ifname}").stdout)
489        for q in qdisc:
490            if q['kind'] == 'clsact':
491                return False
492        cmd(f"tc qdisc add dev {ifname} clsact")
493        return True
494
495    def _get_bpf_prog_ids(self):
496        filters = json.loads(cmd(f"tc -j filter show dev {self.ifname} ingress").stdout)
497        for bpf in filters:
498            if 'options' not in bpf:
499                continue
500            if bpf['options']['bpf_name'].startswith('nk_forward.bpf'):
501                return (bpf['pref'], bpf['options']['prog']['id'])
502        raise Exception("Failed to get BPF prog ID")
503
504    def _find_bss_map_id(self, prog_id):
505        """Find the .bss map ID for a loaded BPF program."""
506        prog_info = bpftool(f"prog show id {prog_id}", json=True)
507        for map_id in prog_info.get("map_ids", []):
508            map_info = bpftool(f"map show id {map_id}", json=True)
509            if map_info.get("name", "").endswith("bss"):
510                return map_id
511        raise Exception(f"Failed to find .bss map for prog {prog_id}")
512
513    def _attach_bpf(self):
514        bpf_obj = self.test_dir / "nk_forward.bpf.o"
515        if not bpf_obj.exists():
516            raise KsftSkipEx("BPF prog not found")
517
518        if self._tc_ensure_clsact():
519            self._tc_clsact_added = True
520        cmd(f"tc filter add dev {self.ifname} ingress bpf obj {bpf_obj}"
521            " sec tc/ingress direct-action")
522        self._tc_attached = True
523
524        (self._bpf_prog_pref, self._bpf_prog_id) = self._get_bpf_prog_ids()
525        bss_map_id = self._find_bss_map_id(self._bpf_prog_id)
526
527        ipv6_addr = ipaddress.IPv6Address(self.ipv6_prefix)
528        ipv6_bytes = ipv6_addr.packed
529        ifindex_bytes = self.nk_host_ifindex.to_bytes(4, byteorder='little')
530        value = ipv6_bytes + ifindex_bytes
531        value_hex = ' '.join(f'{b:02x}' for b in value)
532        bpftool(f"map update id {bss_map_id} key hex 00 00 00 00 value hex {value_hex}")
533
534    def _attach_primary_rx_redirect_bpf(self):
535        """Attach BPF redirect program on the primary netkit ingress."""
536        bpf_obj = self.test_dir / "nk_primary_rx_redirect.bpf.o"
537        if not bpf_obj.exists():
538            raise KsftSkipEx("Primary RX redirect BPF prog not found")
539
540        if self._tc_ensure_clsact(self._nk_host_ifname):
541            self._primary_rx_redirect_clsact_added = True
542        cmd(f"tc filter add dev {self._nk_host_ifname} ingress"
543            f" bpf obj {bpf_obj} sec tc/ingress direct-action")
544        self._primary_rx_redirect_attached = True
545
546        ip(f"-6 route add {self.nk_guest_ipv6}/128 via {self.addr_v['6']}",
547           host=self.remote)
548        self._remote_route_added = True
549
550        filters = json.loads(
551            cmd(f"tc -j filter show dev {self._nk_host_ifname} ingress").stdout)
552        redirect_prog_id = None
553        for bpf in filters:
554            if 'options' not in bpf:
555                continue
556            if bpf['options']['bpf_name'].startswith('nk_primary_rx_redirect'):
557                redirect_prog_id = bpf['options']['prog']['id']
558                break
559        if redirect_prog_id is None:
560            raise Exception("Failed to get primary RX redirect BPF prog ID")
561
562        bss_map_id = self._find_bss_map_id(redirect_prog_id)
563        phys_ifindex_bytes = self.ifindex.to_bytes(4, byteorder=sys.byteorder)
564        value_hex = ' '.join(f'{b:02x}' for b in phys_ifindex_bytes)
565        bpftool(f"map update id {bss_map_id} key hex 00 00 00 00 value hex {value_hex}")
566