selftests: drv-net: add primary_rx_redirect support to NetDrvContEnv

When sending from a namespace that has access to a netkit device with a
leased queue, the nk primary in the host namespace needs to redirect its
RX to the physical device. This patch adds that redirection bpf program
and teaches the harness to install it.

Add primary_rx_redirect=False parameter to NetDrvContEnv.__init__().
When enabled, _attach_primary_rx_redirect_bpf() attaches a new BPF TC
program (nk_primary_rx_redirect.bpf.c) to the primary (host-side) netkit
interface. The program redirects non-ICMPv6 IPv6 packets to the physical
NIC via bpf_redirect_neigh(), with the physical ifindex configured via
the .bss map. ICMPv6 is left on the host's netkit primary so IPv6
neighbor discovery still work locally.

Extract _find_bss_map_id() from _attach_bpf() into a reusable helper so
other BPF attachment methods can use it.

Acked-by: Stanislav Fomichev <sdf@fomichev.me>
Signed-off-by: Bobby Eshleman <bobbyeshleman@meta.com>
Link: https://patch.msgid.link/20260514-tcp-dm-netkit-v5-7-408c59b91e66@meta.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
This commit is contained in:
Bobby Eshleman 2026-05-14 10:22:34 -07:00 committed by Jakub Kicinski
parent 6cac32fc3f
commit 886a790b59
2 changed files with 115 additions and 18 deletions

View File

@ -0,0 +1,39 @@
// SPDX-License-Identifier: GPL-2.0
#include <linux/bpf.h>
#include <linux/pkt_cls.h>
#include <linux/if_ether.h>
#include <linux/in.h>
#include <linux/ipv6.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#define ctx_ptr(field) ((void *)(long)(field))
volatile __u32 phys_ifindex;
SEC("tc/ingress")
int nk_primary_rx_redirect(struct __sk_buff *skb)
{
void *data_end = ctx_ptr(skb->data_end);
void *data = ctx_ptr(skb->data);
struct ethhdr *eth;
struct ipv6hdr *ip6h;
eth = data;
if ((void *)(eth + 1) > data_end)
return TC_ACT_OK;
if (eth->h_proto != bpf_htons(ETH_P_IPV6))
return TC_ACT_OK;
ip6h = data + sizeof(struct ethhdr);
if ((void *)(ip6h + 1) > data_end)
return TC_ACT_OK;
if (ip6h->nexthdr == IPPROTO_ICMPV6)
return TC_ACT_OK;
return bpf_redirect_neigh(phys_ifindex, NULL, 0, 0);
}
char __license[] SEC("license") = "GPL";

View File

@ -2,6 +2,7 @@
import ipaddress
import os
import sys
import time
import json
from pathlib import Path
@ -336,15 +337,18 @@ class NetDrvContEnv(NetDrvEpEnv):
+---------------+
"""
def __init__(self, src_path, rxqueues=1, **kwargs):
def __init__(self, src_path, rxqueues=1, primary_rx_redirect=False, **kwargs):
self.netns = None
self._nk_host_ifname = None
self.nk_guest_ifname = None
self._tc_clsact_added = False
self._tc_attached = False
self._primary_rx_redirect_attached = False
self._primary_rx_redirect_clsact_added = False
self._bpf_prog_pref = None
self._bpf_prog_id = None
self._init_ns_attached = False
self._remote_route_added = False
self._old_fwd = None
self._old_accept_ra = None
@ -396,8 +400,18 @@ class NetDrvContEnv(NetDrvEpEnv):
self._setup_ns()
self._attach_bpf()
if primary_rx_redirect:
self._attach_primary_rx_redirect_bpf()
def __del__(self):
if self._primary_rx_redirect_attached:
cmd(f"tc filter del dev {self._nk_host_ifname} ingress", fail=False)
self._primary_rx_redirect_attached = False
if self._primary_rx_redirect_clsact_added:
cmd(f"tc qdisc del dev {self._nk_host_ifname} clsact", fail=False)
self._primary_rx_redirect_clsact_added = False
if self._tc_attached:
cmd(f"tc filter del dev {self.ifname} ingress pref {self._bpf_prog_pref}")
self._tc_attached = False
@ -406,6 +420,11 @@ class NetDrvContEnv(NetDrvEpEnv):
cmd(f"tc qdisc del dev {self.ifname} clsact")
self._tc_clsact_added = False
if self._remote_route_added:
cmd(f"ip -6 route del {self.nk_guest_ipv6}/128",
host=self.remote, fail=False)
self._remote_route_added = False
if self._nk_host_ifname:
cmd(f"ip link del dev {self._nk_host_ifname}")
self._nk_host_ifname = None
@ -459,13 +478,19 @@ class NetDrvContEnv(NetDrvEpEnv):
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)
def _tc_ensure_clsact(self, ifname=None):
"""Ensure a clsact qdisc exists on @ifname.
Returns True if this call added the qdisc, otherwise returns False.
"""
if ifname is None:
ifname = self.ifname
qdisc = json.loads(cmd(f"tc -j qdisc show dev {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
return False
cmd(f"tc qdisc add dev {ifname} clsact")
return True
def _get_bpf_prog_ids(self):
filters = json.loads(cmd(f"tc -j filter show dev {self.ifname} ingress").stdout)
@ -476,28 +501,28 @@ class NetDrvContEnv(NetDrvEpEnv):
return (bpf['pref'], bpf['options']['prog']['id'])
raise Exception("Failed to get BPF prog ID")
def _find_bss_map_id(self, prog_id):
"""Find the .bss map ID for a loaded BPF program."""
prog_info = bpftool(f"prog show id {prog_id}", json=True)
for map_id in prog_info.get("map_ids", []):
map_info = bpftool(f"map show id {map_id}", json=True)
if map_info.get("name", "").endswith("bss"):
return map_id
raise Exception(f"Failed to find .bss map for prog {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()
if self._tc_ensure_clsact():
self._tc_clsact_added = True
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")
bss_map_id = self._find_bss_map_id(self._bpf_prog_id)
ipv6_addr = ipaddress.IPv6Address(self.ipv6_prefix)
ipv6_bytes = ipv6_addr.packed
@ -505,3 +530,36 @@ class NetDrvContEnv(NetDrvEpEnv):
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}")
def _attach_primary_rx_redirect_bpf(self):
"""Attach BPF redirect program on the primary netkit ingress."""
bpf_obj = self.test_dir / "nk_primary_rx_redirect.bpf.o"
if not bpf_obj.exists():
raise KsftSkipEx("Primary RX redirect BPF prog not found")
if self._tc_ensure_clsact(self._nk_host_ifname):
self._primary_rx_redirect_clsact_added = True
cmd(f"tc filter add dev {self._nk_host_ifname} ingress"
f" bpf obj {bpf_obj} sec tc/ingress direct-action")
self._primary_rx_redirect_attached = True
ip(f"-6 route add {self.nk_guest_ipv6}/128 via {self.addr_v['6']}",
host=self.remote)
self._remote_route_added = True
filters = json.loads(
cmd(f"tc -j filter show dev {self._nk_host_ifname} ingress").stdout)
redirect_prog_id = None
for bpf in filters:
if 'options' not in bpf:
continue
if bpf['options']['bpf_name'].startswith('nk_primary_rx_redirect'):
redirect_prog_id = bpf['options']['prog']['id']
break
if redirect_prog_id is None:
raise Exception("Failed to get primary RX redirect BPF prog ID")
bss_map_id = self._find_bss_map_id(redirect_prog_id)
phys_ifindex_bytes = self.ifindex.to_bytes(4, byteorder=sys.byteorder)
value_hex = ' '.join(f'{b:02x}' for b in phys_ifindex_bytes)
bpftool(f"map update id {bss_map_id} key hex 00 00 00 00 value hex {value_hex}")