From 582f371ff86a0e8960787e649f62749456667c7e Mon Sep 17 00:00:00 2001 From: Frando Date: Tue, 14 Apr 2026 15:00:15 +0200 Subject: [PATCH] perf: use libnftables via dlopen for in-process nft rule application Load libnftables.so.1 at runtime via dlopen and call nft_run_cmd_from_buffer directly instead of spawning the nft binary as a child process. This reduces per-invocation overhead from ~5ms (process spawn) to ~50us (in-process FFI call). Falls back to spawning the nft binary if libnftables.so is not available (e.g., minimal container images that have nft but not the shared library). Also switches run_nft_in from the async worker (rt.spawn) to the sync worker (run_closure_in) since the libnftables call is synchronous and fast. Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 11 +++ patchbay/Cargo.toml | 1 + patchbay/src/nft.rs | 160 ++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 158 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e568467..d43493e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1544,6 +1544,16 @@ version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "libredox" version = "0.1.14" @@ -1947,6 +1957,7 @@ dependencies = [ "ipnet", "iroh-metrics", "libc", + "libloading", "n0-tracing-test", "nix", "rtnetlink", diff --git a/patchbay/Cargo.toml b/patchbay/Cargo.toml index f002f78..71cb2b9 100644 --- a/patchbay/Cargo.toml +++ b/patchbay/Cargo.toml @@ -19,6 +19,7 @@ derive_more = { version = "2.1.1", features = ["debug", "display"] } futures = "0.3" ipnet = { version = "2.11", features = ["serde"] } libc = "0.2" +libloading = "0.8" nix = { version = "0.30", features = ["sched", "mount", "fs", "signal", "process", "user", "ioctl"] } rtnetlink = "0.20" serde = { version = "1", features = ["derive"] } diff --git a/patchbay/src/nft.rs b/patchbay/src/nft.rs index 8938859..6ffd8a5 100644 --- a/patchbay/src/nft.rs +++ b/patchbay/src/nft.rs @@ -11,44 +11,176 @@ use crate::{ NatConfig, NatFiltering, NatMapping, NatV6Mode, }; +// ── libnftables dlopen fast path ───────────────────────────────────── + +mod libnft { + use std::{ + ffi::{CStr, CString}, + sync::OnceLock, + }; + + use anyhow::{anyhow, Result}; + + type NftCtx = *mut std::ffi::c_void; + type NftCtxNew = unsafe extern "C" fn(flags: u32) -> NftCtx; + type NftCtxFree = unsafe extern "C" fn(ctx: NftCtx); + type NftRunCmdFromBuffer = + unsafe extern "C" fn(ctx: NftCtx, buf: *const std::ffi::c_char) -> i32; + type NftCtxBufferOutput = unsafe extern "C" fn(ctx: NftCtx) -> i32; + type NftCtxGetOutputBuffer = unsafe extern "C" fn(ctx: NftCtx) -> *const std::ffi::c_char; + type NftCtxBufferError = unsafe extern "C" fn(ctx: NftCtx) -> i32; + type NftCtxGetErrorBuffer = unsafe extern "C" fn(ctx: NftCtx) -> *const std::ffi::c_char; + + struct Lib { + _lib: libloading::Library, + ctx_new: NftCtxNew, + ctx_free: NftCtxFree, + run_cmd: NftRunCmdFromBuffer, + buffer_output: NftCtxBufferOutput, + _get_output: NftCtxGetOutputBuffer, + buffer_error: NftCtxBufferError, + get_error: NftCtxGetErrorBuffer, + } + + // SAFETY: the function pointers are from a shared library that is + // loaded once and stays loaded. The functions themselves are thread-safe + // when called with distinct nft_ctx pointers. + unsafe impl Send for Lib {} + unsafe impl Sync for Lib {} + + static LIB: OnceLock> = OnceLock::new(); + + fn load() -> Option<&'static Lib> { + LIB.get_or_init(|| { + let lib = match unsafe { libloading::Library::new("libnftables.so.1") } { + Ok(lib) => { + tracing::debug!("libnftables.so.1 loaded, using in-process nft"); + lib + } + Err(e) => { + tracing::debug!(error = %e, "libnftables.so.1 not available, falling back to nft binary"); + return None; + } + }; + unsafe { + let ctx_new = *lib.get::(b"nft_ctx_new\0").ok()?; + let ctx_free = *lib.get::(b"nft_ctx_free\0").ok()?; + let run_cmd = *lib + .get::(b"nft_run_cmd_from_buffer\0") + .ok()?; + let buffer_output = *lib + .get::(b"nft_ctx_buffer_output\0") + .ok()?; + let _get_output = *lib + .get::(b"nft_ctx_get_output_buffer\0") + .ok()?; + let buffer_error = *lib + .get::(b"nft_ctx_buffer_error\0") + .ok()?; + let get_error = *lib + .get::(b"nft_ctx_get_error_buffer\0") + .ok()?; + Some(Lib { + _lib: lib, + ctx_new, + ctx_free, + run_cmd, + buffer_output, + _get_output, + buffer_error, + get_error, + }) + } + }) + .as_ref() + } + + /// Applies nftables rules via libnftables in-process. Returns None if + /// the library is not available. + pub(super) fn try_apply(rules: &str) -> Option> { + let lib = load()?; + let ctx = unsafe { (lib.ctx_new)(0) }; + if ctx.is_null() { + return Some(Err(anyhow!("nft_ctx_new returned null"))); + } + // Buffer output and errors so they don't go to stdout/stderr. + unsafe { + (lib.buffer_output)(ctx); + (lib.buffer_error)(ctx); + } + let c_rules = match CString::new(rules) { + Ok(c) => c, + Err(e) => { + unsafe { (lib.ctx_free)(ctx) }; + return Some(Err(anyhow!("nft rules contain null byte: {e}"))); + } + }; + let ret = unsafe { (lib.run_cmd)(ctx, c_rules.as_ptr()) }; + let result = if ret == 0 { + Ok(()) + } else { + let err = unsafe { + let ptr = (lib.get_error)(ctx); + if ptr.is_null() { + String::from("(no error message)") + } else { + CStr::from_ptr(ptr).to_string_lossy().into_owned() + } + }; + Err(anyhow!("nft apply failed: {}", err.trim())) + }; + unsafe { (lib.ctx_free)(ctx) }; + Some(result) + } +} + /// Applies nftables rules (assumes caller is already in the target namespace). -async fn run_nft(rules: &str) -> Result<()> { - use tokio::io::AsyncWriteExt; +/// Tries libnftables in-process first (~50us), falls back to spawning +/// the nft binary (~5ms) if the library is not available. +fn run_nft_sync(rules: &str) -> Result<()> { + if let Some(result) = libnft::try_apply(rules) { + tracing::trace!("nft: applied via libnftables (in-process)"); + return result; + } + tracing::trace!("nft: applying via nft binary (fallback)"); + run_nft_cmd(rules) +} + +/// Fallback: apply rules by spawning the nft binary. +fn run_nft_cmd(rules: &str) -> Result<()> { + use std::io::Write; let nft = if std::path::Path::new("/usr/sbin/nft").exists() { "/usr/sbin/nft" } else { "nft" }; - let mut child = tokio::process::Command::new(nft) + let mut child = std::process::Command::new(nft) .args(["-f", "-"]) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::inherit()) + .stderr(std::process::Stdio::piped()) .spawn() .context("spawn nft")?; child .stdin .take() - .unwrap() + .expect("stdin") .write_all(rules.as_bytes()) - .await .context("write nft stdin")?; - let st = child.wait().await.context("wait nft")?; - if st.success() { + let output = child.wait_with_output().context("wait nft")?; + if output.status.success() { Ok(()) } else { - Err(anyhow!("nft apply failed")) + let stderr = String::from_utf8_lossy(&output.stderr); + Err(anyhow!("nft apply failed: {}", stderr.trim())) } } -/// Applies nftables rules inside `ns` on the namespace's async worker. +/// Applies nftables rules inside `ns` on the namespace's sync worker. pub(crate) async fn run_nft_in(netns: &netns::NetnsManager, ns: &str, rules: &str) -> Result<()> { trace!(ns = %ns, rules = %rules, "nft: apply rules"); let rules = rules.to_string(); - let rt = netns.rt_handle_for(ns)?; - rt.spawn(async move { run_nft(&rules).await }) - .await - .context("nft task panicked")? + netns.run_closure_in(ns, move || run_nft_sync(&rules)) } /// Generates nftables rules for a [`NatConfig`].