From 6b172c73a7015af37e13b212a8f125255739c05e Mon Sep 17 00:00:00 2001 From: Jason Ish Date: Mon, 4 May 2026 15:53:46 -0600 Subject: [PATCH 1/5] rust: bindgen flow lifecycle callbacks Ticket: #8446 --- rust/sys/src/sys.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++ src/bindgen.h | 1 + src/flow-callbacks.h | 3 ++- 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/rust/sys/src/sys.rs b/rust/sys/src/sys.rs index 740bc56e23b7..daa3cf75b1eb 100644 --- a/rust/sys/src/sys.rs +++ b/rust/sys/src/sys.rs @@ -1791,6 +1791,58 @@ extern "C" { extern "C" { pub fn SCFlowGetAppProtocol(f: *const Flow) -> AppProto; } +#[doc = " \\brief Function type for flow initialization callbacks.\n\n Once registered with SCFlowRegisterInitCallback, this function will\n be called every time a flow is initialized, or in other words,\n every time Suricata picks up a flow.\n\n \\param tv The ThreadVars data structure for the thread creating the\n flow.\n \\param f The newly initialized flow.\n \\param p The packet related to creating the new flow.\n \\param user The user data provided during callback registration."] +pub type SCFlowInitCallbackFn = ::std::option::Option< + unsafe extern "C" fn( + tv: *mut ThreadVars, + f: *mut Flow, + p: *const Packet, + user: *mut ::std::os::raw::c_void, + ), +>; +extern "C" { + #[doc = " \\brief Register a flow init callback.\n\n Register a user provided function to be called every time a flow is\n initialized for use.\n\n \\param fn Pointer to function to be called\n \\param user Additional user data to be passed to callback\n\n \\returns true if callback was registered, otherwise false if the\n callback could not be registered due to memory allocation error."] + pub fn SCFlowRegisterInitCallback( + fn_: SCFlowInitCallbackFn, user: *mut ::std::os::raw::c_void, + ) -> bool; +} +extern "C" { + #[doc = " \\internal\n\n Run all registered flow init callbacks."] + pub fn SCFlowRunInitCallbacks(tv: *mut ThreadVars, f: *mut Flow, p: *const Packet); +} +#[doc = " \\brief Function type for flow update callbacks.\n\n Once registered with SCFlowRegisterUpdateCallback, this function\n will be called every time a flow is updated by a packet (basically\n everytime a packet is seen on a flow).\n\n \\param tv The ThreadVars data structure for the thread updating the\n flow.\n \\param f The flow being updated.\n \\param p The packet responsible for the flow update.\n \\param user The user data provided during callback registration."] +pub type SCFlowUpdateCallbackFn = ::std::option::Option< + unsafe extern "C" fn( + tv: *mut ThreadVars, + f: *mut Flow, + p: *mut Packet, + user: *mut ::std::os::raw::c_void, + ), +>; +extern "C" { + #[doc = " \\brief Register a flow update callback.\n\n Register a user provided function to be called everytime a flow is\n updated.\n\n \\param fn Pointer to function to be called\n \\param user Additional user data to be passed to callback\n\n \\returns true if callback was registered, otherwise false if the\n callback could not be registered due to memory allocation error."] + pub fn SCFlowRegisterUpdateCallback( + fn_: SCFlowUpdateCallbackFn, user: *mut ::std::os::raw::c_void, + ) -> bool; +} +extern "C" { + #[doc = " \\internal\n\n Run all registered flow update callbacks."] + pub fn SCFlowRunUpdateCallbacks(tv: *mut ThreadVars, f: *mut Flow, p: *mut Packet); +} +#[doc = " \\brief Function type for flow finish callbacks.\n\n Once registered with SCFlowRegisterFinshCallback, this function\n will be called when Suricata is done with a flow.\n\n \\param tv The ThreadVars data structure for the thread finishing\n the flow.\n \\param f The flow being finshed.\n \\param user The user data provided during callback registration."] +pub type SCFlowFinishCallbackFn = ::std::option::Option< + unsafe extern "C" fn(tv: *mut ThreadVars, f: *mut Flow, user: *mut ::std::os::raw::c_void), +>; +extern "C" { + #[doc = " \\brief Register a flow init callback.\n\n Register a user provided function to be called every time a flow is\n finished.\n\n \\param fn Pointer to function to be called\n \\param user Additional user data to be passed to callback\n\n \\returns true if callback was registered, otherwise false if the\n callback could not be registered due to memory allocation error."] + pub fn SCFlowRegisterFinishCallback( + fn_: SCFlowFinishCallbackFn, user: *mut ::std::os::raw::c_void, + ) -> bool; +} +extern "C" { + #[doc = " \\internal\n\n Run all registered flow init callbacks."] + pub fn SCFlowRunFinishCallbacks(tv: *mut ThreadVars, f: *mut Flow); +} extern "C" { pub fn SCSRepCatGetByShortname(shortname: *const ::std::os::raw::c_char) -> u8; } diff --git a/src/bindgen.h b/src/bindgen.h index e321dec78d32..6cbc4881d4b1 100644 --- a/src/bindgen.h +++ b/src/bindgen.h @@ -62,6 +62,7 @@ #include "util-spm-bs.h" #include "flow-bindgen.h" +#include "flow-callbacks.h" #include "reputation.h" #include "feature.h" diff --git a/src/flow-callbacks.h b/src/flow-callbacks.h index 4c694807753f..44d2d6003c33 100644 --- a/src/flow-callbacks.h +++ b/src/flow-callbacks.h @@ -18,8 +18,9 @@ #ifndef SURICATA_FLOW_CALLBACKS_H #define SURICATA_FLOW_CALLBACKS_H -#include "suricata-common.h" +#ifndef SURICATA_BINDGEN_H #include "flow.h" +#endif /** \brief Function type for flow initialization callbacks. * From 5512abc8702cf6defc9b740f272b849e978768cc Mon Sep 17 00:00:00 2001 From: Jason Ish Date: Mon, 4 May 2026 16:18:14 -0600 Subject: [PATCH 2/5] rust/ffi: add flow lifecycle callback wrappers Provide Rust friendly callback registrations for flow init, update and finish events. These callbacks are implemented as Rust closures. Ticket: #8446 --- rust/ffi/src/flow.rs | 134 +++++++++++++++++++++++++++++++++++++++++++ rust/ffi/src/lib.rs | 1 + 2 files changed, 135 insertions(+) create mode 100644 rust/ffi/src/flow.rs diff --git a/rust/ffi/src/flow.rs b/rust/ffi/src/flow.rs new file mode 100644 index 000000000000..91f22eca3cb2 --- /dev/null +++ b/rust/ffi/src/flow.rs @@ -0,0 +1,134 @@ +/* Copyright (C) 2026 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use std::os::raw::c_void; + +use suricata_sys::sys::{Flow, Packet, ThreadVars}; +use suricata_sys::sys::{ + SCFlowRegisterFinishCallback, SCFlowRegisterInitCallback, SCFlowRegisterUpdateCallback, +}; + +/// Register a flow initialization callback. +/// +/// The callback is invoked whenever Suricata initializes a flow. It receives: +/// - `tv`: the `ThreadVars` for the thread creating the flow +/// - `f`: the newly initialized `Flow` +/// - `p`: the packet related to creating the flow +/// +/// # Safety +/// +/// The callback receives raw pointers from Suricata. These pointers are only +/// valid for the duration of the callback invocation and must not be stored. +/// +/// The callback must not panic. +pub fn register_init_callback(callback: F) -> Result<(), &'static str> +where + F: Fn(*mut ThreadVars, *mut Flow, *const Packet) + Send + Sync + 'static, +{ + let user = Box::into_raw(Box::new(callback)) as *mut c_void; + if unsafe { SCFlowRegisterInitCallback(Some(init_callback_wrapper::), user) } { + Ok(()) + } else { + unsafe { + drop(Box::from_raw(user as *mut F)); + } + Err("Failed to register flow init callback") + } +} + +/// Register a flow update callback. +/// +/// The callback is invoked whenever Suricata updates a flow with a packet. It +/// receives: +/// - `tv`: the `ThreadVars` for the thread updating the flow +/// - `f`: the flow being updated +/// - `p`: the packet responsible for the flow update +/// +/// # Safety +/// +/// The callback receives raw pointers from Suricata. These pointers are only +/// valid for the duration of the callback invocation and must not be stored. +/// +/// The callback must not panic. +pub fn register_update_callback(callback: F) -> Result<(), &'static str> +where + F: Fn(*mut ThreadVars, *mut Flow, *mut Packet) + Send + Sync + 'static, +{ + let user = Box::into_raw(Box::new(callback)) as *mut c_void; + if unsafe { SCFlowRegisterUpdateCallback(Some(update_callback_wrapper::), user) } { + Ok(()) + } else { + unsafe { + drop(Box::from_raw(user as *mut F)); + } + Err("Failed to register flow update callback") + } +} + +/// Register a flow finish callback. +/// +/// The callback is invoked when Suricata is finished with a flow. It receives: +/// - `tv`: the `ThreadVars` for the thread finishing the flow +/// - `f`: the flow being finished +/// +/// # Safety +/// +/// The callback receives raw pointers from Suricata. These pointers are only +/// valid for the duration of the callback invocation and must not be stored. +/// +/// The callback must not panic. +pub fn register_finish_callback(callback: F) -> Result<(), &'static str> +where + F: Fn(*mut ThreadVars, *mut Flow) + Send + Sync + 'static, +{ + let user = Box::into_raw(Box::new(callback)) as *mut c_void; + if unsafe { SCFlowRegisterFinishCallback(Some(finish_callback_wrapper::), user) } { + Ok(()) + } else { + unsafe { + drop(Box::from_raw(user as *mut F)); + } + Err("Failed to register flow finish callback") + } +} + +unsafe extern "C" fn init_callback_wrapper( + tv: *mut ThreadVars, f: *mut Flow, p: *const Packet, user: *mut c_void, +) where + F: Fn(*mut ThreadVars, *mut Flow, *const Packet) + Send + Sync + 'static, +{ + let callback = &*(user as *const F); + callback(tv, f, p); +} + +unsafe extern "C" fn update_callback_wrapper( + tv: *mut ThreadVars, f: *mut Flow, p: *mut Packet, user: *mut c_void, +) where + F: Fn(*mut ThreadVars, *mut Flow, *mut Packet) + Send + Sync + 'static, +{ + let callback = &*(user as *const F); + callback(tv, f, p); +} + +unsafe extern "C" fn finish_callback_wrapper( + tv: *mut ThreadVars, f: *mut Flow, user: *mut c_void, +) where + F: Fn(*mut ThreadVars, *mut Flow) + Send + Sync + 'static, +{ + let callback = &*(user as *const F); + callback(tv, f); +} diff --git a/rust/ffi/src/lib.rs b/rust/ffi/src/lib.rs index 8586927b324b..2be487dfb990 100644 --- a/rust/ffi/src/lib.rs +++ b/rust/ffi/src/lib.rs @@ -20,6 +20,7 @@ pub mod conf; pub mod debug; pub mod detect; pub mod eve; +pub mod flow; pub mod jsonbuilder; pub mod plugin; From 88996400940a96c6c9314eb18ce7fdc8398ac684 Mon Sep 17 00:00:00 2001 From: Jason Ish Date: Mon, 4 May 2026 16:28:01 -0600 Subject: [PATCH 3/5] doc: document flow life cycle callback API Document for C and Rust, as the C documentation was missing. Ticket: #8446 --- .../extending/flow-lifecycle-callbacks.rst | 131 ++++++++++++++++++ doc/userguide/devguide/extending/index.rst | 1 + 2 files changed, 132 insertions(+) create mode 100644 doc/userguide/devguide/extending/flow-lifecycle-callbacks.rst diff --git a/doc/userguide/devguide/extending/flow-lifecycle-callbacks.rst b/doc/userguide/devguide/extending/flow-lifecycle-callbacks.rst new file mode 100644 index 000000000000..ff1195ce3ae3 --- /dev/null +++ b/doc/userguide/devguide/extending/flow-lifecycle-callbacks.rst @@ -0,0 +1,131 @@ +Flow Life Cycle Callbacks +######################### + +Flow lifecycle callbacks let plugins and library users observe when +Suricata initializes a flow, updates a flow with a packet, and finishes +with a flow. + +These callbacks are useful for maintaining plugin state that follows the +lifetime of a Suricata flow. For example, a plugin can allocate per-flow +state from the init callback, update it as packets are seen, and perform +final accounting from the finish callback. + +C API +***** + +Flow Init Callback +================== + +The init callback is called when Suricata initializes a flow. + +.. literalinclude:: ../../../../src/flow-callbacks.h + :language: c + :start-at: /** \brief Function type for flow initialization callbacks. + :end-at: typedef void (*SCFlowInitCallbackFn)(ThreadVars *tv, Flow *f, const Packet *p, void *user); + +Register an init callback with ``SCFlowRegisterInitCallback``. + +.. literalinclude:: ../../../../src/flow-callbacks.h + :language: c + :start-at: /** \brief Register a flow init callback. + :end-at: bool SCFlowRegisterInitCallback(SCFlowInitCallbackFn fn, void *user); + +Flow Update Callback +==================== + +The update callback is called when Suricata updates a flow with a packet. + +.. literalinclude:: ../../../../src/flow-callbacks.h + :language: c + :start-at: /** \brief Function type for flow update callbacks. + :end-at: typedef void (*SCFlowUpdateCallbackFn)(ThreadVars *tv, Flow *f, Packet *p, void *user); + +Register an update callback with ``SCFlowRegisterUpdateCallback``. + +.. literalinclude:: ../../../../src/flow-callbacks.h + :language: c + :start-at: /** \brief Register a flow update callback. + :end-at: bool SCFlowRegisterUpdateCallback(SCFlowUpdateCallbackFn fn, void *user); + +Flow Finish Callback +==================== + +The finish callback is called when Suricata is done with a flow. + +.. literalinclude:: ../../../../src/flow-callbacks.h + :language: c + :start-at: /** \brief Function type for flow finish callbacks. + :end-at: typedef void (*SCFlowFinishCallbackFn)(ThreadVars *tv, Flow *f, void *user); + +Register a finish callback with ``SCFlowRegisterFinishCallback``. + +.. code-block:: c + + bool SCFlowRegisterFinishCallback(SCFlowFinishCallbackFn fn, void *user); + +Example +======= + +.. code-block:: c + + static void ExampleFlowInit(ThreadVars *tv, Flow *f, const Packet *p, void *user) + { + SCLogNotice("flow initialized: %p", f); + } + + static void ExampleFlowUpdate(ThreadVars *tv, Flow *f, Packet *p, void *user) + { + SCLogNotice("flow updated: %p packet: %p", f, p); + } + + static void ExampleFlowFinish(ThreadVars *tv, Flow *f, void *user) + { + SCLogNotice("flow finished: %p", f); + } + + static void ExampleInit(void) + { + SCFlowRegisterInitCallback(ExampleFlowInit, NULL); + SCFlowRegisterUpdateCallback(ExampleFlowUpdate, NULL); + SCFlowRegisterFinishCallback(ExampleFlowFinish, NULL); + } + +Rust API +******** + +In Rust, use the ``suricata_ffi::flow`` module: + +- ``flow::register_init_callback`` +- ``flow::register_update_callback`` +- ``flow::register_finish_callback`` + +The Rust wrappers register closures or function items and return +``Result<(), &'static str>``. + +.. code-block:: rust + + use suricata_ffi::flow::{self, Flow, Packet, ThreadVars}; + use suricata_ffi::SCLogNotice; + + fn flow_init(_tv: *mut ThreadVars, f: *mut Flow, _p: *const Packet) { + SCLogNotice!("flow initialized: {:p}", f); + } + + fn flow_update(_tv: *mut ThreadVars, f: *mut Flow, p: *mut Packet) { + SCLogNotice!("flow updated: {:p} packet: {:p}", f, p); + } + + fn flow_finish(_tv: *mut ThreadVars, f: *mut Flow) { + SCLogNotice!("flow finished: {:p}", f); + } + + fn register_flow_callbacks() -> Result<(), &'static str> { + flow::register_init_callback(flow_init)?; + flow::register_update_callback(flow_update)?; + flow::register_finish_callback(flow_finish)?; + Ok(()) + } + +The raw pointers passed into callbacks are only valid for the duration +of the callback invocation and must not be stored. Rust callbacks must +not panic. diff --git a/doc/userguide/devguide/extending/index.rst b/doc/userguide/devguide/extending/index.rst index 043b37259483..5c755ea9f8a4 100644 --- a/doc/userguide/devguide/extending/index.rst +++ b/doc/userguide/devguide/extending/index.rst @@ -12,3 +12,4 @@ Extending Suricata output/index.rst output/eve-filetypes.rst output/eve-hooks.rst + flow-lifecycle-callbacks.rst From babf13bfc9c4c7bf8d058a48cfccd9c3d6a55ef5 Mon Sep 17 00:00:00 2001 From: Jason Ish Date: Mon, 4 May 2026 16:29:33 -0600 Subject: [PATCH 4/5] examples: add flow callbacks to rust plugin example Ticket: #8446 --- examples/plugins/rust/src/mod.rs | 41 ++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/examples/plugins/rust/src/mod.rs b/examples/plugins/rust/src/mod.rs index 8f77b10bed94..e71b380f7d85 100644 --- a/examples/plugins/rust/src/mod.rs +++ b/examples/plugins/rust/src/mod.rs @@ -1,26 +1,43 @@ use std::ptr::null_mut; -use suricata_ffi::eve::{self, Flow, Packet, SCJsonBuilder, ThreadVars}; +use suricata_ffi::eve::{self, SCJsonBuilder}; +use suricata_ffi::flow; use suricata_ffi::jsonbuilder::JsonBuilder; use suricata_ffi::{SCLogError, SCLogNotice}; -use suricata_sys::sys::{SCEveRegisterCallback, SCPlugin}; +use suricata_sys::sys::{Flow, Packet, SCEveRegisterCallback, SCPlugin, ThreadVars}; unsafe extern "C" fn init() { suricata_ffi::plugin::init(); SCLogNotice!("Initializing rust example plugin"); - if let Err(err) = register() { - SCLogError!("Failed to register rust example EVE callback: {}", err); + if let Err(err) = register_eve_callbacks() { + SCLogError!("Failed to register rust example EVE callbacks: {}", err); + } + if let Err(err) = register_flow_callbacks() { + SCLogError!("Failed to register rust example flow callbacks: {}", err); } } pub fn register() -> Result<(), &'static str> { + register_eve_callbacks()?; + register_flow_callbacks()?; + Ok(()) +} + +pub fn register_eve_callbacks() -> Result<(), &'static str> { if !unsafe { SCEveRegisterCallback(Some(log_eve_raw), null_mut()) } { return Err("Failed to register raw EVE callback"); } eve::register_callback(log_eve_wrapped) } +pub fn register_flow_callbacks() -> Result<(), &'static str> { + flow::register_init_callback(log_flow_init)?; + flow::register_update_callback(log_flow_update)?; + flow::register_finish_callback(log_flow_finish)?; + Ok(()) +} + unsafe extern "C" fn log_eve_raw( _tv: *mut ThreadVars, _p: *const Packet, @@ -47,6 +64,22 @@ fn log_eve_wrapped( Ok(()) } +fn log_flow_init(_tv: *mut ThreadVars, _f: *mut Flow, _p: *const Packet) { + SCLogNotice!("rust example flow init callback: flow={:p}", _f); +} + +fn log_flow_update(_tv: *mut ThreadVars, _f: *mut Flow, _p: *mut Packet) { + SCLogNotice!( + "rust example flow update callback: flow={:p}, packet={:p}", + _f, + _p + ); +} + +fn log_flow_finish(_tv: *mut ThreadVars, _f: *mut Flow) { + SCLogNotice!("rust example flow finish callback: flow={:p}", _f); +} + #[no_mangle] extern "C" fn SCPluginRegister() -> *mut SCPlugin { suricata_ffi::plugin::Plugin { From b8053a9d6e0d9623c2607eb2106bb28469f5be0c Mon Sep 17 00:00:00 2001 From: Jason Ish Date: Mon, 4 May 2026 16:34:27 -0600 Subject: [PATCH 5/5] github-ci: check formatting and clippy on example rust plugin --- .github/workflows/builds.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 8b2fd2c9174c..b1f84d5147bb 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -92,6 +92,7 @@ jobs: automake \ cargo-vendor \ cbindgen \ + clippy \ diffutils \ numactl-devel \ dpdk-devel \ @@ -120,6 +121,7 @@ jobs: python3-devel \ python3-sphinx \ python3-yaml \ + rustfmt \ rust-toolset \ sudo \ which \ @@ -212,9 +214,12 @@ jobs: working-directory: examples/plugins/c-custom-loggers run: make clean all - - name: Build Rust example plugin + - name: Check and build Rust example plugin working-directory: examples/plugins/rust - run: cargo build + run: | + cargo fmt --check + cargo clippy -- -D warnings + cargo build - name: Install Suricata and library run: make install install-headers install-library