@@ -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.
45169pub ( 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