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