diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 1d3170a5e27d..167b80e959fd 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 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 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 { diff --git a/rules/ftp-events.rules b/rules/ftp-events.rules index d32c93f32759..501fe1016555 100644 --- a/rules/ftp-events.rules +++ b/rules/ftp-events.rules @@ -4,3 +4,4 @@ alert ftp any any -> any any (msg:"SURICATA FTP Request command too long"; flow:to_server; app-layer-event:ftp.request_command_too_long; classtype:protocol-command-decode; sid:2232000; rev:1;) alert ftp any any -> any any (msg:"SURICATA FTP Response command too long"; flow:to_client; app-layer-event:ftp.response_command_too_long; classtype:protocol-command-decode; sid:2232001; rev:1;) +alert ftp any any -> any any (msg:"SURICATA FTP too many transactions"; app-layer-event:ftp.too_many_transactions; classtype:protocol-command-decode; sid:2232002; rev:1;) 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; diff --git a/rust/src/dcerpc/detect.rs b/rust/src/dcerpc/detect.rs index bee935516342..f0c31cd3bfb3 100644 --- a/rust/src/dcerpc/detect.rs +++ b/rust/src/dcerpc/detect.rs @@ -80,10 +80,8 @@ pub(crate) enum DCEOpnumData { fn match_backuuid( tx: &DCERPCTransaction, state: &mut DCERPCState, if_data: &mut DCEIfaceData, ) -> c_int { - let mut ret = 0; if !state.interface_uuids.is_empty() { for uuidentry in &state.interface_uuids { - ret = 1; // if any_frag is not enabled, we need to match only against the first fragment if if_data.any_frag == 0 && (uuidentry.flags & DCERPC_UUID_ENTRY_FLAG_FF == 0) { SCLogDebug!("any frag not enabled"); @@ -91,21 +89,23 @@ fn match_backuuid( } // if the uuid has been rejected(uuidentry->result == 1), we skip to the next uuid if !uuidentry.acked || uuidentry.result != 0 { - ret = 0; SCLogDebug!("Skipping to next UUID"); continue; } + let mut same = true; for i in 0..16 { if if_data.if_uuid[i] != uuidentry.uuid[i] { SCLogDebug!("Iface UUID and BINDACK Accepted UUID does not match"); - ret = 0; + same = false; break; } } + if !same { + continue; + } let ctxid = tx.get_req_ctxid(); - ret &= (uuidentry.ctxid == ctxid) as c_int; - if ret == 0 { + if uuidentry.ctxid != ctxid { SCLogDebug!("CTX IDs/UUIDs do not match"); continue; } @@ -113,17 +113,15 @@ fn match_backuuid( if let Some(x) = &if_data.du16 { if !detect_match_uint(x, uuidentry.version) { SCLogDebug!("Interface version did not match"); - ret &= 0; + continue } } - if ret == 1 { - return 1; - } + return 1; } } - return ret; + return 0; } fn parse_iface_data(arg: &str) -> Result { diff --git a/rust/src/ftp/event.rs b/rust/src/ftp/event.rs index d7ee6969d64c..ce25fd2ce12c 100644 --- a/rust/src/ftp/event.rs +++ b/rust/src/ftp/event.rs @@ -25,6 +25,8 @@ pub enum FtpEvent { FtpEventRequestCommandTooLong, #[name("response_command_too_long")] FtpEventResponseCommandTooLong, + #[name("too_many_transactions")] + FtpEventTooManyTransactions, } /// Wrapper around the Rust generic function for get_event_info. 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/app-layer-ftp.c b/src/app-layer-ftp.c index d1eb32e62b22..6b90683eee92 100644 --- a/src/app-layer-ftp.c +++ b/src/app-layer-ftp.c @@ -223,8 +223,17 @@ static FTPTransaction *FTPTransactionCreate(FtpState *state) SCEnter(); FTPTransaction *firsttx = TAILQ_FIRST(&state->tx_list); if (firsttx && state->tx_cnt - firsttx->tx_id > ftp_config_maxtx) { - // FTP does not set events yet... - return NULL; + FTPTransaction *tx_old; + TAILQ_FOREACH (tx_old, &state->tx_list, next) { + if (!tx_old->done) { + tx_old->done = true; + tx_old->tx_data.updated_ts = true; + tx_old->tx_data.updated_tc = true; + SCAppLayerDecoderEventsSetEventRaw( + &tx_old->tx_data.events, FtpEventTooManyTransactions); + break; + } + } } FTPTransaction *tx = FTPCalloc(1, sizeof(*tx)); if (tx == NULL) { 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/detect-engine-analyzer.c b/src/detect-engine-analyzer.c index 4eb5ad4c5609..7b5e265c5f1a 100644 --- a/src/detect-engine-analyzer.c +++ b/src/detect-engine-analyzer.c @@ -506,6 +506,7 @@ void SetupEngineAnalysis(DetectEngineCtx *de_ctx, bool *fp_analysis, bool *rule_ if (ea->analyzer_items) SCFree(ea->analyzer_items); SCFree(ea); + de_ctx->ea = NULL; } } 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. * diff --git a/suricata.yaml.in b/suricata.yaml.in index 859c443903de..964245d2fba4 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -1000,6 +1000,7 @@ app-layer: ftp: enabled: yes # memcap: 64 MiB + # max-tx: 1024 websocket: #enabled: yes # Maximum used payload size, the rest is skipped