[pve-devel] [PATCH proxmox-firewall v2 25/39] nftables: add libnftables bindings

Stefan Hanreich s.hanreich at proxmox.com
Wed Apr 17 15:53:50 CEST 2024


Add a thin wrapper around libnftables, which can be used to run
commands defined by the rust types.

Reviewed-by: Lukas Wagner <l.wagner at proxmox.com>
Reviewed-by: Max Carrara <m.carrara at proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller at proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich at proxmox.com>
---
 proxmox-nftables/src/context.rs | 243 ++++++++++++++++++++++++++++++++
 proxmox-nftables/src/error.rs   |  43 ++++++
 proxmox-nftables/src/lib.rs     |   3 +
 3 files changed, 289 insertions(+)
 create mode 100644 proxmox-nftables/src/context.rs
 create mode 100644 proxmox-nftables/src/error.rs

diff --git a/proxmox-nftables/src/context.rs b/proxmox-nftables/src/context.rs
new file mode 100644
index 0000000..9ab51fb
--- /dev/null
+++ b/proxmox-nftables/src/context.rs
@@ -0,0 +1,243 @@
+use std::ffi::CString;
+use std::os::raw::{c_int, c_uint};
+use std::path::Path;
+
+use crate::command::{CommandOutput, Commands};
+use crate::error::NftError;
+
+#[rustfmt::skip]
+pub mod debug {
+    use super::c_uint;
+
+    pub const SCANNER    : c_uint = 0x1;
+    pub const PARSER     : c_uint = 0x2;
+    pub const EVALUATION : c_uint = 0x4;
+    pub const NETLINK    : c_uint = 0x8;
+    pub const MNL        : c_uint = 0x10;
+    pub const PROTO_CTX  : c_uint = 0x20;
+    pub const SEGTREE    : c_uint = 0x40;
+}
+
+#[rustfmt::skip]
+pub mod output {
+    use super::c_uint;
+
+    pub const REVERSEDNS     : c_uint = 1;
+    pub const SERVICE        : c_uint = 1 << 1;
+    pub const STATELESS      : c_uint = 1 << 2;
+    pub const HANDLE         : c_uint = 1 << 3;
+    pub const JSON           : c_uint = 1 << 4;
+    pub const ECHO           : c_uint = 1 << 5;
+    pub const GUID           : c_uint = 1 << 6;
+    pub const NUMERIC_PROTO  : c_uint = 1 << 7;
+    pub const NUMERIC_PRIO   : c_uint = 1 << 8;
+    pub const NUMERIC_SYMBOL : c_uint = 1 << 9;
+    pub const NUMERIC_TIME   : c_uint = 1 << 10;
+    pub const TERSE          : c_uint = 1 << 11;
+
+    pub const NUMERIC_ALL    : c_uint = NUMERIC_PROTO | NUMERIC_PRIO | NUMERIC_SYMBOL;
+}
+
+#[link(name = "nftables")]
+extern "C" {
+    fn nft_ctx_new(flags: u32) -> RawNftCtx;
+    fn nft_ctx_free(ctx: RawNftCtx);
+
+    //fn nft_ctx_get_dry_run(ctx: RawNftCtx) -> bool;
+    fn nft_ctx_set_dry_run(ctx: RawNftCtx, dry: bool);
+
+    fn nft_ctx_output_get_flags(ctx: RawNftCtx) -> c_uint;
+    fn nft_ctx_output_set_flags(ctx: RawNftCtx, flags: c_uint);
+
+    // fn nft_ctx_output_get_debug(ctx: RawNftCtx) -> c_uint;
+    fn nft_ctx_output_set_debug(ctx: RawNftCtx, mask: c_uint);
+
+    //fn nft_ctx_set_output(ctx: RawNftCtx, file: RawCFile) -> RawCFile;
+    fn nft_ctx_buffer_output(ctx: RawNftCtx) -> c_int;
+    fn nft_ctx_unbuffer_output(ctx: RawNftCtx) -> c_int;
+    fn nft_ctx_get_output_buffer(ctx: RawNftCtx) -> *const i8;
+
+    fn nft_ctx_buffer_error(ctx: RawNftCtx) -> c_int;
+    fn nft_ctx_unbuffer_error(ctx: RawNftCtx) -> c_int;
+    fn nft_ctx_get_error_buffer(ctx: RawNftCtx) -> *const i8;
+
+    fn nft_run_cmd_from_buffer(ctx: RawNftCtx, buf: *const i8) -> c_int;
+    fn nft_run_cmd_from_filename(ctx: RawNftCtx, filename: *const i8) -> c_int;
+}
+
+#[derive(Clone, Copy)]
+#[repr(transparent)]
+struct RawNftCtx(*mut u8);
+
+pub struct NftCtx(RawNftCtx);
+
+impl Drop for NftCtx {
+    fn drop(&mut self) {
+        if !self.0 .0.is_null() {
+            unsafe {
+                nft_ctx_free(self.0);
+            }
+        }
+    }
+}
+
+impl NftCtx {
+    pub fn new() -> Result<Self, NftError> {
+        let mut this = Self(unsafe { nft_ctx_new(0) });
+
+        if this.0 .0.is_null() {
+            return Err(NftError::msg("failed to instantiate nft context"));
+        }
+
+        this.enable_json();
+
+        Ok(this)
+    }
+
+    fn modify_flags(&mut self, func: impl FnOnce(c_uint) -> c_uint) {
+        unsafe { nft_ctx_output_set_flags(self.0, func(nft_ctx_output_get_flags(self.0))) }
+    }
+
+    pub fn enable_debug(&mut self) {
+        unsafe { nft_ctx_output_set_debug(self.0, debug::PARSER | debug::SCANNER) }
+    }
+
+    pub fn disable_debug(&mut self) {
+        unsafe { nft_ctx_output_set_debug(self.0, 0) }
+    }
+
+    fn enable_json(&mut self) {
+        self.modify_flags(|flags| flags | output::JSON);
+    }
+
+    pub fn set_dry_run(&mut self, on: bool) {
+        unsafe { nft_ctx_set_dry_run(self.0, on) }
+    }
+
+    fn start_output_buffering(&mut self) -> Result<(), NftError> {
+        let rc = unsafe { nft_ctx_buffer_output(self.0) };
+        NftError::expect_zero(rc, || "failed to start output buffering")
+    }
+
+    fn stop_output_buffering(&mut self) {
+        let _ = unsafe { nft_ctx_unbuffer_output(self.0) };
+        // ignore errors
+    }
+
+    fn get_output_buffer(&mut self) -> Result<String, NftError> {
+        let buf = unsafe { nft_ctx_get_output_buffer(self.0) };
+
+        if buf.is_null() {
+            return Err(NftError::msg("failed to get output buffer"));
+        }
+
+        unsafe { std::ffi::CStr::from_ptr(buf) }
+            .to_str()
+            .map_err(NftError::msg)
+            .map(str::to_string)
+    }
+
+    fn start_error_buffering(&mut self) -> Result<(), NftError> {
+        let rc = unsafe { nft_ctx_buffer_error(self.0) };
+        NftError::expect_zero(rc, || "failed to start error buffering")
+    }
+
+    fn stop_error_buffering(&mut self) {
+        let _ = unsafe { nft_ctx_unbuffer_error(self.0) };
+        // ignore errors...
+    }
+
+    fn get_error_buffer(&mut self) -> Result<String, NftError> {
+        let buf = unsafe { nft_ctx_get_error_buffer(self.0) };
+
+        if buf.is_null() {
+            return Err(NftError::msg("failed to get error buffer"));
+        }
+
+        unsafe { std::ffi::CStr::from_ptr(buf) }
+            .to_str()
+            .map_err(NftError::msg)
+            .map(str::to_string)
+    }
+
+    fn start_buffering(&mut self) -> Result<(), NftError> {
+        self.start_output_buffering()?;
+        if let Err(err) = self.start_error_buffering() {
+            self.stop_output_buffering();
+            return Err(err);
+        }
+        Ok(())
+    }
+
+    fn stop_buffering(&mut self) {
+        self.stop_error_buffering();
+        self.stop_output_buffering();
+    }
+
+    fn buffered<F, R, E>(&mut self, func: F) -> Result<R, E>
+    where
+        E: From<NftError>,
+        F: FnOnce(&mut Self) -> Result<R, E>,
+    {
+        self.start_buffering()?;
+        let res = func(self);
+        self.stop_buffering();
+        res
+    }
+
+    fn current_error(&mut self) -> NftError {
+        match self.get_error_buffer() {
+            Ok(msg) => NftError(msg),
+            Err(err) => err,
+        }
+    }
+
+    pub unsafe fn run_buffer(&mut self, buffer: &CString) -> Result<String, NftError> {
+        self.buffered(|this| {
+            let rc = unsafe { nft_run_cmd_from_buffer(this.0, buffer.as_ptr()) };
+
+            if rc != 0 {
+                return Err(this.current_error());
+            }
+
+            this.get_output_buffer()
+        })
+    }
+
+    pub fn run_file<P: AsRef<Path>>(&mut self, filename: P) -> Result<String, NftError> {
+        use std::os::unix::ffi::OsStrExt;
+
+        let filename = CString::new(filename.as_ref().as_os_str().as_bytes())?;
+
+        self.buffered(move |this| {
+            let rc = unsafe { nft_run_cmd_from_filename(this.0, filename.as_ptr()) };
+            if rc != 0 {
+                return Err(this.current_error());
+            }
+            this.get_output_buffer()
+        })
+    }
+
+    pub fn run_nft_commands(&mut self, commands: &str) -> Result<String, NftError> {
+        let string = CString::new(commands).map_err(NftError::msg)?;
+
+        unsafe { self.run_buffer(&string) }
+    }
+
+    pub fn run_commands(&mut self, commands: &Commands) -> Result<Option<CommandOutput>, NftError> {
+        let json =
+            serde_json::to_vec(commands).expect("commands struct can be properly serialized");
+
+        let string = CString::new(json).expect("serialized json does not contain nul bytes");
+
+        let raw_output = unsafe { self.run_buffer(&string)? };
+
+        if raw_output.is_empty() {
+            return Ok(None);
+        }
+
+        serde_json::from_str::<CommandOutput>(&raw_output)
+            .map(Option::from)
+            .map_err(NftError::msg)
+    }
+}
diff --git a/proxmox-nftables/src/error.rs b/proxmox-nftables/src/error.rs
new file mode 100644
index 0000000..efdc13d
--- /dev/null
+++ b/proxmox-nftables/src/error.rs
@@ -0,0 +1,43 @@
+use std::fmt;
+use std::os::raw::c_int;
+
+#[derive(Debug)]
+pub struct NftError(pub(crate) String);
+
+impl std::error::Error for NftError {}
+
+impl fmt::Display for NftError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "nftables error: {}", self.0)
+    }
+}
+
+impl From<serde_json::Error> for NftError {
+    fn from(err: serde_json::Error) -> Self {
+        Self::msg(err)
+    }
+}
+
+impl NftError {
+    pub(crate) fn msg<T: fmt::Display>(msg: T) -> Self {
+        Self(msg.to_string())
+    }
+
+    pub(crate) fn expect_zero<F, T>(rc: c_int, or_else: F) -> Result<(), NftError>
+    where
+        F: FnOnce() -> T,
+        T: fmt::Display,
+    {
+        if rc == 0 {
+            Ok(())
+        } else {
+            Err(Self(or_else().to_string()))
+        }
+    }
+}
+
+impl From<std::ffi::NulError> for NftError {
+    fn from(err: std::ffi::NulError) -> Self {
+        Self::msg(err)
+    }
+}
diff --git a/proxmox-nftables/src/lib.rs b/proxmox-nftables/src/lib.rs
index 60ddb3f..61a6665 100644
--- a/proxmox-nftables/src/lib.rs
+++ b/proxmox-nftables/src/lib.rs
@@ -1,9 +1,12 @@
 pub mod command;
+pub mod context;
+pub mod error;
 pub mod expression;
 pub mod helper;
 pub mod statement;
 pub mod types;
 
 pub use command::Command;
+pub use context::NftCtx;
 pub use expression::Expression;
 pub use statement::Statement;
-- 
2.39.2




More information about the pve-devel mailing list