From: Hannes Laimer <h.laimer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH proxmox v6 5/6] ebpf: tests: add a native harness for the BPF C programs
Date: Wed, 4 Nov 2026 14:49:04 +0100 [thread overview]
Message-ID: <20261104134905.1623451-6-h.laimer@proxmox.com> (raw)
In-Reply-To: <20261104134905.1623451-1-h.laimer@proxmox.com>
The programs' packet parsing and rewriting is plain C whose behavior
does not depend on the compilation target. So the build compiles each
one a second time natively, against shim headers that turn the
helpers into plain extern functions. A test hands a program a crafted
frame and asserts on the rewritten bytes.
Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
proxmox-ebpf/build.rs | 85 ++++++++
proxmox-ebpf/src/bpf-shim/bpf/bpf_endian.h | 15 ++
proxmox-ebpf/src/bpf-shim/bpf/bpf_helpers.h | 32 +++
proxmox-ebpf/src/bpf-shim/bpf_debug.h | 10 +
proxmox-ebpf/src/bpf-shim/linux/bpf.h | 25 +++
proxmox-ebpf/tests/bpf/fixture.bpf.c | 37 ++++
proxmox-ebpf/tests/common/mod.rs | 211 ++++++++++++++++++++
proxmox-ebpf/tests/harness.rs | 84 ++++++++
8 files changed, 499 insertions(+)
create mode 100644 proxmox-ebpf/src/bpf-shim/bpf/bpf_endian.h
create mode 100644 proxmox-ebpf/src/bpf-shim/bpf/bpf_helpers.h
create mode 100644 proxmox-ebpf/src/bpf-shim/bpf_debug.h
create mode 100644 proxmox-ebpf/src/bpf-shim/linux/bpf.h
create mode 100644 proxmox-ebpf/tests/bpf/fixture.bpf.c
create mode 100644 proxmox-ebpf/tests/common/mod.rs
create mode 100644 proxmox-ebpf/tests/harness.rs
diff --git a/proxmox-ebpf/build.rs b/proxmox-ebpf/build.rs
index 9cb143d955a8..33032c61013e 100644
--- a/proxmox-ebpf/build.rs
+++ b/proxmox-ebpf/build.rs
@@ -18,6 +18,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");
@@ -46,8 +49,90 @@ 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,
+ ));
}
}
+
+ // the harness's own program, compiled both ways like a subsystem's, so every build runs the
+ // BPF compile and the shim through clang and the harness tests run a program without a
+ // subsystem
+ let fixture_dir = manifest_dir.join("tests").join("bpf");
+ println!("cargo:rerun-if-changed={}", fixture_dir.display());
+ for entry in std::fs::read_dir(&fixture_dir).expect("read tests/bpf") {
+ let path = entry.expect("read tests/bpf entry").path();
+ let is_program = path
+ .file_name()
+ .and_then(|s| s.to_str())
+ .is_some_and(|s| s.ends_with(".bpf.c"));
+ if is_program {
+ compile_bpf(&path, &fixture_dir, &global_include, &out_dir, bpf_debug);
+ native_objs.push(compile_native(
+ &path,
+ &fixture_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()
+ .unwrap_or_else(|e| panic!("failed to invoke {CLANG}: {e}"));
+ assert!(
+ status.success(),
+ "clang failed to compile {} natively",
+ src.display()
+ );
+ obj_path
}
fn multiarch() -> String {
diff --git a/proxmox-ebpf/src/bpf-shim/bpf/bpf_endian.h b/proxmox-ebpf/src/bpf-shim/bpf/bpf_endian.h
new file mode 100644
index 000000000000..6d7196f9e67e
--- /dev/null
+++ b/proxmox-ebpf/src/bpf-shim/bpf/bpf_endian.h
@@ -0,0 +1,15 @@
+#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.
+
+#if __BYTE_ORDER__ != __ORDER_LITTLE_ENDIAN__
+#error "the native BPF test build assumes a little-endian host"
+#endif
+
+#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/proxmox-ebpf/src/bpf-shim/bpf/bpf_helpers.h b/proxmox-ebpf/src/bpf-shim/bpf/bpf_helpers.h
new file mode 100644
index 000000000000..d4faadefdb9c
--- /dev/null
+++ b/proxmox-ebpf/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. Weak symbols of one
+// name link silently, so program and map names must stay distinct across
+// subsystems. The map definition macros keep their libbpf shapes, which are
+// plain C already.
+
+#include <linux/types.h>
+
+#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
+
+// the value is irrelevant natively
+#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/proxmox-ebpf/src/bpf-shim/bpf_debug.h b/proxmox-ebpf/src/bpf-shim/bpf_debug.h
new file mode 100644
index 000000000000..a7e5eac0d3b3
--- /dev/null
+++ b/proxmox-ebpf/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/proxmox-ebpf/src/bpf-shim/linux/bpf.h b/proxmox-ebpf/src/bpf-shim/linux/bpf.h
new file mode 100644
index 000000000000..ff259c329522
--- /dev/null
+++ b/proxmox-ebpf/src/bpf-shim/linux/bpf.h
@@ -0,0 +1,25 @@
+#ifndef PROXMOX_EBPF_SHIM_LINUX_BPF_H
+#define PROXMOX_EBPF_SHIM_LINUX_BPF_H
+
+// Native stand-in for <linux/bpf.h>, only what the programs touch. The other UAPI headers
+// compile natively as they are.
+
+#include <linux/types.h>
+
+// data and data_end are wide enough for native pointers, 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;
+};
+
+// map-definition constants, the values are irrelevant natively
+enum {
+ BPF_MAP_TYPE_HASH = 1,
+};
+#define BPF_F_NO_PREALLOC 1
+
+#endif
diff --git a/proxmox-ebpf/tests/bpf/fixture.bpf.c b/proxmox-ebpf/tests/bpf/fixture.bpf.c
new file mode 100644
index 000000000000..67b4fd4a38ad
--- /dev/null
+++ b/proxmox-ebpf/tests/bpf/fixture.bpf.c
@@ -0,0 +1,37 @@
+// The harness's own program, compiled like a subsystem's, its BPF object is not used. It touches
+// what a subsystem program touches, the skb fields, the helpers and a map, so the shim is run by
+// the harness tests themselves.
+#include <linux/bpf.h>
+#include <linux/pkt_cls.h>
+#include <bpf/bpf_endian.h>
+#include <bpf/bpf_helpers.h>
+
+struct {
+ __uint(type, BPF_MAP_TYPE_HASH);
+ __uint(max_entries, 16);
+ __type(key, __u32);
+ __type(value, __u8);
+} fixture_stamps SEC(".maps");
+
+// writes the interface's stamp and the frame length over the first two bytes and keeps the
+// bytes they replaced in the mark, accepts a frame of an interface without a stamp as it is
+SEC("tc")
+int fixture(struct __sk_buff *skb)
+{
+ void *data = (void *)(long)skb->data;
+ void *data_end = (void *)(long)skb->data_end;
+ if (data + 2 > data_end)
+ return TC_ACT_OK;
+ __u8 *stamp = bpf_map_lookup_elem(&fixture_stamps, &skb->ifindex);
+ if (!stamp)
+ return TC_ACT_OK;
+ __u8 bytes[2];
+ if (bpf_skb_load_bytes(skb, 0, bytes, sizeof(bytes)))
+ return TC_ACT_OK;
+ skb->mark = bpf_ntohs(*(__be16 *)bytes);
+ bytes[0] = *stamp;
+ bytes[1] = skb->len;
+ if (bpf_skb_store_bytes(skb, 0, bytes, sizeof(bytes), 0))
+ return TC_ACT_OK;
+ return TC_ACT_UNSPEC;
+}
diff --git a/proxmox-ebpf/tests/common/mod.rs b/proxmox-ebpf/tests/common/mod.rs
new file mode 100644
index 000000000000..4d9c241ad1fe
--- /dev/null
+++ b/proxmox-ebpf/tests/common/mod.rs
@@ -0,0 +1,211 @@
+//! Native harness for the subsystems' BPF C programs. A test declares a program's symbol and
+//! calls it like a plain function on a fake skb, with the helpers as bounds-checked functions
+//! over its buffer and a per-thread map registry.
+
+// every test crate compiles the harness and uses its own part of it
+#![allow(dead_code)]
+
+// 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_UNSPEC: c_int = -1;
+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/linux/bpf.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 mark(&self) -> u32 {
+ self.skb.mark
+ }
+
+ 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((self as *mut TestSkb).cast()) }
+ }
+}
+
+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, u64)>> = 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().map(|(ifindex, _)| ifindex))
+}
+
+/// The flags of the redirect the last run issued, zero sends out of the interface.
+pub fn redirect_flags() -> Option<u64> {
+ REDIRECTED.with(|r| r.borrow().map(|(_, flags)| flags))
+}
+
+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 mut maps = maps.borrow_mut();
+ let Some(m) = maps.get_mut(&(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_mut(key) {
+ // the box's heap allocation stays put while the registry holds it
+ Some(v) => v.as_mut_ptr() as *mut c_void,
+ None => std::ptr::null_mut(),
+ }
+ })
+}
+
+const EFAULT: c_long = nix::libc::EFAULT as c_long;
+const ENOMEM: c_long = nix::libc::ENOMEM as c_long;
+const EINVAL: c_long = nix::libc::EINVAL as c_long;
+
+#[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 {
+ // the kernel clears the destination of a failed read
+ unsafe { std::ptr::write_bytes(to as *mut u8, 0, len) };
+ return -EFAULT;
+ }
+ 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 -EFAULT;
+ }
+ 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 { -ENOMEM } 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) };
+ // the kernel refuses to cut into the headers it knows, which is the link header here.
+ // Growing stops at the harness buffer
+ let min_len = 14;
+ if new_len < min_len || new_len as usize > PKT_CAP {
+ return -EINVAL;
+ }
+ // 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, flags)));
+ TC_ACT_REDIRECT as c_long
+}
diff --git a/proxmox-ebpf/tests/harness.rs b/proxmox-ebpf/tests/harness.rs
new file mode 100644
index 000000000000..470e0a1cd88b
--- /dev/null
+++ b/proxmox-ebpf/tests/harness.rs
@@ -0,0 +1,84 @@
+//! The harness's own tests, the frame, the helpers, the map registry and a program built
+//! against the shim.
+mod common;
+
+use std::ffi::{c_int, c_void};
+
+use common::*;
+
+unsafe extern "C" {
+ fn fixture(skb: *mut SkBuff) -> c_int;
+ static fixture_stamps: c_void;
+}
+
+unsafe extern "C" fn pass(_skb: *mut SkBuff) -> c_int {
+ TC_ACT_UNSPEC
+}
+
+unsafe extern "C" fn accept(_skb: *mut SkBuff) -> c_int {
+ TC_ACT_OK
+}
+
+#[test]
+fn skb_round_trips_the_frame() {
+ let frame = [1u8, 2, 3, 4];
+ let mut skb = TestSkb::new(&frame, 7);
+ assert_eq!(skb.run(pass), TC_ACT_UNSPEC);
+ assert_eq!(skb.run(accept), TC_ACT_OK);
+ assert_eq!(skb.packet(), &frame);
+ assert_eq!(redirected(), None);
+ assert_eq!(redirect_flags(), None);
+}
+
+#[test]
+fn helpers_read_and_write_the_frame() {
+ let mut skb = TestSkb::new(&[1, 2, 3, 4, 5, 6], 7);
+ let raw = (&mut skb as *mut TestSkb).cast::<c_void>();
+ let mut out = [0u8; 2];
+ assert_eq!(bpf_skb_load_bytes(raw, 2, out.as_mut_ptr().cast(), 2), 0);
+ assert_eq!(out, [3, 4]);
+ assert_eq!(
+ bpf_skb_store_bytes(raw, 4, [9u8, 9].as_ptr().cast(), 2, 0),
+ 0
+ );
+ assert_eq!(skb.packet(), &[1, 2, 3, 4, 9, 9]);
+ // past the frame, the kernel clears the destination on a failed load
+ assert_ne!(bpf_skb_load_bytes(raw, 5, out.as_mut_ptr().cast(), 2), 0);
+ assert_eq!(out, [0, 0]);
+ assert_ne!(
+ bpf_skb_store_bytes(raw, 5, [9u8, 9].as_ptr().cast(), 2, 0),
+ 0
+ );
+ assert_eq!(skb.packet(), &[1, 2, 3, 4, 9, 9]);
+}
+
+#[test]
+fn maps_answer_registered_entries() {
+ // any address serves as a map's identity, the program hands the map's address to the helper
+ let map = 0x1000 as *const c_void;
+ register_map(map, 4);
+ map_insert(map, &7u32.to_ne_bytes(), &[9, 8, 7]);
+ let key = 7u32.to_ne_bytes();
+ let hit = bpf_map_lookup_elem(map.cast_mut(), key.as_ptr().cast());
+ assert!(!hit.is_null());
+ assert_eq!(
+ unsafe { std::slice::from_raw_parts(hit as *const u8, 3) },
+ &[9, 8, 7]
+ );
+ let key = 8u32.to_ne_bytes();
+ assert!(bpf_map_lookup_elem(map.cast_mut(), key.as_ptr().cast()).is_null());
+}
+
+#[test]
+fn a_program_runs_against_the_shim() {
+ let stamps = &raw const fixture_stamps;
+ register_map(stamps, 4);
+ map_insert(stamps, &7u32.to_ne_bytes(), &[0xab]);
+ let mut skb = TestSkb::new(&[0x12, 0x34, 5, 6], 9);
+ assert_eq!(skb.run(fixture), TC_ACT_OK);
+ assert_eq!(skb.packet(), &[0x12, 0x34, 5, 6]);
+ let mut skb = TestSkb::new(&[0x12, 0x34, 5, 6], 7);
+ assert_eq!(skb.run(fixture), TC_ACT_UNSPEC);
+ assert_eq!(skb.packet(), &[0xab, 4, 5, 6]);
+ assert_eq!(skb.mark(), 0x1234);
+}
--
2.47.3
next prev parent reply other threads:[~2026-11-04 13:50 UTC|newest]
Thread overview: 8+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-04 13:48 [PATCH proxmox v6 0/6] add proxmox-ebpf library Hannes Laimer
2026-11-04 13:49 ` [PATCH proxmox v6 1/6] ebpf: add the crate with its BPF build Hannes Laimer
2026-11-04 13:49 ` [PATCH proxmox v6 2/6] ebpf: tc: add the link plumbing Hannes Laimer
2026-11-04 13:49 ` [PATCH proxmox v6 3/6] ebpf: subsystem: add the shared tc program handling Hannes Laimer
2026-11-04 13:49 ` [PATCH proxmox v6 4/6] ebpf: subsystem: place the links of different subsystems in a fixed order Hannes Laimer
2026-11-04 13:49 ` Hannes Laimer [this message]
2026-11-04 13:49 ` [PATCH proxmox v6 6/6] ebpf: debian: package the crate as a Rust library Hannes Laimer
2026-11-04 16:02 ` superseded: [PATCH proxmox v6 0/6] 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=20261104134905.1623451-6-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.