Skip to content

Commit 1d3e998

Browse files
Frandoclaude
andcommitted
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) <noreply@anthropic.com>
1 parent b3c9cf3 commit 1d3e998

3 files changed

Lines changed: 147 additions & 14 deletions

File tree

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

patchbay/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ derive_more = { version = "2.1.1", features = ["debug", "display"] }
1919
futures = "0.3"
2020
ipnet = { version = "2.11", features = ["serde"] }
2121
libc = "0.2"
22+
libloading = "0.8"
2223
nix = { version = "0.30", features = ["sched", "mount", "fs", "signal", "process", "user", "ioctl"] }
2324
rtnetlink = "0.20"
2425
serde = { version = "1", features = ["derive"] }

patchbay/src/nft.rs

Lines changed: 135 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,44 +11,165 @@ use crate::{
1111
NatConfig, NatFiltering, NatMapping, NatV6Mode,
1212
};
1313

14+
// ── libnftables dlopen fast path ─────────────────────────────────────
15+
16+
mod libnft {
17+
use std::{
18+
ffi::{CStr, CString},
19+
sync::OnceLock,
20+
};
21+
22+
use anyhow::{anyhow, Result};
23+
24+
type NftCtx = *mut std::ffi::c_void;
25+
type NftCtxNew = unsafe extern "C" fn(flags: u32) -> NftCtx;
26+
type NftCtxFree = unsafe extern "C" fn(ctx: NftCtx);
27+
type NftRunCmdFromBuffer =
28+
unsafe extern "C" fn(ctx: NftCtx, buf: *const std::ffi::c_char) -> i32;
29+
type NftCtxBufferOutput = unsafe extern "C" fn(ctx: NftCtx) -> i32;
30+
type NftCtxGetOutputBuffer = unsafe extern "C" fn(ctx: NftCtx) -> *const std::ffi::c_char;
31+
type NftCtxBufferError = unsafe extern "C" fn(ctx: NftCtx) -> i32;
32+
type NftCtxGetErrorBuffer = unsafe extern "C" fn(ctx: NftCtx) -> *const std::ffi::c_char;
33+
34+
struct Lib {
35+
_lib: libloading::Library,
36+
ctx_new: NftCtxNew,
37+
ctx_free: NftCtxFree,
38+
run_cmd: NftRunCmdFromBuffer,
39+
buffer_output: NftCtxBufferOutput,
40+
_get_output: NftCtxGetOutputBuffer,
41+
buffer_error: NftCtxBufferError,
42+
get_error: NftCtxGetErrorBuffer,
43+
}
44+
45+
// SAFETY: the function pointers are from a shared library that is
46+
// loaded once and stays loaded. The functions themselves are thread-safe
47+
// when called with distinct nft_ctx pointers.
48+
unsafe impl Send for Lib {}
49+
unsafe impl Sync for Lib {}
50+
51+
static LIB: OnceLock<Option<Lib>> = OnceLock::new();
52+
53+
fn load() -> Option<&'static Lib> {
54+
LIB.get_or_init(|| {
55+
let lib = unsafe { libloading::Library::new("libnftables.so.1") }.ok()?;
56+
unsafe {
57+
let ctx_new = *lib.get::<NftCtxNew>(b"nft_ctx_new\0").ok()?;
58+
let ctx_free = *lib.get::<NftCtxFree>(b"nft_ctx_free\0").ok()?;
59+
let run_cmd = *lib
60+
.get::<NftRunCmdFromBuffer>(b"nft_run_cmd_from_buffer\0")
61+
.ok()?;
62+
let buffer_output = *lib
63+
.get::<NftCtxBufferOutput>(b"nft_ctx_buffer_output\0")
64+
.ok()?;
65+
let _get_output = *lib
66+
.get::<NftCtxGetOutputBuffer>(b"nft_ctx_get_output_buffer\0")
67+
.ok()?;
68+
let buffer_error = *lib
69+
.get::<NftCtxBufferError>(b"nft_ctx_buffer_error\0")
70+
.ok()?;
71+
let get_error = *lib
72+
.get::<NftCtxGetErrorBuffer>(b"nft_ctx_get_error_buffer\0")
73+
.ok()?;
74+
Some(Lib {
75+
_lib: lib,
76+
ctx_new,
77+
ctx_free,
78+
run_cmd,
79+
buffer_output,
80+
_get_output,
81+
buffer_error,
82+
get_error,
83+
})
84+
}
85+
})
86+
.as_ref()
87+
}
88+
89+
/// Applies nftables rules via libnftables in-process. Returns None if
90+
/// the library is not available.
91+
pub(super) fn try_apply(rules: &str) -> Option<Result<()>> {
92+
let lib = load()?;
93+
let ctx = unsafe { (lib.ctx_new)(0) };
94+
if ctx.is_null() {
95+
return Some(Err(anyhow!("nft_ctx_new returned null")));
96+
}
97+
// Buffer output and errors so they don't go to stdout/stderr.
98+
unsafe {
99+
(lib.buffer_output)(ctx);
100+
(lib.buffer_error)(ctx);
101+
}
102+
let c_rules = match CString::new(rules) {
103+
Ok(c) => c,
104+
Err(e) => {
105+
unsafe { (lib.ctx_free)(ctx) };
106+
return Some(Err(anyhow!("nft rules contain null byte: {e}")));
107+
}
108+
};
109+
let ret = unsafe { (lib.run_cmd)(ctx, c_rules.as_ptr()) };
110+
let result = if ret == 0 {
111+
Ok(())
112+
} else {
113+
let err = unsafe {
114+
let ptr = (lib.get_error)(ctx);
115+
if ptr.is_null() {
116+
String::from("(no error message)")
117+
} else {
118+
CStr::from_ptr(ptr).to_string_lossy().into_owned()
119+
}
120+
};
121+
Err(anyhow!("nft apply failed: {}", err.trim()))
122+
};
123+
unsafe { (lib.ctx_free)(ctx) };
124+
Some(result)
125+
}
126+
}
127+
14128
/// Applies nftables rules (assumes caller is already in the target namespace).
15-
async fn run_nft(rules: &str) -> Result<()> {
16-
use tokio::io::AsyncWriteExt;
129+
/// Tries libnftables in-process first (~50us), falls back to spawning
130+
/// the nft binary (~5ms) if the library is not available.
131+
fn run_nft_sync(rules: &str) -> Result<()> {
132+
if let Some(result) = libnft::try_apply(rules) {
133+
return result;
134+
}
135+
run_nft_cmd(rules)
136+
}
137+
138+
/// Fallback: apply rules by spawning the nft binary.
139+
fn run_nft_cmd(rules: &str) -> Result<()> {
140+
use std::io::Write;
17141
let nft = if std::path::Path::new("/usr/sbin/nft").exists() {
18142
"/usr/sbin/nft"
19143
} else {
20144
"nft"
21145
};
22-
let mut child = tokio::process::Command::new(nft)
146+
let mut child = std::process::Command::new(nft)
23147
.args(["-f", "-"])
24148
.stdin(std::process::Stdio::piped())
25149
.stdout(std::process::Stdio::null())
26-
.stderr(std::process::Stdio::inherit())
150+
.stderr(std::process::Stdio::piped())
27151
.spawn()
28152
.context("spawn nft")?;
29153
child
30154
.stdin
31155
.take()
32-
.unwrap()
156+
.expect("stdin")
33157
.write_all(rules.as_bytes())
34-
.await
35158
.context("write nft stdin")?;
36-
let st = child.wait().await.context("wait nft")?;
37-
if st.success() {
159+
let output = child.wait_with_output().context("wait nft")?;
160+
if output.status.success() {
38161
Ok(())
39162
} else {
40-
Err(anyhow!("nft apply failed"))
163+
let stderr = String::from_utf8_lossy(&output.stderr);
164+
Err(anyhow!("nft apply failed: {}", stderr.trim()))
41165
}
42166
}
43167

44-
/// Applies nftables rules inside `ns` on the namespace's async worker.
168+
/// Applies nftables rules inside `ns` on the namespace's sync worker.
45169
pub(crate) async fn run_nft_in(netns: &netns::NetnsManager, ns: &str, rules: &str) -> Result<()> {
46170
debug!(ns = %ns, rules = %rules, "nft: apply rules");
47171
let rules = rules.to_string();
48-
let rt = netns.rt_handle_for(ns)?;
49-
rt.spawn(async move { run_nft(&rules).await })
50-
.await
51-
.context("nft task panicked")?
172+
netns.run_closure_in(ns, move || run_nft_sync(&rules))
52173
}
53174

54175
/// Generates nftables rules for a [`NatConfig`].

0 commit comments

Comments
 (0)