public inbox for pve-devel@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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal