* [PATCH proxmox-ebpf 1/3] add the shared tc subsystem code
2026-09-02 12:32 [PATCH proxmox-ebpf 0/3] add proxmox-ebpf library Hannes Laimer
@ 2026-09-02 12:32 ` Hannes Laimer
2026-09-02 12:32 ` [PATCH proxmox-ebpf 2/3] tests: add a native harness for the BPF C programs Hannes Laimer
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: Hannes Laimer @ 2026-09-02 12:32 UTC (permalink / raw)
To: pve-devel
A subsystem owns tc classifier programs and their maps, attached per
interface and pinned in bpffs together with an object fingerprint and
a schema version, so state survives between the one-shot calls from
the consumer driving it and re-running never interrupts traffic.
Apply paths coordinate through a shared flock, loading takes it
exclusively.
Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
src/lib.rs | 3 +
src/subsystem.rs | 392 +++++++++++++++++++++++++++++++++++++++++++++++
src/tc.rs | 151 ++++++++++++++++++
3 files changed, 546 insertions(+)
create mode 100644 src/subsystem.rs
create mode 100644 src/tc.rs
diff --git a/src/lib.rs b/src/lib.rs
index 244f44c..572d861 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,2 +1,5 @@
//! eBPF subsystems, one per concern, each behind a cargo feature of its name. Nothing here runs
//! on its own, consumers pull in only the subsystem they drive.
+
+pub mod subsystem;
+pub mod tc;
diff --git a/src/subsystem.rs b/src/subsystem.rs
new file mode 100644
index 0000000..466e5cb
--- /dev/null
+++ b/src/subsystem.rs
@@ -0,0 +1,392 @@
+//! The shared subsystem code. A subsystem's tc programs, one per direction it declares, with
+//! their maps and links, pinned under `/sys/fs/bpf/proxmox-ebpf/<name>/`. The loaded BPF stays in
+//! the kernel between invocations, so [`TcPrograms::ensure_loaded`] loads and verifies only on the
+//! first run and on a version change. Everything else attaches links and syncs maps against what
+//! is already there. Each subsystem owns one [`TcPrograms`].
+
+use std::{collections::HashSet, fs::File, io::ErrorKind, path::PathBuf};
+
+use anyhow::Context;
+use aya::{EbpfLoader, programs::SchedClassifier};
+use nix::fcntl::{Flock, FlockArg};
+
+use crate::tc::{self, Direction};
+
+pub const VERIFY_ROOT: &str = "/sys/fs/bpf/proxmox-ebpf-test";
+
+const PIN_ROOT: &str = "/sys/fs/bpf/proxmox-ebpf";
+const RUN_ROOT: &str = "/run/proxmox-ebpf";
+
+fn pin_root_for(name: &str) -> PathBuf {
+ PathBuf::from(PIN_ROOT).join(name)
+}
+
+// Small persisted state under /run/proxmox-ebpf/<name>/<key>, used to decide on each run whether
+// to tear down (schema changed) and whether to refresh existing links (the object changed).
+fn read_state(name: &str, key: &str) -> Option<u64> {
+ std::fs::read_to_string(PathBuf::from(RUN_ROOT).join(name).join(key))
+ .ok()
+ .and_then(|s| s.trim().parse().ok())
+}
+
+fn write_state(name: &str, key: &str, value: u64) -> anyhow::Result<()> {
+ let path = PathBuf::from(RUN_ROOT).join(name).join(key);
+ if let Some(parent) = path.parent() {
+ std::fs::create_dir_all(parent)?;
+ }
+ std::fs::write(path, format!("{value}\n"))?;
+ Ok(())
+}
+
+/// The pinned tc programs for one subsystem.
+///
+/// Holds only the immutable description. The loaded BPF lives in the kernel, pinned, and is
+/// reached back through those pins, so normal operation runs no verifier.
+pub struct TcPrograms {
+ name: &'static str,
+ obj: &'static [u8],
+ fingerprint: u64,
+ prog_name: fn(Direction) -> &'static str,
+ directions: &'static [Direction],
+ schema_version: Option<u32>,
+}
+
+impl TcPrograms {
+ pub fn new(
+ name: &'static str,
+ obj: &'static [u8],
+ fingerprint: u64,
+ prog_name: fn(Direction) -> &'static str,
+ directions: &'static [Direction],
+ schema_version: Option<u32>,
+ ) -> Self {
+ Self {
+ name,
+ obj,
+ fingerprint,
+ prog_name,
+ directions,
+ schema_version,
+ }
+ }
+
+ fn pin_root(&self) -> PathBuf {
+ pin_root_for(self.name)
+ }
+ fn links_dir(&self) -> PathBuf {
+ self.pin_root().join("links")
+ }
+ fn link_pin_path(&self, ifindex: u32, dir: Direction) -> PathBuf {
+ self.links_dir().join(tc::pin_filename(ifindex, dir))
+ }
+ fn prog_dir(&self) -> PathBuf {
+ self.pin_root().join("prog")
+ }
+ fn prog_pin_path(&self, dir: Direction) -> PathBuf {
+ self.prog_dir().join(dir.as_str())
+ }
+
+ /// Open a pinned BPF hash map by name, for the owning subsystem to sync. Valid once
+ /// [`ensure_loaded`](Self::ensure_loaded) has run, which is every caller's first step.
+ pub fn hash_map<K: aya::Pod, V: aya::Pod>(
+ &self,
+ name: &str,
+ ) -> anyhow::Result<aya::maps::HashMap<aya::maps::MapData, K, V>> {
+ let map = aya::maps::MapData::from_pin(self.pin_root().join(name))
+ .with_context(|| format!("{}: open pinned map {name}", self.name))?;
+ Ok(aya::maps::HashMap::try_from(aya::maps::Map::HashMap(map))?)
+ }
+
+ /// Open one direction's pinned program. A plain `BPF_OBJ_GET`, no verifier, since the program
+ /// was checked once when [`ensure_loaded`](Self::ensure_loaded) installed it.
+ fn program(&self, dir: Direction) -> anyhow::Result<SchedClassifier> {
+ SchedClassifier::from_pin(self.prog_pin_path(dir))
+ .with_context(|| format!("{}: open pinned program {}", self.name, dir.as_str()))
+ }
+
+ /// FNV-1a over the embedded object. A `const fn`, so each subsystem folds it into a `const` at
+ /// compile time and the per-invocation path never rehashes a constant.
+ pub const fn obj_fingerprint(obj: &[u8]) -> u64 {
+ let mut hash = 0xcbf29ce484222325u64;
+ let mut i = 0;
+ while i < obj.len() {
+ hash ^= obj[i] as u64;
+ hash = hash.wrapping_mul(0x100000001b3);
+ i += 1;
+ }
+ hash
+ }
+
+ /// The per-subsystem apply lock under `/run`, one file taken in two modes. A full apply (and
+ /// any install/teardown) takes it [exclusively](Self::lock_exclusive) so its enumerate, detach
+ /// and attach run as one unit. An additive single-interface apply takes it
+ /// [shared](Self::lock_shared) so guest plugs run concurrently and only block while a full
+ /// apply holds it. The kernel drops the lock if the process dies.
+ fn lock(&self, arg: FlockArg) -> anyhow::Result<Flock<File>> {
+ let dir = PathBuf::from(RUN_ROOT).join(self.name);
+ std::fs::create_dir_all(&dir)?;
+ let file = File::create(dir.join("lock"))?;
+ Flock::lock(file, arg).map_err(|(_, e)| anyhow::Error::new(e))
+ }
+
+ /// Take the apply lock in shared mode, for an additive single-interface apply.
+ pub fn lock_shared(&self) -> anyhow::Result<Flock<File>> {
+ self.lock(FlockArg::LockShared)
+ }
+
+ /// Take the apply lock exclusively, for a full apply or an install/teardown.
+ pub fn lock_exclusive(&self) -> anyhow::Result<Flock<File>> {
+ self.lock(FlockArg::LockExclusive)
+ }
+
+ /// Make sure the programs are loaded and pinned. The load runs only when there is no current
+ /// pin, when the schema version changed (a rebuild), or when the object changed (a refresh).
+ /// Otherwise the programs and maps pinned by an earlier run are reused untouched. Returns
+ /// whether a (re)install happened.
+ ///
+ /// Takes no lock of its own: the caller holds the apply lock exclusively whenever an install
+ /// may be needed (it gates on [`is_current`](Self::is_current) first), so the install path
+ /// here is already serialized. A subsystem with no maps passes no schema version and so never
+ /// verifies or tears down.
+ pub fn ensure_loaded(&self) -> anyhow::Result<bool> {
+ if self.is_current() {
+ return Ok(false);
+ }
+
+ // rebuild when the pinned programs are of an unknown or different schema: a different
+ // recorded schema_version, or none recorded while programs are still pinned (the /run
+ // markers were lost but the bpffs pins survived, e.g. across a service restart). The
+ // loaded map layout is then unknown, so tear down rather than bind new code to old maps.
+ let schema_changed = self.programs_pinned()
+ && match self.schema_version {
+ Some(v) => read_state(self.name, "schema_version") != Some(v as u64),
+ None => false,
+ };
+ let code_changed = read_state(self.name, "fingerprint") != Some(self.fingerprint);
+
+ if schema_changed {
+ log::warn!("{}: schema changed or unknown, rebuilding", self.name);
+ // verify the new code loads against throw-away state before tearing the old down, so a
+ // verifier rejection can't leave us with the old state wiped and nothing to replace it
+ self.verify().with_context(|| {
+ format!(
+ "{}: new BPF code does not load against fresh state",
+ self.name
+ )
+ })?;
+ self.tear_down().context("tear_down")?;
+ }
+
+ self.load_and_pin().context("load_and_pin")?;
+
+ // refresh links pinned by a previous run onto the new code. no-op on a fresh install or
+ // right after a teardown, where there are no links yet
+ if code_changed {
+ self.swap_existing_links();
+ }
+
+ if let Some(v) = self.schema_version
+ && let Err(e) = write_state(self.name, "schema_version", v as u64)
+ {
+ log::warn!("{}: failed to persist schema_version: {e:#}", self.name);
+ }
+ if let Err(e) = write_state(self.name, "fingerprint", self.fingerprint) {
+ log::warn!("{}: failed to persist fingerprint: {e:#}", self.name);
+ }
+ Ok(true)
+ }
+
+ /// True when the pinned programs already match this build, the `/run` fingerprint and schema
+ /// version agree with the embedded object and both programs are pinned. Lock-free. Callers use
+ /// it to decide whether a run needs the exclusive lock (an install) or only the shared one.
+ pub fn is_current(&self) -> bool {
+ let schema_changed = match self.schema_version {
+ Some(v) => read_state(self.name, "schema_version").is_some_and(|s| s != v as u64),
+ None => false,
+ };
+ let code_changed = read_state(self.name, "fingerprint") != Some(self.fingerprint);
+ !schema_changed && !code_changed && self.programs_pinned()
+ }
+
+ fn programs_pinned(&self) -> bool {
+ self.directions
+ .iter()
+ .all(|&dir| self.prog_pin_path(dir).exists())
+ }
+
+ fn verify(&self) -> anyhow::Result<()> {
+ let names: Vec<&str> = self
+ .directions
+ .iter()
+ .map(|&d| (self.prog_name)(d))
+ .collect();
+ tc::verify(&[self.obj], &names)
+ }
+
+ fn tear_down(&self) -> anyhow::Result<()> {
+ match std::fs::remove_dir_all(self.pin_root()) {
+ Ok(()) => {}
+ Err(e) if e.kind() == ErrorKind::NotFound => {}
+ Err(e) => return Err(e.into()),
+ }
+ std::fs::create_dir_all(self.links_dir())?;
+ Ok(())
+ }
+
+ /// Load and verify the object, then pin its programs. The verifier runs here and in the
+ /// throwaway [`verify`](Self::verify), nowhere else. Steady-state runs reuse the pins. The
+ /// `bpf` handle is dropped at the end, the pinned programs and maps stay resident in the
+ /// kernel.
+ fn load_and_pin(&self) -> anyhow::Result<()> {
+ std::fs::create_dir_all(self.links_dir())?;
+ std::fs::create_dir_all(self.prog_dir())?;
+ let mut bpf = EbpfLoader::new()
+ .map_pin_path(self.pin_root())
+ .load(self.obj)?;
+ for &dir in self.directions {
+ let prog: &mut SchedClassifier =
+ bpf.program_mut((self.prog_name)(dir)).unwrap().try_into()?;
+ prog.load()?;
+ let path = self.prog_pin_path(dir);
+ // refreshed program is a new kernel object but the old pin file still exists, so
+ // remove it before re-pinning or pin() hits EEXIST
+ match std::fs::remove_file(&path) {
+ Ok(()) => {}
+ Err(e) if e.kind() == ErrorKind::NotFound => {}
+ Err(e) => return Err(e).context("remove stale program pin"),
+ }
+ prog.pin(&path)?;
+ }
+ Ok(())
+ }
+
+ fn live_links(&self) -> anyhow::Result<Vec<(u32, Direction)>> {
+ tc::read_pinned_links(&self.links_dir())
+ }
+
+ fn swap_existing_links(&self) {
+ let live = match self.live_links() {
+ Ok(l) => l,
+ Err(e) => {
+ log::error!("{}: read pinned links: {e:#}", self.name);
+ return;
+ }
+ };
+ for (ifindex, dir) in live {
+ if let Err(e) = self.swap_pinned_link(ifindex, dir) {
+ log::error!("{}: swap link {ifindex}-{}: {e:#}", self.name, dir.as_str());
+ }
+ }
+ }
+
+ /// Make the attached set match `desired`. Detach interfaces no longer wanted, attach the ones
+ /// missing. Refreshing existing links onto new code is done by
+ /// [`ensure_loaded`](Self::ensure_loaded). Runs under the caller's exclusive apply lock, so the
+ /// live set it samples cannot change under it.
+ ///
+ /// Returns an error if any interface failed to attach (after attempting all of them): a NIC
+ /// left without its program would pass traffic unenforced, so that surfaces as a failed apply
+ /// rather than a silent fail-open. A failed detach only leaves over-enforcement and is logged.
+ pub fn reconcile(&self, desired: &HashSet<u32>) -> anyhow::Result<()> {
+ let live: HashSet<(u32, Direction)> = self.live_links()?.into_iter().collect();
+
+ for &(ifidx, dir) in &live {
+ if desired.contains(&ifidx) {
+ continue;
+ }
+ log::debug!("{}: detach {ifidx}-{}", self.name, dir.as_str());
+ if let Err(e) = tc::detach_pinned_link(&self.link_pin_path(ifidx, dir)) {
+ log::error!("{}: detach {ifidx}-{}: {e:#}", self.name, dir.as_str());
+ } else {
+ log::info!("{}: detached {ifidx}-{}", self.name, dir.as_str());
+ }
+ }
+
+ let mut failed = 0usize;
+ for &ifidx in desired {
+ for &dir in self.directions {
+ if live.contains(&(ifidx, dir)) {
+ continue;
+ }
+ if let Err(e) = self.attach_and_pin(ifidx, dir) {
+ log::error!("{}: attach {ifidx}-{}: {e:#}", self.name, dir.as_str());
+ failed += 1;
+ }
+ }
+ }
+ if failed > 0 {
+ anyhow::bail!("{}: {failed} interface(s) failed to attach", self.name);
+ }
+ Ok(())
+ }
+
+ /// Attach the programs to a single interface, additively, without touching others.
+ ///
+ /// If a pin already exists, swap the program in place with no traffic gap. The swap only
+ /// succeeds while the link is still on the live netdev at this ifindex. A recycled ifindex
+ /// leaves a defunct pin, so the swap fails and we reclaim it and attach fresh. A failed attach
+ /// propagates so the caller can refuse to bring the NIC up unenforced.
+ pub fn attach_iface(&self, ifindex: u32) -> anyhow::Result<()> {
+ for &dir in self.directions {
+ let path = self.link_pin_path(ifindex, dir);
+ if path.exists() {
+ match self.swap_pinned_link(ifindex, dir) {
+ Ok(()) => continue,
+ Err(e) => {
+ log::debug!(
+ "{}: {ifindex}-{} swap failed ({e:#}), rebuilding",
+ self.name,
+ dir.as_str()
+ );
+ if let Err(e) = tc::detach_pinned_link(&path) {
+ log::warn!(
+ "{}: reclaim {ifindex}-{}: unpin stale link: {e:#}",
+ self.name,
+ dir.as_str()
+ );
+ let _ = std::fs::remove_file(&path);
+ }
+ }
+ }
+ }
+ self.attach_and_pin(ifindex, dir)
+ .with_context(|| format!("{}: attach {ifindex}-{}", self.name, dir.as_str()))?;
+ }
+ Ok(())
+ }
+
+ fn swap_pinned_link(&self, ifindex: u32, dir: Direction) -> anyhow::Result<()> {
+ let path = self.link_pin_path(ifindex, dir);
+ let mut prog = self.program(dir)?;
+ tc::swap_pinned_link(&mut prog, &path)?;
+ Ok(())
+ }
+
+ fn attach_and_pin(&self, ifindex: u32, dir: Direction) -> anyhow::Result<()> {
+ let path = self.link_pin_path(ifindex, dir);
+ let mut prog = self.program(dir)?;
+ tc::attach_and_pin(&mut prog, ifindex, dir, &path)?;
+ Ok(())
+ }
+
+ /// Detach every pinned link and drop all pinned and `/run` state for this subsystem, under the
+ /// exclusive lock. For package removal: leaving links attached would keep interfaces enforcing
+ /// the last-applied policy with nothing left to update them. Best-effort past the lock: logs
+ /// and continues so one failure does not strand the rest.
+ pub fn clear(&self) -> anyhow::Result<()> {
+ let _lock = self.lock_exclusive()?;
+ for (ifindex, dir) in self.live_links().unwrap_or_default() {
+ if let Err(e) = tc::detach_pinned_link(&self.link_pin_path(ifindex, dir)) {
+ log::warn!("{}: detach {ifindex}-{}: {e:#}", self.name, dir.as_str());
+ }
+ }
+ for path in [self.pin_root(), PathBuf::from(RUN_ROOT).join(self.name)] {
+ if let Err(e) = std::fs::remove_dir_all(&path)
+ && e.kind() != ErrorKind::NotFound
+ {
+ log::warn!("{}: remove {}: {e:#}", self.name, path.display());
+ }
+ }
+ Ok(())
+ }
+}
diff --git a/src/tc.rs b/src/tc.rs
new file mode 100644
index 0000000..205df48
--- /dev/null
+++ b/src/tc.rs
@@ -0,0 +1,151 @@
+//! TC link plumbing shared by all subsystems. A `Direction` enum, attach/swap/detach free
+//! functions, and a uniform pin-filename layout `{ifindex}-{direction}`.
+
+use std::{io::ErrorKind, path::Path, str::FromStr};
+
+use anyhow::Context;
+use aya::{
+ EbpfLoader,
+ programs::{
+ SchedClassifier, TcAttachType,
+ links::{FdLink, PinnedLink},
+ tc,
+ },
+};
+
+use crate::subsystem::VERIFY_ROOT;
+
+#[derive(Copy, Clone, Hash, PartialEq, Eq, Debug)]
+pub enum Direction {
+ Ingress,
+ Egress,
+}
+
+pub const DIRECTIONS: [Direction; 2] = [Direction::Ingress, Direction::Egress];
+
+impl Direction {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::Ingress => "ingress",
+ Self::Egress => "egress",
+ }
+ }
+ pub fn aya_type(self) -> TcAttachType {
+ match self {
+ Self::Ingress => TcAttachType::Ingress,
+ Self::Egress => TcAttachType::Egress,
+ }
+ }
+}
+
+impl FromStr for Direction {
+ type Err = ();
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s {
+ "ingress" => Ok(Self::Ingress),
+ "egress" => Ok(Self::Egress),
+ _ => Err(()),
+ }
+ }
+}
+
+/// RAII handle for the shared verify root. Constructor wipes any stale contents and creates the
+/// dir. `Drop` removes it. Cleanup happens even if the verify body panics or returns Err.
+struct VerifyRoot;
+
+impl VerifyRoot {
+ fn new() -> anyhow::Result<Self> {
+ let _ = std::fs::remove_dir_all(VERIFY_ROOT);
+ std::fs::create_dir_all(VERIFY_ROOT)
+ .with_context(|| format!("create verify root {VERIFY_ROOT}"))?;
+ Ok(Self)
+ }
+ fn path(&self) -> &Path {
+ Path::new(VERIFY_ROOT)
+ }
+}
+
+impl Drop for VerifyRoot {
+ fn drop(&mut self) {
+ if let Err(e) = std::fs::remove_dir_all(VERIFY_ROOT) {
+ log::warn!("failed to clean up verify root {VERIFY_ROOT}: {e:#}");
+ }
+ }
+}
+
+/// Loads every named program in each object against a throwaway pin root, catching verifier
+/// regressions before any real state is touched.
+pub fn verify(objects: &[&[u8]], program_names: &[&str]) -> anyhow::Result<()> {
+ let root = VerifyRoot::new()?;
+ for &obj in objects {
+ let mut bpf = EbpfLoader::new().map_pin_path(root.path()).load(obj)?;
+ for &name in program_names {
+ let p: &mut SchedClassifier = bpf.program_mut(name).unwrap().try_into()?;
+ p.load()?;
+ }
+ }
+ Ok(())
+}
+
+pub fn pin_filename(ifindex: u32, dir: Direction) -> String {
+ format!("{ifindex}-{}", dir.as_str())
+}
+
+/// Ensure clsact qdisc on the iface, attach `prog` in `dir`, pin the link.
+pub fn attach_and_pin(
+ prog: &mut SchedClassifier,
+ ifindex: u32,
+ dir: Direction,
+ pin_path: &Path,
+) -> anyhow::Result<()> {
+ let name = nix::net::if_::if_indextoname(ifindex)?;
+ let name = name.to_str()?;
+ let _ = tc::qdisc_add_clsact(name);
+ let link_id = prog.attach(name, dir.aya_type())?;
+ let link = prog.take_link(link_id)?;
+ let fd_link: FdLink = link.try_into()?;
+ fd_link.pin(pin_path)?;
+ Ok(())
+}
+
+/// Rebind a pinned link to `prog` via LINK_UPDATE. Atomic, traffic sees no detach/reattach gap.
+pub fn swap_pinned_link(prog: &mut SchedClassifier, pin_path: &Path) -> anyhow::Result<()> {
+ let pinned = PinnedLink::from_pin(pin_path)?;
+ let fd_link: FdLink = pinned.into();
+ let link = fd_link.try_into()?;
+ let new_id = prog.attach_to_link(link)?;
+ // take the handle out of aya's internal tracking, we have the pin
+ let _ = prog.take_link(new_id)?;
+ Ok(())
+}
+
+pub fn detach_pinned_link(pin_path: &Path) -> anyhow::Result<()> {
+ let pinned = PinnedLink::from_pin(pin_path)?;
+ let _fd_link = pinned.unpin()?;
+ Ok(())
+}
+
+/// Read and parse every pin file in `links_dir` as `{ifindex}-{direction}`. Unrecognized names are
+/// skipped with a warning.
+pub fn read_pinned_links(links_dir: &Path) -> anyhow::Result<Vec<(u32, Direction)>> {
+ let mut out = Vec::new();
+ let dir = match std::fs::read_dir(links_dir) {
+ Ok(d) => d,
+ Err(e) if e.kind() == ErrorKind::NotFound => return Ok(out),
+ Err(e) => return Err(e.into()),
+ };
+ for entry in dir {
+ let entry = entry?;
+ let name = entry.file_name();
+ let name = name.to_string_lossy();
+ let Some((ifidx_str, dir_str)) = name.split_once('-') else {
+ log::warn!("unrecognized pin file in links dir: {name}");
+ continue;
+ };
+ match (ifidx_str.parse::<u32>(), dir_str.parse::<Direction>()) {
+ (Ok(ifidx), Ok(d)) => out.push((ifidx, d)),
+ _ => log::warn!("unrecognized pin file in links dir: {name}"),
+ }
+ }
+ Ok(out)
+}
--
2.47.3
^ permalink raw reply related [flat|nested] 5+ messages in thread* [PATCH proxmox-ebpf 2/3] tests: add a native harness for the BPF C programs
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
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
3 siblings, 0 replies; 5+ messages in thread
From: Hannes Laimer @ 2026-09-02 12:32 UTC (permalink / raw)
To: pve-devel
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
^ permalink raw reply related [flat|nested] 5+ messages in thread* [PATCH proxmox-ebpf 3/3] debian: package the crate as a rust library
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 ` [PATCH proxmox-ebpf 2/3] tests: add a native harness for the BPF C programs Hannes Laimer
@ 2026-09-02 12:32 ` Hannes Laimer
2026-09-02 12:34 ` [PATCH proxmox-ebpf 0/3] add proxmox-ebpf library Hannes Laimer
3 siblings, 0 replies; 5+ messages in thread
From: Hannes Laimer @ 2026-09-02 12:32 UTC (permalink / raw)
To: pve-devel
Through debcargo with the overlay in debian/, like we do for the other
rust crates. The BPF programs are compiled by the build script, so
building against the crate pulls in clang and the libbpf headers as
well.
Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
.gitignore | 1 +
Cargo.toml | 7 ++++++
Makefile | 48 ++++++++++++++++++++++++++++++++++++++++
build.rs | 5 +++++
debian/changelog | 5 +++++
debian/control | 52 ++++++++++++++++++++++++++++++++++++++++++++
debian/copyright | 18 +++++++++++++++
debian/debcargo.toml | 13 +++++++++++
debian/source/format | 1 +
9 files changed, 150 insertions(+)
create mode 100644 Makefile
create mode 100644 debian/changelog
create mode 100644 debian/control
create mode 100644 debian/copyright
create mode 100644 debian/debcargo.toml
create mode 100644 debian/source/format
diff --git a/.gitignore b/.gitignore
index 3cb8cae..574dcbd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ Cargo.lock
*.changes
*.tar.?z
/proxmox-ebpf-[0-9]*/
+/build
diff --git a/Cargo.toml b/Cargo.toml
index 27010c4..a87bb2e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,14 @@
[package]
name = "proxmox-ebpf"
version = "0.1.0"
+description = "Proxmox VE eBPF"
+authors = ["Proxmox Support Team <support@proxmox.com>"]
edition = "2024"
+rust-version = "1.85"
+license = "AGPL-3"
+homepage = "https://proxmox.com"
+
+exclude = ["build", "debian"]
[features]
# When enabled, BPF programs compile with -DBPF_DEBUG so their DBG() macros
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..3650a9d
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,48 @@
+include /usr/share/dpkg/pkg-info.mk
+
+CRATE := proxmox-ebpf
+BUILDDIR ?= build
+CARGO ?= cargo
+
+DSC := $(BUILDDIR)/rust-$(CRATE)_$(DEB_VERSION).dsc
+
+all: cargo-build
+
+.PHONY: cargo-build
+cargo-build:
+ $(CARGO) build --all-features
+
+.PHONY: check test
+check test:
+ $(CARGO) test --all-features
+
+# the generated control is copied back so the checked-in one stays current
+$(BUILDDIR)/$(CRATE): Cargo.toml build.rs src include tests debian
+ rm -rf $@
+ mkdir -p $(BUILDDIR)
+ echo system > $(BUILDDIR)/rust-toolchain
+ rm -f debian/control
+ debcargo package \
+ --config $(CURDIR)/debian/debcargo.toml \
+ --changelog-ready \
+ --no-overlay-write-back \
+ --directory $(CURDIR)/$@ \
+ $(CRATE) \
+ $(DEB_VERSION_UPSTREAM)
+ rm -f $@/debian/source/format.debcargo.hint
+ cp $@/debian/control debian/control
+
+.PHONY: deb
+deb: $(BUILDDIR)/$(CRATE)
+ cd $(BUILDDIR)/$(CRATE); dpkg-buildpackage -b -uc -us
+ lintian $(BUILDDIR)/*.deb
+
+.PHONY: dsc
+dsc: $(BUILDDIR)/$(CRATE)
+ cd $(BUILDDIR)/$(CRATE); dpkg-buildpackage -S -us -uc -d
+ lintian $(DSC)
+
+.PHONY: clean
+clean:
+ $(CARGO) clean
+ rm -rf $(BUILDDIR)
diff --git a/build.rs b/build.rs
index e62be72..9d3d92f 100644
--- a/build.rs
+++ b/build.rs
@@ -73,6 +73,11 @@ fn main() {
);
println!("cargo:rustc-link-search=native={}", out_dir.display());
println!("cargo:rustc-link-lib=static=bpf_native");
+ // dh-cargo wants every static library in the build declared, this one is from our own sources
+ println!(
+ "dh-cargo:deb-built-using=bpf_native=0={}",
+ manifest_dir.display()
+ );
}
// native build against the shim headers, so tests call the program as a plain function
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..71e3394
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,5 @@
+rust-proxmox-ebpf (0.1.0) trixie; urgency=medium
+
+ * Initial release.
+
+ -- Proxmox Support Team <support@proxmox.com> Wed, 02 Sep 2026 10:00:00 +0200
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..d1400c6
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,52 @@
+Source: rust-proxmox-ebpf
+Section: rust
+Priority: optional
+Build-Depends: debhelper-compat (= 13),
+ dh-sequence-cargo
+Build-Depends-Arch: cargo:native <!nocheck>,
+ rustc:native (>= 1.85) <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-anyhow-1+default-dev <!nocheck>,
+ librust-aya-0.13+default-dev <!nocheck>,
+ librust-log-0.4+default-dev <!nocheck>,
+ librust-nix-0.29+default-dev <!nocheck>,
+ librust-nix-0.29+fs-dev <!nocheck>,
+ librust-nix-0.29+net-dev <!nocheck>,
+ clang <!nocheck>,
+ libbpf-dev <!nocheck>,
+ linux-libc-dev <!nocheck>
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Standards-Version: 4.7.2
+Vcs-Git: https://salsa.debian.org/rust-team/debcargo-conf.git [src/proxmox-ebpf]
+Vcs-Browser: https://salsa.debian.org/rust-team/debcargo-conf/tree/master/src/proxmox-ebpf
+Homepage: https://proxmox.com
+X-Cargo-Crate: proxmox-ebpf
+
+Package: librust-proxmox-ebpf-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-anyhow-1+default-dev,
+ librust-aya-0.13+default-dev,
+ librust-log-0.4+default-dev,
+ librust-nix-0.29+default-dev,
+ librust-nix-0.29+fs-dev,
+ librust-nix-0.29+net-dev,
+ clang,
+ libbpf-dev,
+ linux-libc-dev
+Provides:
+ librust-proxmox-ebpf+bpf-debug-dev (= ${binary:Version}),
+ librust-proxmox-ebpf+default-dev (= ${binary:Version}),
+ librust-proxmox-ebpf-0-dev (= ${binary:Version}),
+ librust-proxmox-ebpf-0+bpf-debug-dev (= ${binary:Version}),
+ librust-proxmox-ebpf-0+default-dev (= ${binary:Version}),
+ librust-proxmox-ebpf-0.1-dev (= ${binary:Version}),
+ librust-proxmox-ebpf-0.1+bpf-debug-dev (= ${binary:Version}),
+ librust-proxmox-ebpf-0.1+default-dev (= ${binary:Version}),
+ librust-proxmox-ebpf-0.1.0-dev (= ${binary:Version}),
+ librust-proxmox-ebpf-0.1.0+bpf-debug-dev (= ${binary:Version}),
+ librust-proxmox-ebpf-0.1.0+default-dev (= ${binary:Version})
+Description: Proxmox VE eBPF - Rust source code
+ Source code for Debianized Rust crate "proxmox-ebpf"
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..01138fa
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,18 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+
+Files:
+ *
+Copyright: 2026 Proxmox Server Solutions GmbH <support@proxmox.com>
+License: AGPL-3.0-or-later
+ This program is free software: you can redistribute it and/or modify it under
+ the terms of the GNU Affero General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option) any
+ later version.
+ .
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ details.
+ .
+ You should have received a copy of the GNU Affero General Public License along
+ with this program. If not, see <https://www.gnu.org/licenses/>.
diff --git a/debian/debcargo.toml b/debian/debcargo.toml
new file mode 100644
index 0000000..38f95f4
--- /dev/null
+++ b/debian/debcargo.toml
@@ -0,0 +1,13 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@proxmox.com>"
+
+# the BPF programs are C sources of this crate, not vendored code
+whitelist = ["src/*/bpf/*.c"]
+
+[source]
+#vcs_git = "git://git.proxmox.com/git/proxmox-ebpf.git"
+#vcs_browser = "https://git.proxmox.com/?p=proxmox-ebpf.git"
+
+[packages.lib]
+depends = ["clang", "libbpf-dev", "linux-libc-dev"]
diff --git a/debian/source/format b/debian/source/format
new file mode 100644
index 0000000..89ae9db
--- /dev/null
+++ b/debian/source/format
@@ -0,0 +1 @@
+3.0 (native)
--
2.47.3
^ permalink raw reply related [flat|nested] 5+ messages in thread