all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Hannes Laimer <h.laimer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH proxmox-ebpf 2/3] tests: add a native harness for the BPF C programs
Date: Wed,  2 Sep 2026 14:32:28 +0200	[thread overview]
Message-ID: <20260902123229.638967-3-h.laimer@proxmox.com> (raw)
In-Reply-To: <20260902123229.638967-1-h.laimer@proxmox.com>

The programs' parse and build logic is plain C whose behavior does
not depend on the compilation target, so build.rs compiles each one
a second time, natively, against shim headers that turn the helpers
into plain extern functions, and archives the results for the test
build. The harness provides those helpers as bounds-checked
functions over an owned buffer plus a per-thread map registry, so a
test hands a program a crafted frame and asserts on the rewritten
bytes.

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 build.rs                       |  62 +++++++++++
 src/bpf-shim/bpf/bpf_endian.h  |  12 +++
 src/bpf-shim/bpf/bpf_helpers.h |  32 ++++++
 src/bpf-shim/bpf_debug.h       |  10 ++
 src/bpf-shim/vmlinux.h         |  70 ++++++++++++
 tests/common/mod.rs            | 190 +++++++++++++++++++++++++++++++++
 6 files changed, 376 insertions(+)
 create mode 100644 src/bpf-shim/bpf/bpf_endian.h
 create mode 100644 src/bpf-shim/bpf/bpf_helpers.h
 create mode 100644 src/bpf-shim/bpf_debug.h
 create mode 100644 src/bpf-shim/vmlinux.h
 create mode 100644 tests/common/mod.rs

diff --git a/build.rs b/build.rs
index 270a97f..e62be72 100644
--- a/build.rs
+++ b/build.rs
@@ -15,6 +15,9 @@ fn main() {
     println!("cargo:rerun-if-changed=build.rs");
     println!("cargo:rerun-if-env-changed=CARGO_FEATURE_BPF_DEBUG");
 
+    let shim_include = src_dir.join("bpf-shim");
+    let mut native_objs = Vec::new();
+
     for sub_entry in std::fs::read_dir(&src_dir).expect("read src/") {
         let sub_path = sub_entry.expect("read src/ entry").path();
         let bpf_dir = sub_path.join("bpf");
@@ -43,8 +46,67 @@ fn main() {
                 continue;
             }
             compile_bpf(&path, &bpf_dir, &global_include, &out_dir, bpf_debug);
+            native_objs.push(compile_native(
+                &path,
+                &bpf_dir,
+                &global_include,
+                &shim_include,
+                &out_dir,
+            ));
         }
     }
+
+    // archived so a member is only linked when a test references its symbols, the regular
+    // build neither grows nor needs the mocked helpers
+    let archive = out_dir.join("libbpf_native.a");
+    let _ = std::fs::remove_file(&archive);
+    let status = Command::new("ar")
+        .arg("rcs")
+        .arg(&archive)
+        .args(&native_objs)
+        .status()
+        .expect("failed to invoke ar");
+    assert!(
+        status.success(),
+        "ar failed to create {}",
+        archive.display()
+    );
+    println!("cargo:rustc-link-search=native={}", out_dir.display());
+    println!("cargo:rustc-link-lib=static=bpf_native");
+}
+
+// native build against the shim headers, so tests call the program as a plain function
+fn compile_native(
+    src: &Path,
+    local_include: &Path,
+    global_include: &Path,
+    shim_include: &Path,
+    out_dir: &Path,
+) -> PathBuf {
+    let stem = src.file_stem().unwrap().to_str().unwrap();
+    let obj_path = out_dir.join(format!("{stem}.native.o"));
+
+    let status = Command::new("clang")
+        .args(["-O2", "-g", "-Wall", "-fPIC"])
+        // shim first, it shadows vmlinux.h and the bpf headers
+        .arg("-I")
+        .arg(shim_include)
+        .arg("-I")
+        .arg(global_include)
+        .arg("-I")
+        .arg(local_include)
+        .arg("-c")
+        .arg(src)
+        .arg("-o")
+        .arg(&obj_path)
+        .status()
+        .expect("failed to invoke clang -- is it installed?");
+    assert!(
+        status.success(),
+        "clang failed to compile {} natively",
+        src.display()
+    );
+    obj_path
 }
 
 fn compile_bpf(
diff --git a/src/bpf-shim/bpf/bpf_endian.h b/src/bpf-shim/bpf/bpf_endian.h
new file mode 100644
index 0000000..ebcbceb
--- /dev/null
+++ b/src/bpf-shim/bpf/bpf_endian.h
@@ -0,0 +1,12 @@
+#ifndef PROXMOX_EBPF_SHIM_BPF_ENDIAN_H
+#define PROXMOX_EBPF_SHIM_BPF_ENDIAN_H
+
+// Native stand-in for <bpf/bpf_endian.h>, little-endian hosts only (the
+// shim vmlinux.h enforces that).
+
+#define bpf_htons(x) __builtin_bswap16(x)
+#define bpf_ntohs(x) __builtin_bswap16(x)
+#define bpf_htonl(x) __builtin_bswap32(x)
+#define bpf_ntohl(x) __builtin_bswap32(x)
+
+#endif
diff --git a/src/bpf-shim/bpf/bpf_helpers.h b/src/bpf-shim/bpf/bpf_helpers.h
new file mode 100644
index 0000000..4980724
--- /dev/null
+++ b/src/bpf-shim/bpf/bpf_helpers.h
@@ -0,0 +1,32 @@
+#ifndef PROXMOX_EBPF_SHIM_BPF_HELPERS_H
+#define PROXMOX_EBPF_SHIM_BPF_HELPERS_H
+
+// Native stand-in for <bpf/bpf_helpers.h>: the helpers become plain extern
+// functions the test harness provides, sections reduce to weak linkage so
+// the program objects can be linked into one test binary, and the map
+// definition macros keep their libbpf shapes, which are plain C already.
+
+#define SEC(name) __attribute__((weak))
+#define __always_inline inline __attribute__((always_inline))
+#define __uint(name, val) int(*name)[val]
+#define __type(name, val) typeof(val) *name
+
+#ifndef barrier_var
+#define barrier_var(var) asm volatile("" : "+r"(var))
+#endif
+
+// map-definition constants, the values are irrelevant natively
+enum {
+    BPF_MAP_TYPE_HASH = 1,
+};
+#define BPF_F_NO_PREALLOC 1
+#define LIBBPF_PIN_BY_NAME 1
+
+extern void *bpf_map_lookup_elem(void *map, const void *key);
+extern long bpf_skb_load_bytes(const void *skb, __u32 offset, void *to, __u32 len);
+extern long bpf_skb_store_bytes(void *skb, __u32 offset, const void *from, __u32 len, __u64 flags);
+extern long bpf_skb_pull_data(void *skb, __u32 len);
+extern long bpf_skb_change_tail(void *skb, __u32 new_len, __u64 flags);
+extern long bpf_redirect(__u32 ifindex, __u64 flags);
+
+#endif
diff --git a/src/bpf-shim/bpf_debug.h b/src/bpf-shim/bpf_debug.h
new file mode 100644
index 0000000..a7e5eac
--- /dev/null
+++ b/src/bpf-shim/bpf_debug.h
@@ -0,0 +1,10 @@
+#ifndef PROXMOX_EBPF_SHIM_BPF_DEBUG_H
+#define PROXMOX_EBPF_SHIM_BPF_DEBUG_H
+
+// Native stand-in for bpf_debug.h, debug printing is a no-op in tests.
+
+#define DBG(...)                                                                                   \
+    do {                                                                                           \
+    } while (0)
+
+#endif
diff --git a/src/bpf-shim/vmlinux.h b/src/bpf-shim/vmlinux.h
new file mode 100644
index 0000000..52954f2
--- /dev/null
+++ b/src/bpf-shim/vmlinux.h
@@ -0,0 +1,70 @@
+#ifndef PROXMOX_EBPF_SHIM_VMLINUX_H
+#define PROXMOX_EBPF_SHIM_VMLINUX_H
+
+// Native stand-in for vmlinux.h, only the types the programs touch. The
+// header layouts match the little-endian kernel wire layouts, data and
+// data_end are wide enough to hold real pointers.
+
+#if __BYTE_ORDER__ != __ORDER_LITTLE_ENDIAN__
+#error "the native BPF test build assumes a little-endian host"
+#endif
+
+typedef unsigned char __u8;
+typedef unsigned short __u16;
+typedef unsigned int __u32;
+typedef unsigned long long __u64;
+typedef __u16 __be16;
+typedef __u32 __be32;
+
+// keep in sync with SkBuff in tests/common/mod.rs
+struct __sk_buff {
+    unsigned long data;
+    unsigned long data_end;
+    __u32 len;
+    __u32 ifindex;
+    __u32 mark;
+};
+
+struct ethhdr {
+    __u8 h_dest[6];
+    __u8 h_source[6];
+    __be16 h_proto;
+} __attribute__((packed));
+
+struct iphdr {
+    __u8 ihl : 4;
+    __u8 version : 4;
+    __u8 tos;
+    __be16 tot_len;
+    __be16 id;
+    __be16 frag_off;
+    __u8 ttl;
+    __u8 protocol;
+    __u16 check;
+    __be32 saddr;
+    __be32 daddr;
+};
+
+struct udphdr {
+    __be16 source;
+    __be16 dest;
+    __be16 len;
+    __u16 check;
+};
+
+struct in6_addr {
+    __u8 s6_addr[16];
+};
+
+struct ipv6hdr {
+    __u8 priority : 4;
+    __u8 version : 4;
+    __u8 flow_lbl[3];
+    __be16 payload_len;
+    __u8 nexthdr;
+    __u8 hop_limit;
+    struct in6_addr saddr;
+    struct in6_addr daddr;
+};
+
+#endif
diff --git a/tests/common/mod.rs b/tests/common/mod.rs
new file mode 100644
index 0000000..af8cabd
--- /dev/null
+++ b/tests/common/mod.rs
@@ -0,0 +1,190 @@
+//! Native harness for the subsystems' BPF C programs: a fake skb over an owned buffer, the
+//! helpers as plain bounds-checked functions over it, and a per-thread map registry. build.rs
+//! compiles every program a second time against the shim headers in src/bpf-shim and archives
+//! them, tests declare the program symbols and call them like plain functions.
+
+// the archive is linked through the lib, a test crate that never uses the lib
+// would drop it from the link together with the program symbols
+use proxmox_ebpf as _;
+
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::ffi::{c_int, c_long, c_void};
+
+pub const TC_ACT_OK: c_int = 0;
+pub const TC_ACT_REDIRECT: c_int = 7;
+
+pub const PKT_CAP: usize = 2048;
+
+/// Keep in sync with struct __sk_buff in src/bpf-shim/vmlinux.h.
+#[repr(C)]
+pub struct SkBuff {
+    pub data: usize,
+    pub data_end: usize,
+    pub len: u32,
+    pub ifindex: u32,
+    pub mark: u32,
+}
+
+/// The skb handed to a program plus the buffer behind it. The helpers get the skb pointer and
+/// cast back to this, so the skb must stay the first field.
+#[repr(C)]
+pub struct TestSkb {
+    skb: SkBuff,
+    buf: Box<[u8; PKT_CAP]>,
+}
+
+impl TestSkb {
+    pub fn new(packet: &[u8], ifindex: u32) -> Self {
+        assert!(packet.len() <= PKT_CAP);
+        let mut buf = Box::new([0u8; PKT_CAP]);
+        buf[..packet.len()].copy_from_slice(packet);
+        let mut t = TestSkb {
+            skb: SkBuff {
+                data: 0,
+                data_end: 0,
+                len: packet.len() as u32,
+                ifindex,
+                mark: 0,
+            },
+            buf,
+        };
+        t.sync();
+        t
+    }
+
+    fn sync(&mut self) {
+        let base = self.buf.as_ptr() as usize;
+        self.skb.data = base;
+        self.skb.data_end = base + self.skb.len as usize;
+    }
+
+    pub fn packet(&self) -> &[u8] {
+        &self.buf[..self.skb.len as usize]
+    }
+
+    pub fn run(&mut self, prog: unsafe extern "C" fn(*mut SkBuff) -> c_int) -> c_int {
+        REDIRECTED.with(|r| *r.borrow_mut() = None);
+        unsafe { prog(&mut self.skb) }
+    }
+}
+
+struct MockMap {
+    key_size: usize,
+    entries: HashMap<Vec<u8>, Box<[u8]>>,
+}
+
+thread_local! {
+    static MAPS: RefCell<HashMap<usize, MockMap>> = RefCell::new(HashMap::new());
+    static REDIRECTED: RefCell<Option<u32>> = const { RefCell::new(None) };
+}
+
+pub fn register_map(map: *const c_void, key_size: usize) {
+    MAPS.with(|maps| {
+        maps.borrow_mut().insert(
+            map as usize,
+            MockMap {
+                key_size,
+                entries: HashMap::new(),
+            },
+        )
+    });
+}
+
+pub fn map_insert(map: *const c_void, key: &[u8], value: &[u8]) {
+    MAPS.with(|maps| {
+        let mut maps = maps.borrow_mut();
+        let m = maps.get_mut(&(map as usize)).expect("map not registered");
+        assert_eq!(key.len(), m.key_size);
+        m.entries.insert(key.to_vec(), value.into());
+    });
+}
+
+/// The ifindex of the redirect the last run issued, if any.
+pub fn redirected() -> Option<u32> {
+    REDIRECTED.with(|r| *r.borrow())
+}
+
+unsafe fn testskb<'a>(skb: *mut c_void) -> &'a mut TestSkb {
+    unsafe { &mut *(skb as *mut TestSkb) }
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn bpf_map_lookup_elem(map: *mut c_void, key: *const c_void) -> *mut c_void {
+    MAPS.with(|maps| {
+        let maps = maps.borrow();
+        let Some(m) = maps.get(&(map as usize)) else {
+            panic!("lookup on unregistered map");
+        };
+        let key = unsafe { std::slice::from_raw_parts(key as *const u8, m.key_size) };
+        match m.entries.get(key) {
+            // the box's heap allocation stays put while the registry holds it
+            Some(v) => v.as_ptr() as *mut c_void,
+            None => std::ptr::null_mut(),
+        }
+    })
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn bpf_skb_load_bytes(
+    skb: *mut c_void,
+    offset: u32,
+    to: *mut c_void,
+    len: u32,
+) -> c_long {
+    let t = unsafe { testskb(skb) };
+    let (offset, len) = (offset as usize, len as usize);
+    if offset + len > t.skb.len as usize {
+        return -1;
+    }
+    unsafe { std::ptr::copy_nonoverlapping(t.buf.as_ptr().add(offset), to as *mut u8, len) };
+    0
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn bpf_skb_store_bytes(
+    skb: *mut c_void,
+    offset: u32,
+    from: *const c_void,
+    len: u32,
+    _flags: u64,
+) -> c_long {
+    let t = unsafe { testskb(skb) };
+    let (offset, len) = (offset as usize, len as usize);
+    if offset + len > t.skb.len as usize {
+        return -1;
+    }
+    unsafe {
+        std::ptr::copy_nonoverlapping(from as *const u8, t.buf.as_mut_ptr().add(offset), len)
+    };
+    0
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn bpf_skb_pull_data(skb: *mut c_void, len: u32) -> c_long {
+    // the buffer is always linear, pulling within it is a no-op
+    let t = unsafe { testskb(skb) };
+    if len > t.skb.len { -1 } else { 0 }
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn bpf_skb_change_tail(skb: *mut c_void, new_len: u32, _flags: u64) -> c_long {
+    let t = unsafe { testskb(skb) };
+    if new_len as usize > PKT_CAP {
+        return -1;
+    }
+    // like the kernel, grown room reads as zeros
+    let old = t.skb.len as usize;
+    if new_len as usize > old {
+        t.buf[old..new_len as usize].fill(0);
+    }
+    t.skb.len = new_len;
+    t.sync();
+    0
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn bpf_redirect(ifindex: u32, _flags: u64) -> c_long {
+    REDIRECTED.with(|r| *r.borrow_mut() = Some(ifindex));
+    TC_ACT_REDIRECT as c_long
+}
-- 
2.47.3





  parent reply	other threads:[~2026-09-02 12:33 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-02 12:32 [PATCH proxmox-ebpf 0/3] add proxmox-ebpf library Hannes Laimer
2026-09-02 12:32 ` [PATCH proxmox-ebpf 1/3] add the shared tc subsystem code Hannes Laimer
2026-09-02 12:32 ` Hannes Laimer [this message]
2026-09-02 12:32 ` [PATCH proxmox-ebpf 3/3] debian: package the crate as a rust library Hannes Laimer
2026-09-02 12:34 ` [PATCH proxmox-ebpf 0/3] add proxmox-ebpf library Hannes Laimer

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=20260902123229.638967-3-h.laimer@proxmox.com \
    --to=h.laimer@proxmox.com \
    --cc=pve-devel@lists.proxmox.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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal