xref: /linux/tools/testing/selftests/drivers/net/lib/py/env.py (revision f1529936c0b65fb343f62f50e5313078719fc336)
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
243    def resolve_remote_ifc(self):
244        v4 = v6 = None
245        if self.remote_addr_v["4"]:
246            v4 = ip("addr show to " + self.remote_addr_v["4"], json=True, host=self.remote)
247        if self.remote_addr_v["6"]:
248            v6 = ip("addr show to " + self.remote_addr_v["6"], json=True, host=self.remote)
249        if v4 and v6 and v4[0]["ifname"] != v6[0]["ifname"]:
250            raise Exception("Can't resolve remote interface name, v4 and v6 don't match")
251        if (v4 and len(v4) > 1) or (v6 and len(v6) > 1):
252            raise Exception("Can't resolve remote interface name, multiple interfaces match")
253        return v6[0]["ifname"] if v6 else v4[0]["ifname"]
254
255    def __del__(self):
256        if self._ns:
257            self._ns.remove()
258            self._ns = None
259        if self._ns_peer:
260            self._ns_peer.remove()
261            self._ns_peer = None
262        if self._netns:
263            del self._netns
264            self._netns = None
265        if self.remote:
266            del self.remote
267            self.remote = None
268
269    def require_ipver(self, ipver):
270        if not self.addr_v[ipver] or not self.remote_addr_v[ipver]:
271            raise KsftSkipEx(f"Test requires IPv{ipver} connectivity")
272
273    def set_ipver(self, ipver):
274        """
275        Modify the IP version used by the generic address fields.
276        """
277        if ipver == getattr(self, "addr_ipver", None):
278            return
279
280        self.require_ipver(ipver)
281
282        self.addr_ipver = ipver
283        self.addr = self.addr_v[ipver]
284        self.remote_addr = self.remote_addr_v[ipver]
285
286        # Bracketed addresses, some commands need IPv6 to be inside []
287        self.baddr = (f"[{self.addr_v['6']}]" if ipver == "6"
288                      else self.addr_v["4"])
289        self.remote_baddr = (f"[{self.remote_addr_v['6']}]" if ipver == "6"
290                             else self.remote_addr_v["4"])
291
292    def require_nsim(self, nsim_test=True):
293        """Require or exclude netdevsim for this test"""
294        if nsim_test and self._ns is None:
295            raise KsftXfailEx("Test only works on netdevsim")
296        if nsim_test is False and self._ns is not None:
297            raise KsftXfailEx("Test does not work on netdevsim")
298
299    def get_local_nsim_dev(self):
300        """Returns the local netdevsim device or None.
301           Using this method is discouraged, as it makes tests nsim-specific.
302           Standard interfaces available on all HW should ideally be used.
303           This method is intended for the few cases where nsim-specific
304           assertions need to be verified which cannot be verified otherwise.
305        """
306        return self._ns
307
308    def _require_cmd(self, comm, key, host=None):
309        cached = self._required_cmd.get(comm, {})
310        if cached.get(key) is None:
311            cached[key] = cmd("command -v -- " + comm, fail=False,
312                              shell=True, host=host).ret == 0
313        self._required_cmd[comm] = cached
314        return cached[key]
315
316    def require_cmd(self, comm, local=True, remote=False):
317        if local:
318            if not self._require_cmd(comm, "local"):
319                raise KsftSkipEx("Test requires command: " + comm)
320        if remote:
321            if not self._require_cmd(comm, "remote", host=self.remote):
322                raise KsftSkipEx("Test requires (remote) command: " + comm)
323
324    def wait_hw_stats_settle(self):
325        """
326        Wait for HW stats to become consistent, some devices DMA HW stats
327        periodically so events won't be reflected until next sync.
328        Good drivers will tell us via ethtool what their sync period is.
329        """
330        if self._stats_settle_time is None:
331            data = {}
332            try:
333                data = ethtool("-c " + self.ifname, json=True)[0]
334            except CmdExitFailure as e:
335                if "Operation not supported" not in e.cmd.stderr:
336                    raise
337
338            self._stats_settle_time = \
339                1.25 * data.get('stats-block-usecs', 20000) / 1000 / 1000
340
341        time.sleep(self._stats_settle_time)
342
343
344class NetDrvContEnv(NetDrvEpEnv):
345    """
346    Class for an environment with a netkit pair setup for forwarding traffic
347    between the physical interface and a network namespace.
348      NETIF           = "eth0"
349      LOCAL_V6        = "2001:db8:1::1"
350      REMOTE_V6       = "2001:db8:1::2"
351      LOCAL_PREFIX_V6 = "2001:db8:2::0/64"
352
353              +-----------------------------+        +------------------------------+
354      dst     | INIT NS                     |        | TEST NS                      |
355      2001:   | +---------------+           |        |                              |
356      db8:2::2| | NETIF         |           |  bpf   |                              |
357          +---|>| 2001:db8:1::1 |           |redirect| +-------------------------+  |
358          |   | |               |-----------|--------|>| Netkit                  |  |
359          |   | +---------------+           | _peer  | | nk_guest                |  |
360          |   | +-------------+ Netkit pair |        | | fe80::2/64              |  |
361          |   | | Netkit      |.............|........|>| 2001:db8:2::2/64        |  |
362          |   | | nk_host     |             |        | +-------------------------+  |
363          |   | | fe80::1/64  |             |        |                              |
364          |   | +-------------+             |        | route:                       |
365          |   |                             |        |   default                    |
366          |   | route:                      |        |     via fe80::1 dev nk_guest |
367          |   |   2001:db8:2::2/128         |        +------------------------------+
368          |   |     via fe80::2 dev nk_host |
369          |   +-----------------------------+
370          |
371          |   +---------------+
372          |   | REMOTE        |
373          +---| 2001:db8:1::2 |
374              +---------------+
375    """
376
377    def __init__(self, src_path, rxqueues=1, primary_rx_redirect=False,
378                 userns=False, **kwargs):
379        self.netns = None
380        self._userns = userns
381        self.nk_host_ifname = None
382        self.nk_guest_ifname = None
383        self._tc_clsact_added = False
384        self._tc_attached = False
385        self._primary_rx_redirect_attached = False
386        self._primary_rx_redirect_clsact_added = False
387        self._bpf_prog_pref = None
388        self._bpf_prog_id = None
389        self._init_ns_attached = False
390        self._remote_route_added = False
391        self._old_fwd = None
392        self._old_accept_ra = None
393
394        super().__init__(src_path, **kwargs)
395
396        self.require_ipver("6")
397        local_prefix = self.env.get("LOCAL_PREFIX_V6")
398        if not local_prefix:
399            raise KsftSkipEx("LOCAL_PREFIX_V6 required")
400
401        net = ipaddress.IPv6Network(local_prefix, strict=False)
402        self.ipv6_prefix = str(net.network_address)
403        self.nk_host_ipv6 = f"{self.ipv6_prefix}2:1"
404        self.nk_guest_ipv6 = f"{self.ipv6_prefix}2:2"
405
406        local_v6 = ipaddress.IPv6Address(self.addr_v["6"])
407        if local_v6 in net:
408            raise KsftSkipEx("LOCAL_V6 must not fall within LOCAL_PREFIX_V6")
409
410        rtnl = RtnlFamily()
411        rtnl.newlink(
412            {
413                "linkinfo": {
414                    "kind": "netkit",
415                    "data": {
416                        "mode": "l2",
417                        "policy": "forward",
418                        "peer-policy": "forward",
419                    },
420                },
421                "num-rx-queues": rxqueues,
422            },
423            flags=[Netlink.NLM_F_CREATE, Netlink.NLM_F_EXCL],
424        )
425
426        all_links = ip("-d link show", json=True)
427        netkit_links = [link for link in all_links
428                        if link.get('linkinfo', {}).get('info_kind') == 'netkit'
429                        and 'UP' not in link.get('flags', [])]
430
431        if len(netkit_links) != 2:
432            raise KsftSkipEx("Failed to create netkit pair")
433
434        netkit_links.sort(key=lambda x: x['ifindex'])
435        self.nk_host_ifname = netkit_links[1]['ifname']
436        self.nk_guest_ifname = netkit_links[0]['ifname']
437        self.nk_host_ifindex = netkit_links[1]['ifindex']
438        self.nk_guest_ifindex = netkit_links[0]['ifindex']
439
440        self._setup_ns()
441        self.attach_bpf()
442        if primary_rx_redirect:
443            self._attach_primary_rx_redirect_bpf()
444
445    def __del__(self):
446        if self._primary_rx_redirect_attached:
447            cmd(f"tc filter del dev {self.nk_host_ifname} ingress", fail=False)
448            self._primary_rx_redirect_attached = False
449
450        if self._primary_rx_redirect_clsact_added:
451            cmd(f"tc qdisc del dev {self.nk_host_ifname} clsact", fail=False)
452            self._primary_rx_redirect_clsact_added = False
453
454        if self._tc_attached:
455            cmd(f"tc filter del dev {self.ifname} ingress pref {self._bpf_prog_pref}")
456            self._tc_attached = False
457
458        if self._tc_clsact_added:
459            cmd(f"tc qdisc del dev {self.ifname} clsact")
460            self._tc_clsact_added = False
461
462        if self._remote_route_added:
463            cmd(f"ip -6 route del {self.nk_guest_ipv6}/128",
464                host=self.remote, fail=False)
465            self._remote_route_added = False
466
467        if self.nk_host_ifname:
468            cmd(f"ip link del dev {self.nk_host_ifname}")
469            self.nk_host_ifname = None
470            self.nk_guest_ifname = None
471
472        if self._init_ns_attached:
473            cmd("ip netns del init", fail=False)
474            self._init_ns_attached = False
475
476        if self.netns:
477            del self.netns
478            self.netns = None
479
480        if self._old_fwd is not None:
481            with open("/proc/sys/net/ipv6/conf/all/forwarding", "w",
482                      encoding="utf-8") as f:
483                f.write(self._old_fwd)
484            self._old_fwd = None
485        if self._old_accept_ra is not None:
486            with open("/proc/sys/net/ipv6/conf/all/accept_ra", "w",
487                      encoding="utf-8") as f:
488                f.write(self._old_accept_ra)
489            self._old_accept_ra = None
490
491        super().__del__()
492
493    def _setup_ns(self):
494        fwd_path = "/proc/sys/net/ipv6/conf/all/forwarding"
495        ra_path = "/proc/sys/net/ipv6/conf/all/accept_ra"
496        with open(fwd_path, encoding="utf-8") as f:
497            self._old_fwd = f.read().strip()
498        with open(ra_path, encoding="utf-8") as f:
499            self._old_accept_ra = f.read().strip()
500        with open(fwd_path, "w", encoding="utf-8") as f:
501            f.write("1")
502        with open(ra_path, "w", encoding="utf-8") as f:
503            f.write("2")
504
505        self.netns = UserNetNS() if self._userns else NetNS()
506        cmd("ip netns attach init 1")
507        self._init_ns_attached = True
508        ip("netns set init 0", ns=self.netns)
509        ip(f"link set dev {self.nk_guest_ifname} netns {self.netns.name}")
510        nk_guest_dev = ip(f"link show dev {self.nk_guest_ifname}",
511                          json=True, ns=self.netns)[0]
512        self.nk_guest_ifindex = nk_guest_dev['ifindex']
513        ip(f"link set dev {self.nk_host_ifname} up")
514        ip(f"-6 addr add fe80::1/64 dev {self.nk_host_ifname} nodad")
515        ip(f"-6 route add {self.nk_guest_ipv6}/128 via fe80::2 dev {self.nk_host_ifname}")
516
517        ip("link set lo up", ns=self.netns)
518        ip(f"link set dev {self.nk_guest_ifname} up", ns=self.netns)
519        ip(f"-6 addr add fe80::2/64 dev {self.nk_guest_ifname}", ns=self.netns)
520        ip(f"-6 addr add {self.nk_guest_ipv6}/64 dev {self.nk_guest_ifname} nodad", ns=self.netns)
521        ip(f"-6 route add default via fe80::1 dev {self.nk_guest_ifname}", ns=self.netns)
522
523    def _tc_ensure_clsact(self, ifname=None):
524        """Ensure a clsact qdisc exists on @ifname.
525
526        Returns True if this call added the qdisc, otherwise returns False.
527        """
528        if ifname is None:
529            ifname = self.ifname
530        qdisc = json.loads(cmd(f"tc -j qdisc show dev {ifname}").stdout)
531        for q in qdisc:
532            if q['kind'] == 'clsact':
533                return False
534        cmd(f"tc qdisc add dev {ifname} clsact")
535        return True
536
537    def _get_bpf_prog_ids(self):
538        filters = json.loads(cmd(f"tc -j filter show dev {self.ifname} ingress").stdout)
539        for bpf in filters:
540            if 'options' not in bpf:
541                continue
542            if bpf['options']['bpf_name'].startswith('nk_forward.bpf'):
543                return (bpf['pref'], bpf['options']['prog']['id'])
544        raise Exception("Failed to get BPF prog ID")
545
546    def _find_bss_map_id(self, prog_id):
547        """Find the .bss map ID for a loaded BPF program."""
548        prog_info = bpftool(f"prog show id {prog_id}", json=True)
549        for map_id in prog_info.get("map_ids", []):
550            map_info = bpftool(f"map show id {map_id}", json=True)
551            if map_info.get("name", "").endswith("bss"):
552                return map_id
553        raise Exception(f"Failed to find .bss map for prog {prog_id}")
554
555    def _find_bpf_obj(self, name):
556        bpf_obj = self.test_dir / name
557        if bpf_obj.exists():
558            return bpf_obj
559        bpf_obj = self.test_dir / "hw" / name
560        if bpf_obj.exists():
561            return bpf_obj
562        return None
563
564    def detach_bpf(self):
565        if self._tc_attached:
566            cmd(f"tc filter del dev {self.ifname} ingress pref "
567                f"{self._bpf_prog_pref}", fail=False)
568            self._tc_attached = False
569
570    def attach_bpf(self):
571        bpf_obj = self._find_bpf_obj("nk_forward.bpf.o")
572        if not bpf_obj:
573            raise KsftSkipEx("BPF prog nk_forward.bpf.o not found")
574
575        if self._tc_ensure_clsact():
576            self._tc_clsact_added = True
577        cmd(f"tc filter add dev {self.ifname} ingress bpf obj {bpf_obj}"
578            " sec tc/ingress direct-action")
579        self._tc_attached = True
580
581        (self._bpf_prog_pref, self._bpf_prog_id) = self._get_bpf_prog_ids()
582        bss_map_id = self._find_bss_map_id(self._bpf_prog_id)
583
584        ipv6_addr = ipaddress.IPv6Address(self.ipv6_prefix)
585        ipv6_bytes = ipv6_addr.packed
586        ifindex_bytes = self.nk_host_ifindex.to_bytes(4, byteorder='little')
587        value = ipv6_bytes + ifindex_bytes
588        value_hex = ' '.join(f'{b:02x}' for b in value)
589        bpftool(f"map update id {bss_map_id} key hex 00 00 00 00 value hex {value_hex}")
590
591    def _attach_primary_rx_redirect_bpf(self):
592        """Attach BPF redirect program on the primary netkit ingress."""
593        bpf_obj = self._find_bpf_obj("nk_primary_rx_redirect.bpf.o")
594        if not bpf_obj:
595            raise KsftSkipEx("nk_primary_rx_redirect.bpf.o not found")
596
597        if self._tc_ensure_clsact(self.nk_host_ifname):
598            self._primary_rx_redirect_clsact_added = True
599        cmd(f"tc filter add dev {self.nk_host_ifname} ingress"
600            f" bpf obj {bpf_obj} sec tc/ingress direct-action")
601        self._primary_rx_redirect_attached = True
602
603        ip(f"-6 route add {self.nk_guest_ipv6}/128 via {self.addr_v['6']}",
604           host=self.remote)
605        self._remote_route_added = True
606
607        filters = json.loads(
608            cmd(f"tc -j filter show dev {self.nk_host_ifname} ingress").stdout)
609        redirect_prog_id = None
610        for bpf in filters:
611            if 'options' not in bpf:
612                continue
613            if bpf['options']['bpf_name'].startswith('nk_primary_rx_redirect'):
614                redirect_prog_id = bpf['options']['prog']['id']
615                break
616        if redirect_prog_id is None:
617            raise Exception("Failed to get primary RX redirect BPF prog ID")
618
619        bss_map_id = self._find_bss_map_id(redirect_prog_id)
620        phys_ifindex_bytes = self.ifindex.to_bytes(4, byteorder=sys.byteorder)
621        value_hex = ' '.join(f'{b:02x}' for b in phys_ifindex_bytes)
622        bpftool(f"map update id {bss_map_id} key hex 00 00 00 00 value hex {value_hex}")
623