From 84f408524e80020a2456b5c48fae2356af6b8629 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Tue, 21 Jan 2025 14:56:08 +0100 Subject: [PATCH 1/9] template: rustfmt and use generic logger callback prototype with later cast and do some other small modifications so that the plugin has less diff --- rust/src/applayertemplate/detect.rs | 2 +- rust/src/applayertemplate/logger.rs | 3 ++- rust/src/applayertemplate/mod.rs | 2 +- rust/src/applayertemplate/template.rs | 19 ++++++++++--------- scripts/setup-app-layer.py | 2 ++ src/output.c | 3 ++- 6 files changed, 18 insertions(+), 13 deletions(-) diff --git a/rust/src/applayertemplate/detect.rs b/rust/src/applayertemplate/detect.rs index 9eaf880ed785..d69cdba38fa2 100644 --- a/rust/src/applayertemplate/detect.rs +++ b/rust/src/applayertemplate/detect.rs @@ -19,12 +19,12 @@ use super::template::{TemplateTransaction, ALPROTO_TEMPLATE}; /* TEMPLATE_START_REMOVE */ use crate::conf::conf_get_node; /* TEMPLATE_END_REMOVE */ -use crate::direction::Direction; use crate::detect::{ DetectBufferSetActiveList, DetectHelperBufferMpmRegister, DetectHelperGetData, DetectHelperKeywordRegister, DetectSignatureSetAppProto, SCSigTableElmt, SIGMATCH_INFO_STICKY_BUFFER, SIGMATCH_NOOPT, }; +use crate::direction::Direction; use std::os::raw::{c_int, c_void}; static mut G_TEMPLATE_BUFFER_BUFFER_ID: c_int = 0; diff --git a/rust/src/applayertemplate/logger.rs b/rust/src/applayertemplate/logger.rs index 766a07acdb9d..a970e832776e 100644 --- a/rust/src/applayertemplate/logger.rs +++ b/rust/src/applayertemplate/logger.rs @@ -33,8 +33,9 @@ fn log_template(tx: &TemplateTransaction, js: &mut JsonBuilder) -> Result<(), Js #[no_mangle] pub unsafe extern "C" fn rs_template_logger_log( - tx: *mut std::os::raw::c_void, js: &mut JsonBuilder, + tx: *const std::os::raw::c_void, js: *mut std::os::raw::c_void, ) -> bool { let tx = cast_pointer!(tx, TemplateTransaction); + let js = cast_pointer!(js, JsonBuilder); log_template(tx, js).is_ok() } diff --git a/rust/src/applayertemplate/mod.rs b/rust/src/applayertemplate/mod.rs index ae7005a09d0e..70e6d99f03aa 100644 --- a/rust/src/applayertemplate/mod.rs +++ b/rust/src/applayertemplate/mod.rs @@ -20,6 +20,6 @@ mod parser; pub mod template; /* TEMPLATE_START_REMOVE */ -pub mod logger; pub mod detect; +pub mod logger; /* TEMPLATE_END_REMOVE */ diff --git a/rust/src/applayertemplate/template.rs b/rust/src/applayertemplate/template.rs index 9f706a74bba0..5a0acebfc24d 100644 --- a/rust/src/applayertemplate/template.rs +++ b/rust/src/applayertemplate/template.rs @@ -16,7 +16,7 @@ */ use super::parser; -use crate::applayer::{self, *}; +use crate::applayer::*; use crate::conf::conf_get; use crate::core::{AppProto, ALPROTO_UNKNOWN, IPPROTO_TCP}; use crate::flow::Flow; @@ -120,7 +120,9 @@ impl TemplateState { } fn find_request(&mut self) -> Option<&mut TemplateTransaction> { - self.transactions.iter_mut().find(|tx| tx.response.is_none()) + self.transactions + .iter_mut() + .find(|tx| tx.response.is_none()) } fn parse_request(&mut self, input: &[u8]) -> AppLayerResult { @@ -151,11 +153,12 @@ impl TemplateState { SCLogNotice!("Request: {}", request); let mut tx = self.new_tx(); tx.request = Some(request); - if self.transactions.len() >= unsafe {TEMPLATE_MAX_TX} { - tx.tx_data.set_event(TemplateEvent::TooManyTransactions as u8); + if self.transactions.len() >= unsafe { TEMPLATE_MAX_TX } { + tx.tx_data + .set_event(TemplateEvent::TooManyTransactions as u8); } self.transactions.push_back(tx); - if self.transactions.len() >= unsafe {TEMPLATE_MAX_TX} { + if self.transactions.len() >= unsafe { TEMPLATE_MAX_TX } { return AppLayerResult::err(); } } @@ -200,7 +203,7 @@ impl TemplateState { Ok((rem, response)) => { start = rem; - if let Some(tx) = self.find_request() { + if let Some(tx) = self.find_request() { tx.tx_data.updated_tc = true; tx.response = Some(response); SCLogNotice!("Found response for request:"); @@ -387,9 +390,7 @@ pub unsafe extern "C" fn rs_template_register_parser() { localstorage_new: None, localstorage_free: None, get_tx_files: None, - get_tx_iterator: Some( - applayer::state_get_tx_iterator::, - ), + get_tx_iterator: Some(state_get_tx_iterator::), get_tx_data: template_get_tx_data, get_state_data: template_get_state_data, apply_tx_config: None, diff --git a/scripts/setup-app-layer.py b/scripts/setup-app-layer.py index f94e68ae7d7e..6bbc96d3e69b 100755 --- a/scripts/setup-app-layer.py +++ b/scripts/setup-app-layer.py @@ -210,6 +210,8 @@ def logger_patch_output_c(proto): if line.find("rs_template_logger_log") > -1: output.write(inlines[i].replace("TEMPLATE", proto.upper()).replace( "template", proto.lower())) + # RegisterSimpleJsonApplayerLogger( on itw own line for clang-format + output.write(inlines[i-1]) if line.find("OutputTemplateLogInitSub(") > -1: output.write(inlines[i].replace("Template", proto)) output.write(inlines[i+1]) diff --git a/src/output.c b/src/output.c index b86e3d483c8b..c45683ba5b4a 100644 --- a/src/output.c +++ b/src/output.c @@ -894,7 +894,8 @@ void OutputRegisterRootLoggers(void) RegisterSimpleJsonApplayerLogger(ALPROTO_WEBSOCKET, rs_websocket_logger_log, NULL); RegisterSimpleJsonApplayerLogger(ALPROTO_LDAP, rs_ldap_logger_log, NULL); RegisterSimpleJsonApplayerLogger(ALPROTO_DOH2, AlertJsonDoh2, NULL); - RegisterSimpleJsonApplayerLogger(ALPROTO_TEMPLATE, rs_template_logger_log, NULL); + RegisterSimpleJsonApplayerLogger( + ALPROTO_TEMPLATE, (EveJsonSimpleTxLogFunc)rs_template_logger_log, NULL); RegisterSimpleJsonApplayerLogger(ALPROTO_RDP, (EveJsonSimpleTxLogFunc)rs_rdp_to_json, NULL); // special case : http2 is logged in http object RegisterSimpleJsonApplayerLogger(ALPROTO_HTTP2, rs_http2_log_json, "http"); From bd42f7075d9a34c5837bfa03ce6f04a82d294efe Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Thu, 23 Jan 2025 10:18:09 +0100 Subject: [PATCH 2/9] rust/detect: extend visibility of SIGMATH_NOOPT so that it can be used by plugins Avoid export by cbindgen as this constant is also defined in C --- rust/cbindgen.toml | 2 ++ rust/src/detect/mod.rs | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/rust/cbindgen.toml b/rust/cbindgen.toml index eac6aa737760..9186f0d39d82 100644 --- a/rust/cbindgen.toml +++ b/rust/cbindgen.toml @@ -110,6 +110,8 @@ exclude = [ "IPPROTO_TCP", "IPPROTO_UDP", "SRepCatGetByShortname", + "SIGMATCH_NOOPT", + "SIGMATCH_INFO_STICKY_BUFFER", ] # Types of items that we'll generate. If empty, then all types of item are emitted. diff --git a/rust/src/detect/mod.rs b/rust/src/detect/mod.rs index c00f0dfdeb18..ff795374e054 100644 --- a/rust/src/detect/mod.rs +++ b/rust/src/detect/mod.rs @@ -76,9 +76,9 @@ pub struct SCSigTableElmt { >, } -pub(crate) const SIGMATCH_NOOPT: u16 = 1; // BIT_U16(0) in detect.h +pub const SIGMATCH_NOOPT: u16 = 1; // BIT_U16(0) in detect.h pub(crate) const SIGMATCH_QUOTES_MANDATORY: u16 = 0x40; // BIT_U16(6) in detect.h -pub(crate) const SIGMATCH_INFO_STICKY_BUFFER: u16 = 0x200; // BIT_U16(9) +pub const SIGMATCH_INFO_STICKY_BUFFER: u16 = 0x200; // BIT_U16(9) /// cbindgen:ignore extern { From a863ac964acf689a71221c9726f1d58758a06d6d Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Thu, 23 Jan 2025 10:19:07 +0100 Subject: [PATCH 3/9] rust: increase visibility of debug module so that it can be used by plugins that want to call SCLogError and such --- rust/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/src/lib.rs b/rust/src/lib.rs index f28e4c249ace..ad280a368d2d 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -83,7 +83,7 @@ extern crate suricata_derive; pub mod core; #[macro_use] -pub(crate) mod debug; +pub mod debug; pub mod common; pub mod conf; From 28366749b8bbeab17e976ce89be26705c7c085af Mon Sep 17 00:00:00 2001 From: Jason Ish Date: Tue, 21 Jan 2025 13:32:49 -0600 Subject: [PATCH 4/9] rust/plugins: fix logging initialization With the recent refactor, the log level as seen by plugins was not being updated when being set through the C interface, so just set it directly upon plugin initialization. --- rust/src/plugin.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rust/src/plugin.rs b/rust/src/plugin.rs index 7f3758640846..db47131848e5 100644 --- a/rust/src/plugin.rs +++ b/rust/src/plugin.rs @@ -19,9 +19,9 @@ pub fn init() { unsafe { - let context = super::core::SCGetContext(); - super::core::init_ffi(context); + let context = crate::core::SCGetContext(); + crate::core::init_ffi(context); - super::debug::SCSetRustLogLevel(super::debug::SCLogGetLogLevel()); + crate::debug::LEVEL = crate::debug::SCLogGetLogLevel(); } } From 8582f9947b5e982ee1de99b56aba055c2b99e33c Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Fri, 24 Jan 2025 23:02:38 +0100 Subject: [PATCH 5/9] detect/helper: memset 0 after realloc --- src/detect-engine-helper.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/detect-engine-helper.c b/src/detect-engine-helper.c index 07ffb8177057..f0c39702ad10 100644 --- a/src/detect-engine-helper.c +++ b/src/detect-engine-helper.c @@ -104,6 +104,7 @@ int DetectHelperKeywordRegister(const SCSigTableElmt *kw) return -1; } sigmatch_table = tmp; + memset(&sigmatch_table[DETECT_TBLSIZE], 0, DETECT_TBLSIZE_STEP * sizeof(SigTableElmt)); DETECT_TBLSIZE += DETECT_TBLSIZE_STEP; } @@ -130,6 +131,7 @@ int DetectHelperTransformRegister(const SCTransformTableElmt *kw) return -1; } sigmatch_table = tmp; + memset(&sigmatch_table[DETECT_TBLSIZE], 0, DETECT_TBLSIZE_STEP * sizeof(SigTableElmt)); DETECT_TBLSIZE += DETECT_TBLSIZE_STEP; } From 6ec992055cf168d9f1be2a37b13f74ece4e66c2d Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Thu, 23 Jan 2025 10:19:35 +0100 Subject: [PATCH 6/9] plugins: move some structures definitions to rust so that they can be used by rust plugins --- examples/plugins/ci-capture/Makefile.in | 2 +- plugins/pfring/Makefile.am | 2 ++ rust/cbindgen.toml | 1 + rust/src/plugin.rs | 35 +++++++++++++++++++++++++ src/decode.h | 3 ++- src/suricata-plugin.h | 15 +---------- 6 files changed, 42 insertions(+), 16 deletions(-) diff --git a/examples/plugins/ci-capture/Makefile.in b/examples/plugins/ci-capture/Makefile.in index 413abf5cc152..0ce4d5aeb269 100644 --- a/examples/plugins/ci-capture/Makefile.in +++ b/examples/plugins/ci-capture/Makefile.in @@ -9,7 +9,7 @@ SRCS := plugin.c \ # But as this is an example in the Suricata source tree we'll look for # includes in the source tree. -CPPFLAGS += -I@top_srcdir@/src -DHAVE_CONFIG_H +CPPFLAGS += -I@top_srcdir@/src -I@top_srcdir@/rust/gen -DHAVE_CONFIG_H # Currently the Suricata logging system requires this to be even for # plugins. diff --git a/plugins/pfring/Makefile.am b/plugins/pfring/Makefile.am index 10cb22bb15d8..0422ea3c9a48 100644 --- a/plugins/pfring/Makefile.am +++ b/plugins/pfring/Makefile.am @@ -1,5 +1,7 @@ pkglib_LTLIBRARIES = pfring.la +AM_CPPFLAGS = -I$(top_srcdir)/src -I@top_srcdir@/rust/gen -I@top_srcdir@/rust/dist + pfring_la_SOURCES = runmode-pfring.c source-pfring.c plugin.c pfring_la_LDFLAGS = -module -avoid-version -shared pfring_la_LIBADD = -lpfring diff --git a/rust/cbindgen.toml b/rust/cbindgen.toml index 9186f0d39d82..0328570493ce 100644 --- a/rust/cbindgen.toml +++ b/rust/cbindgen.toml @@ -84,6 +84,7 @@ include = [ "FtpEvent", "SCSigTableElmt", "SCTransformTableElmt", + "SCPlugin", ] # A list of items to not include in the generated bindings diff --git a/rust/src/plugin.rs b/rust/src/plugin.rs index db47131848e5..dd3023512935 100644 --- a/rust/src/plugin.rs +++ b/rust/src/plugin.rs @@ -17,6 +17,8 @@ //! Plugin utility module. +use core::ffi::c_int; + pub fn init() { unsafe { let context = crate::core::SCGetContext(); @@ -25,3 +27,36 @@ pub fn init() { crate::debug::LEVEL = crate::debug::SCLogGetLogLevel(); } } + +// Struct definitions +#[repr(C)] +#[allow(non_snake_case)] +pub struct SCPlugin { + pub name: *const libc::c_char, + pub license: *const libc::c_char, + pub author: *const libc::c_char, + pub Init: extern "C" fn(), +} + +#[repr(C)] +#[allow(non_snake_case)] +pub struct SCAppLayerPlugin { + pub version: u64, + pub name: *const libc::c_char, + pub Register: unsafe extern "C" fn(), + pub KeywordsRegister: unsafe extern "C" fn(), + pub logname: *const libc::c_char, + pub confname: *const libc::c_char, + pub Logger: unsafe extern "C" fn( + tx: *const std::os::raw::c_void, + jb: *mut std::os::raw::c_void, + ) -> bool, +} + +// Every change in the API used by plugins should change this number +pub const SC_PLUGIN_API_VERSION: u64 = 8; + +/// cbindgen:ignore +extern { + pub fn SCPluginRegisterAppLayer(plugin: *const SCAppLayerPlugin) -> c_int; +} diff --git a/src/decode.h b/src/decode.h index 1b299864a7c0..434d39582163 100644 --- a/src/decode.h +++ b/src/decode.h @@ -28,7 +28,6 @@ #define COUNTERS #include "suricata-common.h" -#include "suricata-plugin.h" #include "threadvars.h" #include "util-debug.h" #include "decode-events.h" @@ -456,6 +455,8 @@ struct PacketL4 { } vars; }; +#define PLUGIN_VAR_SIZE 64 + /* sizes of the members: * src: 17 bytes * dst: 17 bytes diff --git a/src/suricata-plugin.h b/src/suricata-plugin.h index 8bc2183d70fd..e688f9120c24 100644 --- a/src/suricata-plugin.h +++ b/src/suricata-plugin.h @@ -22,22 +22,12 @@ #include #include "queue.h" +#include "rust.h" /** * The size of the data chunk inside each packet structure a plugin * has for private data (Packet->plugin_v). */ -#define PLUGIN_VAR_SIZE 64 - -/** - * Structure to define a Suricata plugin. - */ -typedef struct SCPlugin_ { - const char *name; - const char *license; - const char *author; - void (*Init)(void); -} SCPlugin; typedef SCPlugin *(*SCPluginRegisterFunc)(void); @@ -52,9 +42,6 @@ typedef struct SCCapturePlugin_ { int SCPluginRegisterCapture(SCCapturePlugin *); -// Every change in the API used by plugins should change this number -#define SC_PLUGIN_API_VERSION 8 - typedef struct SCAppLayerPlugin_ { // versioning to check suricata/plugin API compatibility uint64_t version; From 7d8e0e526d89f9b471c53f90afd5ab578afa72f6 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Wed, 13 Nov 2024 11:07:03 +0100 Subject: [PATCH 7/9] plugin: add in-tree app-layer template plugin for testing Ticket: 7151 Ticket: 7152 Ticket: 7154 --- .github/workflows/builds.yml | 10 + examples/plugins/README.md | 5 + .../plugins/altemplate/.cargo/config.toml | 3 + examples/plugins/altemplate/Cargo.toml | 16 + examples/plugins/altemplate/altemplate.rules | 2 + examples/plugins/altemplate/altemplate.yaml | 17 + examples/plugins/altemplate/src/detect.rs | 105 ++++ examples/plugins/altemplate/src/lib.rs | 5 + examples/plugins/altemplate/src/log.rs | 85 +++ examples/plugins/altemplate/src/parser.rs | 66 +++ examples/plugins/altemplate/src/plugin.rs | 37 ++ examples/plugins/altemplate/src/template.rs | 502 ++++++++++++++++++ 12 files changed, 853 insertions(+) create mode 100644 examples/plugins/altemplate/.cargo/config.toml create mode 100644 examples/plugins/altemplate/Cargo.toml create mode 100644 examples/plugins/altemplate/altemplate.rules create mode 100644 examples/plugins/altemplate/altemplate.yaml create mode 100644 examples/plugins/altemplate/src/detect.rs create mode 100644 examples/plugins/altemplate/src/lib.rs create mode 100644 examples/plugins/altemplate/src/log.rs create mode 100644 examples/plugins/altemplate/src/parser.rs create mode 100644 examples/plugins/altemplate/src/plugin.rs create mode 100644 examples/plugins/altemplate/src/template.rs diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 04a2f1d5102c..ab9b8224358d 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -186,6 +186,16 @@ jobs: cat eve.json | jq -c 'select(.dns)' test $(cat eve.json | jq -c 'select(.dns)' | wc -l) = "1" + - name: Test app-layer plugin + working-directory: examples/plugins/altemplate + # test with RUSTFLAGS different than suricata build to test runtime compatibility + run: | + cargo build + ../../../src/suricata -S altemplate.rules --set plugins.0=./target/debug/libsuricata_altemplate.so --runmode=single -l . -c altemplate.yaml -k none -r ../../../rust/src/applayertemplate/template.pcap + cat eve.json | jq -c 'select(.altemplate)' + test $(cat eve.json | jq -c 'select(.altemplate)' | wc -l) = "3" + # we get 2 alerts and 1 altemplate events + - name: Test library build in tree working-directory: examples/lib/simple run: make clean all diff --git a/examples/plugins/README.md b/examples/plugins/README.md index 5300f750342b..869d7b3ea6be 100644 --- a/examples/plugins/README.md +++ b/examples/plugins/README.md @@ -9,3 +9,8 @@ is useful if you want to send EVE output to custom destinations. A minimal capture plugin that can be used as a template, but also used for testing capture plugin loading and registration in CI. + +## altemplate + +An app-layer template plugin with logging and detection. +Most code copied from rust/src/applayertemplate diff --git a/examples/plugins/altemplate/.cargo/config.toml b/examples/plugins/altemplate/.cargo/config.toml new file mode 100644 index 000000000000..98203da72330 --- /dev/null +++ b/examples/plugins/altemplate/.cargo/config.toml @@ -0,0 +1,3 @@ +[build] +# custom flags to pass to all compiler invocations +rustflags = ["-Clink-args=-Wl,-undefined,dynamic_lookup"] \ No newline at end of file diff --git a/examples/plugins/altemplate/Cargo.toml b/examples/plugins/altemplate/Cargo.toml new file mode 100644 index 000000000000..0dc0931a55d7 --- /dev/null +++ b/examples/plugins/altemplate/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "suricata-altemplate" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +nom7 = { version="7.0", package="nom" } +libc = "~0.2.82" +suricata = { path = "../../../rust/" } + +[features] +default = ["suricata8"] +suricata8 = [] diff --git a/examples/plugins/altemplate/altemplate.rules b/examples/plugins/altemplate/altemplate.rules new file mode 100644 index 000000000000..458f546eece7 --- /dev/null +++ b/examples/plugins/altemplate/altemplate.rules @@ -0,0 +1,2 @@ +alert altemplate any any -> any any (msg:"TEST"; altemplate.buffer; content:"Hello"; flow:established,to_server; sid:1; rev:1;) +alert altemplate any any -> any any (msg:"TEST"; altemplate.buffer; content:"Bye"; flow:established,to_client; sid:2; rev:1;) diff --git a/examples/plugins/altemplate/altemplate.yaml b/examples/plugins/altemplate/altemplate.yaml new file mode 100644 index 000000000000..d41035b4660b --- /dev/null +++ b/examples/plugins/altemplate/altemplate.yaml @@ -0,0 +1,17 @@ +%YAML 1.1 +--- + +outputs: + - eve-log: + enabled: yes + types: + - altemplate + - alert + - flow + +app-layer: + protocols: + altemplate: + enabled: yes + detection-ports: + dp: 7000 diff --git a/examples/plugins/altemplate/src/detect.rs b/examples/plugins/altemplate/src/detect.rs new file mode 100644 index 000000000000..e8ed4f3b15e7 --- /dev/null +++ b/examples/plugins/altemplate/src/detect.rs @@ -0,0 +1,105 @@ +/* Copyright (C) 2024 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. + */ + +// same file as rust/src/applayertemplate/detect.rs except +// TEMPLATE_START_REMOVE removed +// different paths for use statements +// keywords prefixed with altemplate instead of just template + +use super::template::{TemplateTransaction, ALPROTO_TEMPLATE}; +use std::os::raw::{c_int, c_void}; +use suricata::cast_pointer; +use suricata::detect::{ + DetectBufferSetActiveList, DetectHelperBufferMpmRegister, DetectHelperGetData, + DetectHelperKeywordRegister, DetectSignatureSetAppProto, SCSigTableElmt, + SIGMATCH_INFO_STICKY_BUFFER, SIGMATCH_NOOPT, +}; +use suricata::direction::Direction; + +static mut G_TEMPLATE_BUFFER_BUFFER_ID: c_int = 0; + +unsafe extern "C" fn template_buffer_setup( + de: *mut c_void, s: *mut c_void, _raw: *const std::os::raw::c_char, +) -> c_int { + if DetectSignatureSetAppProto(s, ALPROTO_TEMPLATE) != 0 { + return -1; + } + if DetectBufferSetActiveList(de, s, G_TEMPLATE_BUFFER_BUFFER_ID) < 0 { + return -1; + } + return 0; +} + +/// Get the request/response buffer for a transaction from C. +unsafe extern "C" fn template_buffer_get_data( + tx: *const c_void, flags: u8, buf: *mut *const u8, len: *mut u32, +) -> bool { + let tx = cast_pointer!(tx, TemplateTransaction); + if flags & Direction::ToClient as u8 != 0 { + if let Some(ref response) = tx.response { + *len = response.len() as u32; + *buf = response.as_ptr(); + return true; + } + } else if let Some(ref request) = tx.request { + *len = request.len() as u32; + *buf = request.as_ptr(); + return true; + } + return false; +} + +unsafe extern "C" fn template_buffer_get( + de: *mut c_void, transforms: *const c_void, flow: *const c_void, flow_flags: u8, + tx: *const c_void, list_id: c_int, +) -> *mut c_void { + return DetectHelperGetData( + de, + transforms, + flow, + flow_flags, + tx, + list_id, + template_buffer_get_data, + ); +} + +#[no_mangle] +pub unsafe extern "C" fn DetectTemplateRegister() { + // TODO create a suricata-verify test + // Setup a keyword structure and register it + let kw = SCSigTableElmt { + name: b"altemplate.buffer\0".as_ptr() as *const libc::c_char, + desc: b"Template content modifier to match on the template buffer\0".as_ptr() + as *const libc::c_char, + // TODO use the right anchor for url and write doc + url: b"/rules/template-keywords.html#buffer\0".as_ptr() as *const libc::c_char, + Setup: template_buffer_setup, + flags: SIGMATCH_NOOPT | SIGMATCH_INFO_STICKY_BUFFER, + AppLayerTxMatch: None, + Free: None, + }; + let _g_template_buffer_kw_id = DetectHelperKeywordRegister(&kw); + G_TEMPLATE_BUFFER_BUFFER_ID = DetectHelperBufferMpmRegister( + b"altemplate.buffer\0".as_ptr() as *const libc::c_char, + b"template.buffer intern description\0".as_ptr() as *const libc::c_char, + ALPROTO_TEMPLATE, + true, //toclient + true, //toserver + template_buffer_get, + ); +} diff --git a/examples/plugins/altemplate/src/lib.rs b/examples/plugins/altemplate/src/lib.rs new file mode 100644 index 000000000000..40aec4c18f47 --- /dev/null +++ b/examples/plugins/altemplate/src/lib.rs @@ -0,0 +1,5 @@ +mod detect; +mod log; +mod parser; +pub mod plugin; +mod template; diff --git a/examples/plugins/altemplate/src/log.rs b/examples/plugins/altemplate/src/log.rs new file mode 100644 index 000000000000..9bf5c8f8f313 --- /dev/null +++ b/examples/plugins/altemplate/src/log.rs @@ -0,0 +1,85 @@ +/* Copyright (C) 2018 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. + */ + +// same file as rust/src/applayertemplate/logger.rs except +// different paths for use statements +// open_object using altemplate instead of just template +// Jsonbuilder using C API due to opaque implementation + +use super::template::TemplateTransaction; +use std::ffi::{c_char, CString}; +use suricata::cast_pointer; +use suricata::jsonbuilder::JsonError; + +use std; + +// Jsonbuilder opaque with implementation using C API to feel like usual +#[repr(C)] +pub struct JsonBuilder { + _data: [u8; 0], +} + +extern "C" { + pub fn jb_set_string(jb: &mut JsonBuilder, key: *const c_char, val: *const c_char) -> bool; + pub fn jb_close(jb: &mut JsonBuilder) -> bool; + pub fn jb_open_object(jb: &mut JsonBuilder, key: *const c_char) -> bool; +} + +impl JsonBuilder { + pub fn close(&mut self) -> Result<(), JsonError> { + if unsafe { !jb_close(self) } { + return Err(JsonError::Memory); + } + Ok(()) + } + pub fn open_object(&mut self, key: &str) -> Result<(), JsonError> { + let keyc = CString::new(key).unwrap(); + if unsafe { !jb_open_object(self, keyc.as_ptr()) } { + return Err(JsonError::Memory); + } + Ok(()) + } + pub fn set_string(&mut self, key: &str, val: &str) -> Result<(), JsonError> { + let keyc = CString::new(key).unwrap(); + let valc = CString::new(val.escape_default().to_string()).unwrap(); + if unsafe { !jb_set_string(self, keyc.as_ptr(), valc.as_ptr()) } { + return Err(JsonError::Memory); + } + Ok(()) + } +} + +fn log_template(tx: &TemplateTransaction, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("altemplate")?; + if let Some(ref request) = tx.request { + js.set_string("request", request)?; + } + if let Some(ref response) = tx.response { + js.set_string("response", response)?; + } + js.close()?; + Ok(()) +} + +#[no_mangle] +pub unsafe extern "C" fn template_logger_log( + tx: *const std::os::raw::c_void, js: *mut std::os::raw::c_void, +) -> bool { + let tx = cast_pointer!(tx, TemplateTransaction); + let js = cast_pointer!(js, JsonBuilder); + log_template(tx, js).is_ok() +} diff --git a/examples/plugins/altemplate/src/parser.rs b/examples/plugins/altemplate/src/parser.rs new file mode 100644 index 000000000000..1660cc16bbb4 --- /dev/null +++ b/examples/plugins/altemplate/src/parser.rs @@ -0,0 +1,66 @@ +/* Copyright (C) 2018 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. + */ + +// same file as rust/src/applayertemplate/parser.rs except this comment + +use nom7::{ + bytes::streaming::{take, take_until}, + combinator::map_res, + IResult, +}; +use std; + +fn parse_len(input: &str) -> Result { + input.parse::() +} + +pub fn parse_message(i: &[u8]) -> IResult<&[u8], String> { + let (i, len) = map_res(map_res(take_until(":"), std::str::from_utf8), parse_len)(i)?; + let (i, _sep) = take(1_usize)(i)?; + let (i, msg) = map_res(take(len as usize), std::str::from_utf8)(i)?; + let result = msg.to_string(); + Ok((i, result)) +} + +#[cfg(test)] +mod tests { + use super::*; + use nom7::Err; + + /// Simple test of some valid data. + #[test] + fn test_parse_valid() { + let buf = b"12:Hello World!4:Bye."; + + let result = parse_message(buf); + match result { + Ok((remainder, message)) => { + // Check the first message. + assert_eq!(message, "Hello World!"); + + // And we should have 6 bytes left. + assert_eq!(remainder.len(), 6); + } + Err(Err::Incomplete(_)) => { + panic!("Result should not have been incomplete."); + } + Err(Err::Error(err)) | Err(Err::Failure(err)) => { + panic!("Result should not be an error: {:?}.", err); + } + } + } +} diff --git a/examples/plugins/altemplate/src/plugin.rs b/examples/plugins/altemplate/src/plugin.rs new file mode 100644 index 000000000000..7e812af8427d --- /dev/null +++ b/examples/plugins/altemplate/src/plugin.rs @@ -0,0 +1,37 @@ +use super::template::template_register_parser; +use crate::detect::DetectTemplateRegister; +use crate::log::template_logger_log; +use suricata::plugin::{ + SCAppLayerPlugin, SCPlugin, SCPluginRegisterAppLayer, SC_PLUGIN_API_VERSION, +}; +use suricata::{SCLogError, SCLogNotice}; + +extern "C" fn altemplate_plugin_init() { + suricata::plugin::init(); + SCLogNotice!("Initializing altemplate plugin"); + let plugin = SCAppLayerPlugin { + version: SC_PLUGIN_API_VERSION, // api version for suricata compatibility + name: b"altemplate\0".as_ptr() as *const libc::c_char, + logname: b"JsonaltemplateLog\0".as_ptr() as *const libc::c_char, + confname: b"eve-log.altemplate\0".as_ptr() as *const libc::c_char, + Register: template_register_parser, + Logger: template_logger_log, + KeywordsRegister: DetectTemplateRegister, + }; + unsafe { + if SCPluginRegisterAppLayer(Box::into_raw(Box::new(plugin))) != 0 { + SCLogError!("Failed to register altemplate plugin"); + } + } +} + +#[no_mangle] +extern "C" fn SCPluginRegister() -> *const SCPlugin { + let plugin = SCPlugin { + name: b"altemplate\0".as_ptr() as *const libc::c_char, + license: b"MIT\0".as_ptr() as *const libc::c_char, + author: b"Philippe Antoine\0".as_ptr() as *const libc::c_char, + Init: altemplate_plugin_init, + }; + Box::into_raw(Box::new(plugin)) +} diff --git a/examples/plugins/altemplate/src/template.rs b/examples/plugins/altemplate/src/template.rs new file mode 100644 index 000000000000..3473a9e5603d --- /dev/null +++ b/examples/plugins/altemplate/src/template.rs @@ -0,0 +1,502 @@ +/* Copyright (C) 2018-2022 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. + */ + +// same file as rust/src/applayertemplate/template.rs except +// different paths for use statements +// remove TEMPLATE_START_REMOVE +// name is altemplate instead of template + +use super::parser; +use nom7 as nom; +use std; +use std::collections::VecDeque; +use std::ffi::CString; +use std::os::raw::{c_char, c_int, c_void}; +use suricata::applayer::{ + state_get_tx_iterator, AppLayerEvent, AppLayerParserConfParserEnabled, + AppLayerParserRegisterLogger, AppLayerParserStateIssetFlag, + AppLayerProtoDetectConfProtoDetectionEnabled, AppLayerRegisterParser, + AppLayerRegisterProtocolDetection, AppLayerResult, AppLayerStateData, AppLayerTxData, + RustParser, State, StreamSlice, Transaction, APP_LAYER_PARSER_EOF_TC, APP_LAYER_PARSER_EOF_TS, + APP_LAYER_PARSER_OPT_ACCEPT_GAPS, +}; +use suricata::conf::conf_get; +use suricata::core::{AppProto, ALPROTO_UNKNOWN, IPPROTO_TCP}; +use suricata::flow::Flow; +use suricata::{ + build_slice, cast_pointer, export_state_data_get, export_tx_data_get, SCLogError, SCLogNotice, +}; + +static mut TEMPLATE_MAX_TX: usize = 256; + +pub(super) static mut ALPROTO_TEMPLATE: AppProto = ALPROTO_UNKNOWN; + +#[derive(AppLayerEvent)] +enum TemplateEvent { + TooManyTransactions, +} + +pub struct TemplateTransaction { + tx_id: u64, + pub request: Option, + pub response: Option, + + tx_data: AppLayerTxData, +} + +impl Default for TemplateTransaction { + fn default() -> Self { + Self::new() + } +} + +impl TemplateTransaction { + pub fn new() -> TemplateTransaction { + Self { + tx_id: 0, + request: None, + response: None, + tx_data: AppLayerTxData::new(), + } + } +} + +impl Transaction for TemplateTransaction { + fn id(&self) -> u64 { + self.tx_id + } +} + +#[derive(Default)] +pub struct TemplateState { + state_data: AppLayerStateData, + tx_id: u64, + transactions: VecDeque, + request_gap: bool, + response_gap: bool, +} + +impl State for TemplateState { + fn get_transaction_count(&self) -> usize { + self.transactions.len() + } + + fn get_transaction_by_index(&self, index: usize) -> Option<&TemplateTransaction> { + self.transactions.get(index) + } +} + +impl TemplateState { + pub fn new() -> Self { + Default::default() + } + + // Free a transaction by ID. + fn free_tx(&mut self, tx_id: u64) { + let len = self.transactions.len(); + let mut found = false; + let mut index = 0; + for i in 0..len { + let tx = &self.transactions[i]; + if tx.tx_id == tx_id + 1 { + found = true; + index = i; + break; + } + } + if found { + self.transactions.remove(index); + } + } + + pub fn get_tx(&mut self, tx_id: u64) -> Option<&TemplateTransaction> { + self.transactions.iter().find(|tx| tx.tx_id == tx_id + 1) + } + + fn new_tx(&mut self) -> TemplateTransaction { + let mut tx = TemplateTransaction::new(); + self.tx_id += 1; + tx.tx_id = self.tx_id; + return tx; + } + + fn find_request(&mut self) -> Option<&mut TemplateTransaction> { + self.transactions + .iter_mut() + .find(|tx| tx.response.is_none()) + } + + fn parse_request(&mut self, input: &[u8]) -> AppLayerResult { + // We're not interested in empty requests. + if input.is_empty() { + return AppLayerResult::ok(); + } + + // If there was gap, check we can sync up again. + if self.request_gap { + if probe(input).is_err() { + // The parser now needs to decide what to do as we are not in sync. + // For this template, we'll just try again next time. + return AppLayerResult::ok(); + } + + // It looks like we're in sync with a message header, clear gap + // state and keep parsing. + self.request_gap = false; + } + + let mut start = input; + while !start.is_empty() { + match parser::parse_message(start) { + Ok((rem, request)) => { + start = rem; + + SCLogNotice!("Request: {}", request); + let mut tx = self.new_tx(); + tx.request = Some(request); + if self.transactions.len() >= unsafe { TEMPLATE_MAX_TX } { + tx.tx_data + .set_event(TemplateEvent::TooManyTransactions as u8); + } + self.transactions.push_back(tx); + if self.transactions.len() >= unsafe { TEMPLATE_MAX_TX } { + return AppLayerResult::err(); + } + } + Err(nom::Err::Incomplete(_)) => { + // Not enough data. This parser doesn't give us a good indication + // of how much data is missing so just ask for one more byte so the + // parse is called as soon as more data is received. + let consumed = input.len() - start.len(); + let needed = start.len() + 1; + return AppLayerResult::incomplete(consumed as u32, needed as u32); + } + Err(_) => { + return AppLayerResult::err(); + } + } + } + + // Input was fully consumed. + return AppLayerResult::ok(); + } + + fn parse_response(&mut self, input: &[u8]) -> AppLayerResult { + // We're not interested in empty responses. + if input.is_empty() { + return AppLayerResult::ok(); + } + + if self.response_gap { + if probe(input).is_err() { + // The parser now needs to decide what to do as we are not in sync. + // For this template, we'll just try again next time. + return AppLayerResult::ok(); + } + + // It looks like we're in sync with a message header, clear gap + // state and keep parsing. + self.response_gap = false; + } + let mut start = input; + while !start.is_empty() { + match parser::parse_message(start) { + Ok((rem, response)) => { + start = rem; + + if let Some(tx) = self.find_request() { + tx.tx_data.updated_tc = true; + tx.response = Some(response); + SCLogNotice!("Found response for request:"); + SCLogNotice!("- Request: {:?}", tx.request); + SCLogNotice!("- Response: {:?}", tx.response); + } + } + Err(nom::Err::Incomplete(_)) => { + let consumed = input.len() - start.len(); + let needed = start.len() + 1; + return AppLayerResult::incomplete(consumed as u32, needed as u32); + } + Err(_) => { + return AppLayerResult::err(); + } + } + } + + // All input was fully consumed. + return AppLayerResult::ok(); + } + + fn on_request_gap(&mut self, _size: u32) { + self.request_gap = true; + } + + fn on_response_gap(&mut self, _size: u32) { + self.response_gap = true; + } +} + +/// Probe for a valid header. +/// +/// As this template protocol uses messages prefixed with the size +/// as a string followed by a ':', we look at up to the first 10 +/// characters for that pattern. +fn probe(input: &[u8]) -> nom::IResult<&[u8], ()> { + let size = std::cmp::min(10, input.len()); + let (rem, prefix) = nom::bytes::complete::take(size)(input)?; + nom::sequence::terminated( + nom::bytes::complete::take_while1(nom::character::is_digit), + nom::bytes::complete::tag(":"), + )(prefix)?; + Ok((rem, ())) +} + +// C exports. + +/// C entry point for a probing parser. +unsafe extern "C" fn rs_template_probing_parser( + _flow: *const Flow, _direction: u8, input: *const u8, input_len: u32, _rdir: *mut u8, +) -> AppProto { + // Need at least 2 bytes. + if input_len > 1 && !input.is_null() { + let slice = build_slice!(input, input_len as usize); + if probe(slice).is_ok() { + return ALPROTO_TEMPLATE; + } + } + return ALPROTO_UNKNOWN; +} + +extern "C" fn rs_template_state_new( + _orig_state: *mut c_void, _orig_proto: AppProto, +) -> *mut c_void { + let state = TemplateState::new(); + let boxed = Box::new(state); + return Box::into_raw(boxed) as *mut c_void; +} + +unsafe extern "C" fn rs_template_state_free(state: *mut c_void) { + std::mem::drop(Box::from_raw(state as *mut TemplateState)); +} + +unsafe extern "C" fn rs_template_state_tx_free(state: *mut c_void, tx_id: u64) { + let state = cast_pointer!(state, TemplateState); + state.free_tx(tx_id); +} + +unsafe extern "C" fn rs_template_parse_request( + _flow: *const Flow, state: *mut c_void, pstate: *mut c_void, stream_slice: StreamSlice, + _data: *const c_void, +) -> AppLayerResult { + let eof = AppLayerParserStateIssetFlag(pstate, APP_LAYER_PARSER_EOF_TS) > 0; + + if eof { + // If needed, handle EOF, or pass it into the parser. + return AppLayerResult::ok(); + } + + let state = cast_pointer!(state, TemplateState); + + if stream_slice.is_gap() { + // Here we have a gap signaled by the input being null, but a greater + // than 0 input_len which provides the size of the gap. + state.on_request_gap(stream_slice.gap_size()); + AppLayerResult::ok() + } else { + let buf = stream_slice.as_slice(); + state.parse_request(buf) + } +} + +unsafe extern "C" fn rs_template_parse_response( + _flow: *const Flow, state: *mut c_void, pstate: *mut c_void, stream_slice: StreamSlice, + _data: *const c_void, +) -> AppLayerResult { + let _eof = AppLayerParserStateIssetFlag(pstate, APP_LAYER_PARSER_EOF_TC) > 0; + let state = cast_pointer!(state, TemplateState); + + if stream_slice.is_gap() { + // Here we have a gap signaled by the input being null, but a greater + // than 0 input_len which provides the size of the gap. + state.on_response_gap(stream_slice.gap_size()); + AppLayerResult::ok() + } else { + let buf = stream_slice.as_slice(); + state.parse_response(buf) + } +} + +unsafe extern "C" fn rs_template_state_get_tx(state: *mut c_void, tx_id: u64) -> *mut c_void { + let state = cast_pointer!(state, TemplateState); + match state.get_tx(tx_id) { + Some(tx) => { + return tx as *const _ as *mut _; + } + None => { + return std::ptr::null_mut(); + } + } +} + +unsafe extern "C" fn rs_template_state_get_tx_count(state: *mut c_void) -> u64 { + let state = cast_pointer!(state, TemplateState); + return state.tx_id; +} + +unsafe extern "C" fn rs_template_tx_get_alstate_progress(tx: *mut c_void, _direction: u8) -> c_int { + let tx = cast_pointer!(tx, TemplateTransaction); + + // Transaction is done if we have a response. + if tx.response.is_some() { + return 1; + } + return 0; +} + +export_tx_data_get!(rs_template_get_tx_data, TemplateTransaction); +export_state_data_get!(rs_template_get_state_data, TemplateState); + +// Parser name as a C style string. +const PARSER_NAME: &[u8] = b"altemplate\0"; + +#[no_mangle] +pub unsafe extern "C" fn template_register_parser() { + let default_port = CString::new("[7000]").unwrap(); + let parser = RustParser { + name: PARSER_NAME.as_ptr() as *const c_char, + default_port: default_port.as_ptr(), + ipproto: IPPROTO_TCP, + probe_ts: Some(rs_template_probing_parser), + probe_tc: Some(rs_template_probing_parser), + min_depth: 0, + max_depth: 16, + state_new: rs_template_state_new, + state_free: rs_template_state_free, + tx_free: rs_template_state_tx_free, + parse_ts: rs_template_parse_request, + parse_tc: rs_template_parse_response, + get_tx_count: rs_template_state_get_tx_count, + get_tx: rs_template_state_get_tx, + tx_comp_st_ts: 1, + tx_comp_st_tc: 1, + tx_get_progress: rs_template_tx_get_alstate_progress, + get_eventinfo: Some(TemplateEvent::get_event_info), + get_eventinfo_byid: Some(TemplateEvent::get_event_info_by_id), + localstorage_new: None, + localstorage_free: None, + get_tx_files: None, + get_tx_iterator: Some(state_get_tx_iterator::), + get_tx_data: rs_template_get_tx_data, + get_state_data: rs_template_get_state_data, + apply_tx_config: None, + flags: APP_LAYER_PARSER_OPT_ACCEPT_GAPS, + get_frame_id_by_name: None, + get_frame_name_by_id: None, + }; + + let ip_proto_str = CString::new("tcp").unwrap(); + + if AppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let alproto = AppLayerRegisterProtocolDetection(&parser, 1); + ALPROTO_TEMPLATE = alproto; + if AppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let _ = AppLayerRegisterParser(&parser, alproto); + } + if let Some(val) = conf_get("app-layer.protocols.template.max-tx") { + if let Ok(v) = val.parse::() { + TEMPLATE_MAX_TX = v; + } else { + SCLogError!("Invalid value for template.max-tx"); + } + } + AppLayerParserRegisterLogger(IPPROTO_TCP, ALPROTO_TEMPLATE); + SCLogNotice!("Rust template parser registered."); + } else { + SCLogNotice!("Protocol detector and parser disabled for TEMPLATE."); + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_probe() { + assert!(probe(b"1").is_err()); + assert!(probe(b"1:").is_ok()); + assert!(probe(b"123456789:").is_ok()); + assert!(probe(b"0123456789:").is_err()); + } + + #[test] + fn test_incomplete() { + let mut state = TemplateState::new(); + let buf = b"5:Hello3:bye"; + + let r = state.parse_request(&buf[0..0]); + assert_eq!( + r, + AppLayerResult { + status: 0, + consumed: 0, + needed: 0 + } + ); + + let r = state.parse_request(&buf[0..1]); + assert_eq!( + r, + AppLayerResult { + status: 1, + consumed: 0, + needed: 2 + } + ); + + let r = state.parse_request(&buf[0..2]); + assert_eq!( + r, + AppLayerResult { + status: 1, + consumed: 0, + needed: 3 + } + ); + + // This is the first message and only the first message. + let r = state.parse_request(&buf[0..7]); + assert_eq!( + r, + AppLayerResult { + status: 0, + consumed: 0, + needed: 0 + } + ); + + // The first message and a portion of the second. + let r = state.parse_request(&buf[0..9]); + assert_eq!( + r, + AppLayerResult { + status: 1, + consumed: 7, + needed: 3 + } + ); + } +} From 55484af2fc7864cd1279ea51f98c590f58767041 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Fri, 17 Jan 2025 12:18:15 +0100 Subject: [PATCH 8/9] plugin: document app-layer plugins Ticket: 7149 Ticket: 7150 Ticket: 7153 --- doc/userguide/devguide/libsuricata/index.rst | 60 +++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/doc/userguide/devguide/libsuricata/index.rst b/doc/userguide/devguide/libsuricata/index.rst index d0c58c29b18b..3e2a10898632 100644 --- a/doc/userguide/devguide/libsuricata/index.rst +++ b/doc/userguide/devguide/libsuricata/index.rst @@ -1,7 +1,7 @@ .. _libsuricata: -LibSuricata -=========== +LibSuricata and Plugins +======================= Using Suricata as a Library --------------------------- @@ -10,5 +10,61 @@ The ability to turn Suricata into a library that can be utilized in other tools is currently a work in progress, tracked by Redmine Ticket #2693: https://redmine.openinfosecfoundation.org/issues/2693. +Plugins +------- + A related work are Suricata plugins, also in progress and tracked by Redmine Ticket #4101: https://redmine.openinfosecfoundation.org/issues/4101. + +Plugins can be used by modifying suricata.yaml ``plugins`` section to include +the path of the dynamic library to load. + +Plugins should export a ``SCPluginRegister`` function that will be the entry point +used by Suricata. + +Application-layer plugins +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Application layer plugins can be added as demonstrated by example +https://github.com/OISF/suricata/blob/master/examples/plugins/altemplate/ + +The plugin code contains the same files as an application layer in the source tree: +- alname.rs +- detect.rs +- lib.rs +- log.rs +- parser.rs + +These files will have different ``use`` statements, targetting the suricata crate. + +.. attention:: A plugin should not use rust structures from suricata crate if they are not repr(C), especially JsonBuilder. + +This is because the rust compiler does not guarantee the structure layout unless you specify this representation. +Thus, the plugin may expect the ``JsonBuilder`` fields at different offsets that they are supplied by Suricata at runtime. +The solution is to go through the ``JsonBuilder`` C API which uses an opaque pointer. + +And the plugin contains also additional files: +- plugin.rs : defines the entry point of the plugin ``SCPluginRegister`` + +``SCPluginRegister`` should register callback that should then call ``SCPluginRegisterAppLayer`` +passing a ``SCAppLayerPlugin`` structure to suricata. +It should also call ``suricata::plugin::init();`` to ensure the plugin has initialized +its value of the Suricata Context, a structure needed by rust, to call some C functions, +that cannot be found at compile time because of circular dependencies, and are therefore +resolved at runtime. + +This ``SCAppLayerPlugin`` begins by a version number ``SC_PLUGIN_API_VERSION`` for runtime compatibility +between Suricata and the plugin. + +Known limitations are: + +- Plugins can only use simple logging as defined by ``EveJsonSimpleTxLogFunc`` + without suricata.yaml configuration, see https://github.com/OISF/suricata/pull/11160 +- Keywords cannot use validate callbacks, see https://redmine.openinfosecfoundation.org/issues/5634 +- Plugins cannot have keywords matching on mulitple protocols (like ja4), + see https://redmine.openinfosecfoundation.org/issues/7304 + +.. attention:: A pure rust plugin needs to be compiled with ``RUSTFLAGS=-Clink-args=-Wl,-undefined,dynamic_lookup`` + +This is because the plugin will link dynamically at runtime the functions defined in Suricata runtime. +You can define this rust flags in a ``.cargo/config.toml`` file. \ No newline at end of file From b35cd6d528d88df351e34d1727923dca24480e12 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Fri, 24 Jan 2025 22:53:58 +0100 Subject: [PATCH 9/9] plugin: restrict rust visibility in example --- examples/plugins/altemplate/src/detect.rs | 3 +-- examples/plugins/altemplate/src/log.rs | 9 ++++----- examples/plugins/altemplate/src/parser.rs | 2 +- examples/plugins/altemplate/src/plugin.rs | 4 ++-- examples/plugins/altemplate/src/template.rs | 7 +++---- examples/plugins/c-custom-loggers/Makefile.in | 2 +- 6 files changed, 12 insertions(+), 15 deletions(-) diff --git a/examples/plugins/altemplate/src/detect.rs b/examples/plugins/altemplate/src/detect.rs index e8ed4f3b15e7..ed1955a6c98c 100644 --- a/examples/plugins/altemplate/src/detect.rs +++ b/examples/plugins/altemplate/src/detect.rs @@ -78,8 +78,7 @@ unsafe extern "C" fn template_buffer_get( ); } -#[no_mangle] -pub unsafe extern "C" fn DetectTemplateRegister() { +pub(super) unsafe extern "C" fn detect_template_register() { // TODO create a suricata-verify test // Setup a keyword structure and register it let kw = SCSigTableElmt { diff --git a/examples/plugins/altemplate/src/log.rs b/examples/plugins/altemplate/src/log.rs index 9bf5c8f8f313..8fc4870d2968 100644 --- a/examples/plugins/altemplate/src/log.rs +++ b/examples/plugins/altemplate/src/log.rs @@ -40,20 +40,20 @@ extern "C" { } impl JsonBuilder { - pub fn close(&mut self) -> Result<(), JsonError> { + fn close(&mut self) -> Result<(), JsonError> { if unsafe { !jb_close(self) } { return Err(JsonError::Memory); } Ok(()) } - pub fn open_object(&mut self, key: &str) -> Result<(), JsonError> { + fn open_object(&mut self, key: &str) -> Result<(), JsonError> { let keyc = CString::new(key).unwrap(); if unsafe { !jb_open_object(self, keyc.as_ptr()) } { return Err(JsonError::Memory); } Ok(()) } - pub fn set_string(&mut self, key: &str, val: &str) -> Result<(), JsonError> { + fn set_string(&mut self, key: &str, val: &str) -> Result<(), JsonError> { let keyc = CString::new(key).unwrap(); let valc = CString::new(val.escape_default().to_string()).unwrap(); if unsafe { !jb_set_string(self, keyc.as_ptr(), valc.as_ptr()) } { @@ -75,8 +75,7 @@ fn log_template(tx: &TemplateTransaction, js: &mut JsonBuilder) -> Result<(), Js Ok(()) } -#[no_mangle] -pub unsafe extern "C" fn template_logger_log( +pub(super) unsafe extern "C" fn template_logger_log( tx: *const std::os::raw::c_void, js: *mut std::os::raw::c_void, ) -> bool { let tx = cast_pointer!(tx, TemplateTransaction); diff --git a/examples/plugins/altemplate/src/parser.rs b/examples/plugins/altemplate/src/parser.rs index 1660cc16bbb4..2c9af698bf66 100644 --- a/examples/plugins/altemplate/src/parser.rs +++ b/examples/plugins/altemplate/src/parser.rs @@ -28,7 +28,7 @@ fn parse_len(input: &str) -> Result { input.parse::() } -pub fn parse_message(i: &[u8]) -> IResult<&[u8], String> { +pub(super) fn parse_message(i: &[u8]) -> IResult<&[u8], String> { let (i, len) = map_res(map_res(take_until(":"), std::str::from_utf8), parse_len)(i)?; let (i, _sep) = take(1_usize)(i)?; let (i, msg) = map_res(take(len as usize), std::str::from_utf8)(i)?; diff --git a/examples/plugins/altemplate/src/plugin.rs b/examples/plugins/altemplate/src/plugin.rs index 7e812af8427d..ac53cdb7ab09 100644 --- a/examples/plugins/altemplate/src/plugin.rs +++ b/examples/plugins/altemplate/src/plugin.rs @@ -1,5 +1,5 @@ use super::template::template_register_parser; -use crate::detect::DetectTemplateRegister; +use crate::detect::detect_template_register; use crate::log::template_logger_log; use suricata::plugin::{ SCAppLayerPlugin, SCPlugin, SCPluginRegisterAppLayer, SC_PLUGIN_API_VERSION, @@ -16,7 +16,7 @@ extern "C" fn altemplate_plugin_init() { confname: b"eve-log.altemplate\0".as_ptr() as *const libc::c_char, Register: template_register_parser, Logger: template_logger_log, - KeywordsRegister: DetectTemplateRegister, + KeywordsRegister: detect_template_register, }; unsafe { if SCPluginRegisterAppLayer(Box::into_raw(Box::new(plugin))) != 0 { diff --git a/examples/plugins/altemplate/src/template.rs b/examples/plugins/altemplate/src/template.rs index 3473a9e5603d..fc6d651ada00 100644 --- a/examples/plugins/altemplate/src/template.rs +++ b/examples/plugins/altemplate/src/template.rs @@ -50,7 +50,7 @@ enum TemplateEvent { TooManyTransactions, } -pub struct TemplateTransaction { +pub(super) struct TemplateTransaction { tx_id: u64, pub request: Option, pub response: Option, @@ -82,7 +82,7 @@ impl Transaction for TemplateTransaction { } #[derive(Default)] -pub struct TemplateState { +struct TemplateState { state_data: AppLayerStateData, tx_id: u64, transactions: VecDeque, @@ -373,8 +373,7 @@ export_state_data_get!(rs_template_get_state_data, TemplateState); // Parser name as a C style string. const PARSER_NAME: &[u8] = b"altemplate\0"; -#[no_mangle] -pub unsafe extern "C" fn template_register_parser() { +pub(super) unsafe extern "C" fn template_register_parser() { let default_port = CString::new("[7000]").unwrap(); let parser = RustParser { name: PARSER_NAME.as_ptr() as *const c_char, diff --git a/examples/plugins/c-custom-loggers/Makefile.in b/examples/plugins/c-custom-loggers/Makefile.in index a30bc8a385cf..0d7aa9d1fbf7 100644 --- a/examples/plugins/c-custom-loggers/Makefile.in +++ b/examples/plugins/c-custom-loggers/Makefile.in @@ -5,7 +5,7 @@ # But as this is an example in the Suricata source tree we'll look for # includes in the source tree. -CPPFLAGS += -I@top_srcdir@/src -DHAVE_CONFIG_H +CPPFLAGS += -I@top_srcdir@/src -I@top_srcdir@/rust/gen -DHAVE_CONFIG_H # Currently the Suricata logging system requires this to be even for # plugins.