public inbox for netdev@vger.kernel.org
 help / color / mirror / Atom feed
From: Wei Wang <weibunny@fb.com>
To: <netdev@vger.kernel.org>, Jakub Kicinski <kuba@kernel.org>,
	Daniel Zahka <daniel.zahka@gmail.com>,
	Willem de Bruijn <willemdebruijn.kernel@gmail.com>,
	David Wei <dw@davidwei.uk>, Andrew Lunn <andrew+netdev@lunn.ch>,
	"David S. Miller" <davem@davemloft.net>,
	Eric Dumazet <edumazet@google.com>
Cc: Wei Wang <weibunny@fb.com>, Daniel Borkmann <daniel@iogearbox.net>
Subject: [PATCH v2 net-next 3/9] selftests/net: Add env for container based tests
Date: Tue, 3 Mar 2026 16:00:43 -0800	[thread overview]
Message-ID: <20260304000050.3366381-4-weibunny@fb.com> (raw)
In-Reply-To: <20260304000050.3366381-1-weibunny@fb.com>

From: David Wei <dw@davidwei.uk>

Add an env NetDrvContEnv for container based selftests. This automates
the setup of a netns, netkit pair with one inside the netns, and a BPF
program that forwards skbs from the NETIF host inside the container.

Currently only netkit is used, but other virtual netdevs e.g. veth can
be used too.

Expect netkit container datapath selftests to have a publicly routable
IP prefix to assign to netkit in a container, such that packets will
land on eth0. The BPF skb forward program will then forward such packets
from the host netns to the container netns.

Signed-off-by: David Wei <dw@davidwei.uk>
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
 .../testing/selftests/drivers/net/README.rst  |  38 ++++
 .../drivers/net/hw/lib/py/__init__.py         |   7 +-
 .../selftests/drivers/net/lib/py/__init__.py  |   7 +-
 .../selftests/drivers/net/lib/py/env.py       | 208 ++++++++++++++++++
 4 files changed, 254 insertions(+), 6 deletions(-)

diff --git a/tools/testing/selftests/drivers/net/README.rst b/tools/testing/selftests/drivers/net/README.rst
index eb838ae94844..24bf4e10024f 100644
--- a/tools/testing/selftests/drivers/net/README.rst
+++ b/tools/testing/selftests/drivers/net/README.rst
@@ -62,6 +62,44 @@ LOCAL_V4, LOCAL_V6, REMOTE_V4, REMOTE_V6
 
 Local and remote endpoint IP addresses.
 
+LOCAL_PREFIX_V6
+~~~~~~~~~~~~~~~
+
+Local IP prefix/subnet which can be used to allocate extra IP addresses (for
+network name spaces behind macvlan, veth, netkit devices). DUT must be
+reachable using these addresses from the endpoint.
+
+LOCAL_PREFIX_V6 must NOT match LOCAL_V6.
+
+Example:
+  NETIF           = "eth0"
+  LOCAL_V6        = "2001:db8::1"
+  REMOTE_V6       = "2001:db8::2"
+  LOCAL_PREFIX_V6 = "fd00:cafe::"
+
+          +-----------------------------+        +------------------------------+
+  dst     | INIT NS                     |        | TEST NS                      |
+  fd00:   | +-------------+             |        |                              |
+  cafe::  | | NETIF       |             |  bpf   |                              |
+  2:2 +---|>| 2001:db8::1 |             |redirect| +-------------------------+  |
+      |   | |             |-------------|--------|>| Netkit                  |  |
+      |   | +-------------+             | _peer  | | nk_guest                |  |
+      |   | +-------------+ Netkit pair |        | | fe80::2/64              |  |
+      |   | | Netkit      |.............|........|>| fd00:cafe::2:2/64       |  |
+      |   | | nk_host     |             |        | +-------------------------+  |
+      |   | | fe80::1/64  |             |        |                              |
+      |   | +-------------+             |        | route:                       |
+      |   |                             |        |   default                    |
+      |   | route:                      |        |     via fe80::1 dev nk_guest |
+      |   |   fd00:cafe::2:2/128        |        +------------------------------+
+      |   |     via fe80::2 dev nk_host |
+      |   +-----------------------------+
+      |
+      |   +---------------+
+      |   | REMOTE        |
+      +---| 2001:db8::2   |
+          +---------------+
+
 REMOTE_TYPE
 ~~~~~~~~~~~
 
diff --git a/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py b/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
index 1971577d47e9..b8d9ae282390 100644
--- a/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
+++ b/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
@@ -3,6 +3,7 @@
 """
 Driver test environment (hardware-only tests).
 NetDrvEnv and NetDrvEpEnv are the main environment classes.
+NetDrvContEnv extends NetDrvEpEnv with netkit container support.
 Former is for local host only tests, latter creates / connects
 to a remote endpoint. See NIPA wiki for more information about
 running and writing driver tests.
@@ -30,7 +31,7 @@ try:
     from net.lib.py import ksft_eq, ksft_ge, ksft_in, ksft_is, ksft_lt, \
         ksft_ne, ksft_not_in, ksft_raises, ksft_true, ksft_gt, ksft_not_none
     from drivers.net.lib.py import GenerateTraffic, Remote, Iperf3Runner
-    from drivers.net.lib.py import NetDrvEnv, NetDrvEpEnv
+    from drivers.net.lib.py import NetDrvEnv, NetDrvEpEnv, NetDrvContEnv
 
     __all__ = ["NetNS", "NetNSEnter", "NetdevSimDev",
                "EthtoolFamily", "NetdevFamily", "NetshaperFamily",
@@ -45,8 +46,8 @@ try:
                "ksft_eq", "ksft_ge", "ksft_in", "ksft_is", "ksft_lt",
                "ksft_ne", "ksft_not_in", "ksft_raises", "ksft_true", "ksft_gt",
                "ksft_not_none", "ksft_not_none",
-               "NetDrvEnv", "NetDrvEpEnv", "GenerateTraffic", "Remote",
-               "Iperf3Runner"]
+               "NetDrvEnv", "NetDrvEpEnv", "NetDrvContEnv", "GenerateTraffic",
+               "Remote", "Iperf3Runner"]
 except ModuleNotFoundError as e:
     print("Failed importing `net` library from kernel sources")
     print(str(e))
diff --git a/tools/testing/selftests/drivers/net/lib/py/__init__.py b/tools/testing/selftests/drivers/net/lib/py/__init__.py
index e5ef5e949c12..374d4f08dd05 100644
--- a/tools/testing/selftests/drivers/net/lib/py/__init__.py
+++ b/tools/testing/selftests/drivers/net/lib/py/__init__.py
@@ -3,6 +3,7 @@
 """
 Driver test environment.
 NetDrvEnv and NetDrvEpEnv are the main environment classes.
+NetDrvContEnv extends NetDrvEpEnv with netkit container support.
 Former is for local host only tests, latter creates / connects
 to a remote endpoint. See NIPA wiki for more information about
 running and writing driver tests.
@@ -43,12 +44,12 @@ try:
                "ksft_ne", "ksft_not_in", "ksft_raises", "ksft_true", "ksft_gt",
                "ksft_not_none", "ksft_not_none"]
 
-    from .env import NetDrvEnv, NetDrvEpEnv
+    from .env import NetDrvEnv, NetDrvEpEnv, NetDrvContEnv
     from .load import GenerateTraffic, Iperf3Runner
     from .remote import Remote
 
-    __all__ += ["NetDrvEnv", "NetDrvEpEnv", "GenerateTraffic", "Remote",
-                "Iperf3Runner"]
+    __all__ += ["NetDrvEnv", "NetDrvEpEnv", "NetDrvContEnv", "GenerateTraffic",
+                "Remote", "Iperf3Runner"]
 except ModuleNotFoundError as e:
     print("Failed importing `net` library from kernel sources")
     print(str(e))
diff --git a/tools/testing/selftests/drivers/net/lib/py/env.py b/tools/testing/selftests/drivers/net/lib/py/env.py
index 41cc248ac848..36d98e1df15b 100644
--- a/tools/testing/selftests/drivers/net/lib/py/env.py
+++ b/tools/testing/selftests/drivers/net/lib/py/env.py
@@ -1,13 +1,16 @@
 # SPDX-License-Identifier: GPL-2.0
 
+import ipaddress
 import os
 import time
+import json
 from pathlib import Path
 from lib.py import KsftSkipEx, KsftXfailEx
 from lib.py import ksft_setup, wait_file
 from lib.py import cmd, ethtool, ip, CmdExitFailure
 from lib.py import NetNS, NetdevSimDev
 from .remote import Remote
+from . import bpftool, RtnlFamily, Netlink
 
 
 class NetDrvEnvBase:
@@ -289,3 +292,208 @@ class NetDrvEpEnv(NetDrvEnvBase):
                 data.get('stats-block-usecs', 0) / 1000 / 1000
 
         time.sleep(self._stats_settle_time)
+
+
+class NetDrvContEnv(NetDrvEpEnv):
+    """
+    Class for an environment with a netkit pair setup for forwarding traffic
+    between the physical interface and a network namespace.
+      NETIF           = "eth0"
+      LOCAL_V6        = "2001:db8::1"
+      REMOTE_V6       = "2001:db8::2"
+      LOCAL_PREFIX_V6 = "fd00:cafe::"
+
+              +-----------------------------+        +------------------------------+
+      dst     | INIT NS                     |        | TEST NS                      |
+      fd00:   | +-------------+             |        |                              |
+      cafe::  | | NETIF       |             |  bpf   |                              |
+      2:2 +---|>| 2001:db8::1 |             |redirect| +-------------------------+  |
+          |   | |             |-------------|--------|>| Netkit                  |  |
+          |   | +-------------+             | _peer  | | nk_guest                |  |
+          |   | +-------------+ Netkit pair |        | | fe80::2/64              |  |
+          |   | | Netkit      |.............|........|>| fd00:cafe::2:2/64       |  |
+          |   | | nk_host     |             |        | +-------------------------+  |
+          |   | | fe80::1/64  |             |        |                              |
+          |   | +-------------+             |        | route:                       |
+          |   |                             |        |   default                    |
+          |   | route:                      |        |     via fe80::1 dev nk_guest |
+          |   |   fd00:cafe::2:2/128        |        +------------------------------+
+          |   |     via fe80::2 dev nk_host |
+          |   +-----------------------------+
+          |
+          |   +---------------+
+          |   | REMOTE        |
+          +---| 2001:db8::2   |
+              +---------------+
+    """
+
+    def __init__(self, src_path, rxqueues=1, **kwargs):
+        self.netns = None
+        self._nk_host_ifname = None
+        self._nk_guest_ifname = None
+        self._tc_clsact_added = False
+        self._tc_attached = False
+        self._bpf_prog_pref = None
+        self._bpf_prog_id = None
+        self._init_ns_attached = False
+        self._old_fwd = None
+        self._old_accept_ra = None
+
+        super().__init__(src_path, **kwargs)
+
+        self.require_ipver("6")
+        local_prefix = self.env.get("LOCAL_PREFIX_V6")
+        if not local_prefix:
+            raise KsftSkipEx("LOCAL_PREFIX_V6 required")
+
+        try:
+            net = ipaddress.IPv6Network(local_prefix, strict=False)
+        except ValueError:
+            net = ipaddress.IPv6Network(f"{local_prefix}::/64", strict=False)
+        self.ipv6_prefix = str(net.network_address)
+        self.nk_host_ipv6 = f"{self.ipv6_prefix}2:1"
+        self.nk_guest_ipv6 = f"{self.ipv6_prefix}2:2"
+
+        local_v6 = ipaddress.IPv6Address(self.addr_v["6"])
+        if local_v6 in net:
+            raise KsftSkipEx("LOCAL_V6 must not fall within LOCAL_PREFIX_V6")
+
+        rtnl = RtnlFamily()
+        rtnl.newlink(
+            {
+                "linkinfo": {
+                    "kind": "netkit",
+                    "data": {
+                        "mode": "l2",
+                        "policy": "forward",
+                        "peer-policy": "forward",
+                    },
+                },
+                "num-rx-queues": rxqueues,
+            },
+            flags=[Netlink.NLM_F_CREATE, Netlink.NLM_F_EXCL],
+        )
+
+        all_links = ip("-d link show", json=True)
+        netkit_links = [link for link in all_links
+                        if link.get('linkinfo', {}).get('info_kind') == 'netkit'
+                        and 'UP' not in link.get('flags', [])]
+
+        if len(netkit_links) != 2:
+            raise KsftSkipEx("Failed to create netkit pair")
+
+        netkit_links.sort(key=lambda x: x['ifindex'])
+        self._nk_host_ifname = netkit_links[1]['ifname']
+        self._nk_guest_ifname = netkit_links[0]['ifname']
+        self.nk_host_ifindex = netkit_links[1]['ifindex']
+        self.nk_guest_ifindex = netkit_links[0]['ifindex']
+
+        self._setup_ns()
+        self._attach_bpf()
+
+    def __del__(self):
+        if self._tc_attached:
+            cmd(f"tc filter del dev {self.ifname} ingress pref {self._bpf_prog_pref}")
+            self._tc_attached = False
+
+        if self._tc_clsact_added:
+            cmd(f"tc qdisc del dev {self.ifname} clsact")
+            self._tc_clsact_added = False
+
+        if self._nk_host_ifname:
+            cmd(f"ip link del dev {self._nk_host_ifname}")
+            self._nk_host_ifname = None
+            self._nk_guest_ifname = None
+
+        if self._init_ns_attached:
+            cmd("ip netns del init", fail=False)
+            self._init_ns_attached = False
+
+        if self.netns:
+            del self.netns
+            self.netns = None
+
+        if self._old_fwd is not None:
+            with open("/proc/sys/net/ipv6/conf/all/forwarding", "w") as f:
+                f.write(self._old_fwd)
+            self._old_fwd = None
+        if self._old_accept_ra is not None:
+            with open("/proc/sys/net/ipv6/conf/all/accept_ra", "w") as f:
+                f.write(self._old_accept_ra)
+            self._old_accept_ra = None
+
+        super().__del__()
+
+    def _setup_ns(self):
+        fwd_path = "/proc/sys/net/ipv6/conf/all/forwarding"
+        ra_path = "/proc/sys/net/ipv6/conf/all/accept_ra"
+        with open(fwd_path) as f:
+            self._old_fwd = f.read().strip()
+        with open(ra_path) as f:
+            self._old_accept_ra = f.read().strip()
+        with open(fwd_path, "w") as f:
+            f.write("1")
+        with open(ra_path, "w") as f:
+            f.write("2")
+
+        self.netns = NetNS()
+        cmd("ip netns attach init 1")
+        self._init_ns_attached = True
+        ip("netns set init 0", ns=self.netns)
+        ip(f"link set dev {self._nk_guest_ifname} netns {self.netns.name}")
+        ip(f"link set dev {self._nk_host_ifname} up")
+        ip(f"-6 addr add fe80::1/64 dev {self._nk_host_ifname} nodad")
+        ip(f"-6 route add {self.nk_guest_ipv6}/128 via fe80::2 dev {self._nk_host_ifname}")
+
+        ip("link set lo up", ns=self.netns)
+        ip(f"link set dev {self._nk_guest_ifname} up", ns=self.netns)
+        ip(f"-6 addr add fe80::2/64 dev {self._nk_guest_ifname}", ns=self.netns)
+        ip(f"-6 addr add {self.nk_guest_ipv6}/64 dev {self._nk_guest_ifname} nodad", ns=self.netns)
+        ip(f"-6 route add default via fe80::1 dev {self._nk_guest_ifname}", ns=self.netns)
+
+    def _tc_ensure_clsact(self):
+        qdisc = json.loads(cmd(f"tc -j qdisc show dev {self.ifname}").stdout)
+        for q in qdisc:
+            if q['kind'] == 'clsact':
+                return
+        cmd(f"tc qdisc add dev {self.ifname} clsact")
+        self._tc_clsact_added = True
+
+    def _get_bpf_prog_ids(self):
+        filter = json.loads(cmd(f"tc -j filter show dev {self.ifname} ingress").stdout)
+        for bpf in filter:
+            if 'options' not in bpf:
+                continue
+            if bpf['options']['bpf_name'].startswith('nk_forward.bpf'):
+                return (bpf['pref'], bpf['options']['prog']['id'])
+        if self._bpf_prog_pref is None:
+            raise Exception("Failed to get BPF prog ID")
+
+    def _attach_bpf(self):
+        bpf_obj = self.test_dir / "nk_forward.bpf.o"
+        if not bpf_obj.exists():
+            raise KsftSkipEx("BPF prog not found")
+
+        self._tc_ensure_clsact()
+        cmd(f"tc filter add dev {self.ifname} ingress bpf obj {bpf_obj} sec tc/ingress direct-action")
+        self._tc_attached = True
+
+        (self._bpf_prog_pref, self._bpf_prog_id) = self._get_bpf_prog_ids()
+        prog_info = bpftool(f"prog show id {self._bpf_prog_id}", json=True)
+        map_ids = prog_info.get("map_ids", [])
+
+        bss_map_id = None
+        for map_id in map_ids:
+            map_info = bpftool(f"map show id {map_id}", json=True)
+            if map_info.get("name").endswith("bss"):
+                bss_map_id = map_id
+
+        if bss_map_id is None:
+            raise Exception("Failed to find .bss map")
+
+        ipv6_addr = ipaddress.IPv6Address(self.ipv6_prefix)
+        ipv6_bytes = ipv6_addr.packed
+        ifindex_bytes = self.nk_host_ifindex.to_bytes(4, byteorder='little')
+        value = ipv6_bytes + ifindex_bytes
+        value_hex = ' '.join(f'{b:02x}' for b in value)
+        bpftool(f"map update id {bss_map_id} key hex 00 00 00 00 value hex {value_hex}")
-- 
2.47.3


  parent reply	other threads:[~2026-03-04  0:14 UTC|newest]

Thread overview: 21+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-03-04  0:00 [PATCH v2 net-next 0/9] psp: Add support for dev-assoc/disassoc Wei Wang
2026-03-04  0:00 ` [PATCH v2 net-next 2/9] selftests/net: Export Netlink class via lib.py Wei Wang
2026-03-04  0:00 ` Wei Wang [this message]
2026-03-04  0:00 ` [PATCH v2 net-next 4/9] selftests/net: Add netkit container ping test Wei Wang
2026-03-04  0:00 ` [PATCH v2 net-next 5/9] psp: add unprivileged version of psp_device_get_locked Wei Wang
2026-03-04 15:00   ` Daniel Zahka
2026-03-04 16:25   ` Willem de Bruijn
2026-03-04 17:42     ` Wei Wang
2026-03-04 18:01       ` Daniel Zahka
2026-03-04 18:03         ` Daniel Zahka
2026-03-04 22:31           ` Wei Wang
2026-03-04 23:41             ` Willem de Bruijn
2026-03-06 21:34               ` Jakub Kicinski
2026-03-04  0:00 ` [PATCH v2 net-next 6/9] psp: Add new netlink cmd for dev-assoc and dev-disassoc Wei Wang
2026-03-04 15:20   ` Daniel Zahka
2026-03-04 16:17   ` Daniel Zahka
2026-03-04 17:28     ` Wei Wang
2026-03-04  0:00 ` [PATCH v2 net-next 7/9] psp: add a new netdev event for dev unregister Wei Wang
2026-03-04  0:00 ` [PATCH v2 net-next 8/9] selftests/net: Add bpf skb forwarding program Wei Wang
2026-03-04  0:00 ` [PATCH v2 net-next 9/9] selftest/net: psp: Add test for dev-assoc/disassoc Wei Wang
2026-03-06 21:53   ` Jakub Kicinski

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260304000050.3366381-4-weibunny@fb.com \
    --to=weibunny@fb.com \
    --cc=andrew+netdev@lunn.ch \
    --cc=daniel.zahka@gmail.com \
    --cc=daniel@iogearbox.net \
    --cc=davem@davemloft.net \
    --cc=dw@davidwei.uk \
    --cc=edumazet@google.com \
    --cc=kuba@kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=willemdebruijn.kernel@gmail.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox