From 1487e382f0f9bc2ba0914e3801797a423b927928 Mon Sep 17 00:00:00 2001 From: Jeff Lucovsky Date: Sun, 27 Oct 2024 09:28:08 -0400 Subject: [PATCH 01/11] memcap/socket: Improve memcap array support Remove hard-coded value for the memcap array and substitute compile-time value for array sizing. Issue: 845 --- src/runmode-unix-socket.c | 63 ++++++++++----------------------------- 1 file changed, 15 insertions(+), 48 deletions(-) diff --git a/src/runmode-unix-socket.c b/src/runmode-unix-socket.c index e0b314a1cd2a..bdb9156d5b0d 100644 --- a/src/runmode-unix-socket.c +++ b/src/runmode-unix-socket.c @@ -86,50 +86,20 @@ const char *RunModeUnixSocketGetDefaultMode(void) return "autofp"; } -#define MEMCAPS_MAX 7 -static MemcapCommand memcaps[MEMCAPS_MAX] = { +static MemcapCommand memcaps[] = { { - "stream", - StreamTcpSetMemcap, - StreamTcpGetMemcap, - StreamTcpMemuseCounter, - }, - { - "stream-reassembly", - StreamTcpReassembleSetMemcap, - StreamTcpReassembleGetMemcap, - StreamTcpReassembleMemuseGlobalCounter - }, - { - "flow", - FlowSetMemcap, - FlowGetMemcap, - FlowGetMemuse - }, - { - "applayer-proto-http", - HTPSetMemcap, - HTPGetMemcap, - HTPMemuseGlobalCounter - }, - { - "defrag", - DefragTrackerSetMemcap, - DefragTrackerGetMemcap, - DefragTrackerGetMemuse - }, - { - "ippair", - IPPairSetMemcap, - IPPairGetMemcap, - IPPairGetMemuse - }, - { - "host", - HostSetMemcap, - HostGetMemcap, - HostGetMemuse + "stream", + StreamTcpSetMemcap, + StreamTcpGetMemcap, + StreamTcpMemuseCounter, }, + { "stream-reassembly", StreamTcpReassembleSetMemcap, StreamTcpReassembleGetMemcap, + StreamTcpReassembleMemuseGlobalCounter }, + { "flow", FlowSetMemcap, FlowGetMemcap, FlowGetMemuse }, + { "applayer-proto-http", HTPSetMemcap, HTPGetMemcap, HTPMemuseGlobalCounter }, + { "defrag", DefragTrackerSetMemcap, DefragTrackerGetMemcap, DefragTrackerGetMemuse }, + { "ippair", IPPairSetMemcap, IPPairGetMemcap, IPPairGetMemuse }, + { "host", HostSetMemcap, HostGetMemcap, HostGetMemuse }, }; float MemcapsGetPressure(void) @@ -1523,7 +1493,6 @@ TmEcode UnixSocketSetMemcap(json_t *cmd, json_t* answer, void *data) char *memcap = NULL; char *value_str = NULL; uint64_t value; - int i; json_t *jarg = json_object_get(cmd, "config"); if (!json_is_string(jarg)) { @@ -1549,7 +1518,7 @@ TmEcode UnixSocketSetMemcap(json_t *cmd, json_t* answer, void *data) return TM_ECODE_FAILED; } - for (i = 0; i < MEMCAPS_MAX; i++) { + for (size_t i = 0; i < ARRAY_SIZE(memcaps); i++) { if (strcmp(memcaps[i].name, memcap) == 0 && memcaps[i].SetFunc) { int updated = memcaps[i].SetFunc(value); char message[150]; @@ -1592,7 +1561,6 @@ TmEcode UnixSocketSetMemcap(json_t *cmd, json_t* answer, void *data) TmEcode UnixSocketShowMemcap(json_t *cmd, json_t *answer, void *data) { char *memcap = NULL; - int i; json_t *jarg = json_object_get(cmd, "config"); if (!json_is_string(jarg)) { @@ -1601,7 +1569,7 @@ TmEcode UnixSocketShowMemcap(json_t *cmd, json_t *answer, void *data) } memcap = (char *)json_string_value(jarg); - for (i = 0; i < MEMCAPS_MAX; i++) { + for (size_t i = 0; i < ARRAY_SIZE(memcaps); i++) { if (strcmp(memcaps[i].name, memcap) == 0 && memcaps[i].GetFunc) { char str[50]; uint64_t val = memcaps[i].GetFunc(); @@ -1632,7 +1600,6 @@ TmEcode UnixSocketShowMemcap(json_t *cmd, json_t *answer, void *data) TmEcode UnixSocketShowAllMemcap(json_t *cmd, json_t *answer, void *data) { json_t *jmemcaps = json_array(); - int i; if (jmemcaps == NULL) { json_object_set_new(answer, "message", @@ -1640,7 +1607,7 @@ TmEcode UnixSocketShowAllMemcap(json_t *cmd, json_t *answer, void *data) return TM_ECODE_FAILED; } - for (i = 0; i < MEMCAPS_MAX; i++) { + for (size_t i = 0; i < ARRAY_SIZE(memcaps); i++) { json_t *jobj = json_object(); if (jobj == NULL) { json_decref(jmemcaps); From f911b8b59fef90d22e380362c8e586ac5a6b2f54 Mon Sep 17 00:00:00 2001 From: Jeff Lucovsky Date: Sun, 27 Oct 2024 09:53:31 -0400 Subject: [PATCH 02/11] thash/memcap: Use atomics for memcap Issue: 845 Maintain the memcap as an atomic counter so changes through the unix-socket interface can be supported. --- src/util-thash.c | 22 +++++++++++++--------- src/util-thash.h | 7 ++++--- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/util-thash.c b/src/util-thash.c index 3787454a37f4..be6e930bcc5f 100644 --- a/src/util-thash.c +++ b/src/util-thash.c @@ -228,12 +228,15 @@ static int THashInitConfig(THashTableContext *ctx, const char *cnf_prefix) GET_VAR(cnf_prefix, "memcap"); if ((ConfGet(varname, &conf_val)) == 1) { - if (ParseSizeStringU64(conf_val, &ctx->config.memcap) < 0) { + uint64_t memcap; + if (ParseSizeStringU64(conf_val, &memcap) < 0) { SCLogError("Error parsing %s " "from conf file - %s. Killing engine", varname, conf_val); return -1; } + SC_ATOMIC_INIT(ctx->config.memcap); + SC_ATOMIC_SET(ctx->config.memcap, memcap); } GET_VAR(cnf_prefix, "hash-size"); if ((ConfGet(varname, &conf_val)) == 1) @@ -261,7 +264,7 @@ static int THashInitConfig(THashTableContext *ctx, const char *cnf_prefix) "Memcap: %" PRIu64 ", Hash table size %" PRIu64 ". Calculate " "total hash size by multiplying \"hash-size\" with %" PRIuMAX ", " "which is the hash bucket size.", - ctx->config.memcap, hash_size, (uintmax_t)sizeof(THashHashRow)); + SC_ATOMIC_GET(ctx->config.memcap), hash_size, (uintmax_t)sizeof(THashHashRow)); return -1; } ctx->array = SCMallocAligned(ctx->config.hash_size * sizeof(THashHashRow), CLS); @@ -283,7 +286,7 @@ static int THashInitConfig(THashTableContext *ctx, const char *cnf_prefix) SCLogError("preallocating data failed: " "max thash memcap reached. Memcap %" PRIu64 ", " "Memuse %" PRIu64 ".", - ctx->config.memcap, + SC_ATOMIC_GET(ctx->config.memcap), ((uint64_t)SC_ATOMIC_GET(ctx->memuse) + THASH_DATA_SIZE(ctx))); return -1; } @@ -323,12 +326,12 @@ THashTableContext *THashInit(const char *cnf_prefix, size_t data_size, unless defined by the rule keyword */ #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION // limit memcap size to default when fuzzing - ctx->config.memcap = THASH_DEFAULT_MEMCAP; + SC_ATOMIC_SET(ctx->config.memcap, THASH_DEFAULT_MEMCAP); #else if (memcap > 0) { - ctx->config.memcap = memcap; + SC_ATOMIC_SET(ctx->config.memcap, memcap); } else { - ctx->config.memcap = reset_memcap ? UINT64_MAX : THASH_DEFAULT_MEMCAP; + SC_ATOMIC_SET(ctx->config.memcap, reset_memcap ? UINT64_MAX : THASH_DEFAULT_MEMCAP); } #endif ctx->config.prealloc = THASH_DEFAULT_PREALLOC; @@ -349,8 +352,9 @@ THashTableContext *THashInit(const char *cnf_prefix, size_t data_size, * */ void THashConsolidateMemcap(THashTableContext *ctx) { - ctx->config.memcap = MAX(SC_ATOMIC_GET(ctx->memuse), ctx->config.memcap); - SCLogDebug("memcap after load set to: %" PRIu64, ctx->config.memcap); + SC_ATOMIC_SET( + ctx->config.memcap, MAX(SC_ATOMIC_GET(ctx->memuse), SC_ATOMIC_GET(ctx->config.memcap))); + SCLogDebug("memcap after load set to: %" PRIu64, SC_ATOMIC_GET(ctx->config.memcap)); } /** \brief shutdown the flow engine @@ -598,7 +602,7 @@ static THashData *THashDataGetNew(THashTableContext *ctx, void *data) SC_ATOMIC_SET(ctx->memcap_reached, true); } SCLogError("Adding data will exceed memcap: %" PRIu64 ", current memuse: %" PRIu64, - (ctx)->config.memcap, SC_ATOMIC_GET(ctx->memuse)); + SC_ATOMIC_GET((ctx)->config.memcap), SC_ATOMIC_GET(ctx->memuse)); } } } diff --git a/src/util-thash.h b/src/util-thash.h index 803a5f477c3c..5d4a61f10b12 100644 --- a/src/util-thash.h +++ b/src/util-thash.h @@ -122,7 +122,7 @@ typedef int (*THashOutputFunc)(void *output_ctx, const uint8_t *data, const uint typedef int (*THashFormatFunc)(const void *in_data, char *output, size_t output_size); typedef struct THashDataConfig_ { - uint64_t memcap; + SC_ATOMIC_DECLARE(uint64_t, memcap); uint32_t hash_rand; uint32_t hash_size; uint32_t prealloc; @@ -161,8 +161,9 @@ typedef struct THashTableContext_ { * \retval 1 it fits * \retval 0 no fit */ -#define THASH_CHECK_MEMCAP(ctx, size) \ - ((((uint64_t)SC_ATOMIC_GET((ctx)->memuse) + (uint64_t)(size)) <= (ctx)->config.memcap)) +#define THASH_CHECK_MEMCAP(ctx, size) \ + ((((uint64_t)SC_ATOMIC_GET((ctx)->memuse) + (uint64_t)(size)) <= \ + SC_ATOMIC_GET((ctx)->config.memcap))) #define THashIncrUsecnt(h) \ (void)SC_ATOMIC_ADD((h)->use_cnt, 1) From f3b3689bcabc4913932c4598a3200cd8f56f6a37 Mon Sep 17 00:00:00 2001 From: Jeff Lucovsky Date: Sun, 27 Oct 2024 10:13:07 -0400 Subject: [PATCH 03/11] app-layer/stats: Expand memuse/memcap handling This commit adds memcap/memuse handling to the unix-socket interface: - ftp - http-byterange - host New stats: - ippair: memuse, memcap - host: memuse, memcap - http-byterange: memuse, memcap --- etc/schema.json | 36 ++++++++++++++++++++++++++++++++++++ src/app-layer-ftp.c | 10 ++++++++++ src/app-layer-ftp.h | 1 + src/app-layer-htp-range.c | 24 +++++++++++++++++++++++- src/app-layer-htp-range.h | 6 +++++- src/app-layer.c | 7 +++++++ src/runmode-unix-socket.c | 5 +++++ 7 files changed, 87 insertions(+), 2 deletions(-) diff --git a/etc/schema.json b/etc/schema.json index cf03a2db30b6..08959e3c7a1d 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -6389,6 +6389,42 @@ "additionalProperties": false }, "http": { + "type": "object", + "properties": { + "memcap": { + "type": "integer" + }, + "memuse": { + "type": "integer" + }, + "byterange": { + "type": "object", + "properties": { + "memcap": { + "type": "integer" + }, + "memuse": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "host": { + "type": "object", + "properties": { + "memcap": { + "type": "integer" + }, + "memuse": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "ippair": { "type": "object", "properties": { "memcap": { diff --git a/src/app-layer-ftp.c b/src/app-layer-ftp.c index a1a99d4bd701..15238b9f65ef 100644 --- a/src/app-layer-ftp.c +++ b/src/app-layer-ftp.c @@ -174,6 +174,16 @@ uint64_t FTPMemcapGlobalCounter(void) return tmpval; } +int FTPSetMemcap(uint64_t size) +{ + if ((uint64_t)SC_ATOMIC_GET(ftp_memcap) < size) { + SC_ATOMIC_SET(ftp_memcap, size); + return 1; + } + + return 0; +} + /** * \brief Check if alloc'ing "size" would mean we're over memcap * diff --git a/src/app-layer-ftp.h b/src/app-layer-ftp.h index 5be11d81f81c..e69415d8cf13 100644 --- a/src/app-layer-ftp.h +++ b/src/app-layer-ftp.h @@ -185,6 +185,7 @@ typedef struct FtpDataState_ { void RegisterFTPParsers(void); void FTPParserRegisterTests(void); void FTPParserCleanup(void); +int FTPSetMemcap(uint64_t size); uint64_t FTPMemuseGlobalCounter(void); uint64_t FTPMemcapGlobalCounter(void); diff --git a/src/app-layer-htp-range.c b/src/app-layer-htp-range.c index b1f2b62423fc..9e8a4e1e641f 100644 --- a/src/app-layer-htp-range.c +++ b/src/app-layer-htp-range.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Open Information Security Foundation +/* 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 @@ -42,6 +42,28 @@ static void HttpRangeBlockDerefContainer(HttpRangeContainerBlock *b); #define CONTAINER_URLRANGE_HASH_SIZE 256 +int HTPByteRangeSetMemcap(uint64_t size) +{ + if (size == 0 || (uint64_t)SC_ATOMIC_GET(ContainerUrlRangeList.ht->memuse) < size) { + SC_ATOMIC_SET(ContainerUrlRangeList.ht->config.memcap, size); + return 1; + } + + return 0; +} + +uint64_t HTPByteRangeMemcapGlobalCounter(void) +{ + uint64_t tmpval = SC_ATOMIC_GET(ContainerUrlRangeList.ht->config.memcap); + return tmpval; +} + +uint64_t HTPByteRangeMemuseGlobalCounter(void) +{ + uint64_t tmpval = SC_ATOMIC_GET(ContainerUrlRangeList.ht->memuse); + return tmpval; +} + int HttpRangeContainerBufferCompare(HttpRangeContainerBuffer *a, HttpRangeContainerBuffer *b) { // lexical order : start, buflen, offset diff --git a/src/app-layer-htp-range.h b/src/app-layer-htp-range.h index fb0dc5b76875..8fb561020b0a 100644 --- a/src/app-layer-htp-range.h +++ b/src/app-layer-htp-range.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Open Information Security Foundation +/* 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 @@ -111,4 +111,8 @@ HttpRangeContainerBlock *HttpRangeContainerOpenFile(const unsigned char *key, ui void HttpRangeFreeBlock(HttpRangeContainerBlock *b); +uint64_t HTPByteRangeMemcapGlobalCounter(void); +uint64_t HTPByteRangeMemuseGlobalCounter(void); +int HTPByteRangeSetMemcap(uint64_t); + #endif /* SURICATA_APP_LAYER_HTP_RANGE_H */ diff --git a/src/app-layer.c b/src/app-layer.c index 94f99f44f83e..9654c7d82e64 100644 --- a/src/app-layer.c +++ b/src/app-layer.c @@ -31,6 +31,7 @@ #include "app-layer-protos.h" #include "app-layer-expectation.h" #include "app-layer-ftp.h" +#include "app-layer-htp-range.h" #include "app-layer-detect-proto.h" #include "app-layer-frames.h" #include "stream-tcp-reassemble.h" @@ -1113,6 +1114,12 @@ void AppLayerRegisterGlobalCounters(void) StatsRegisterGlobalCounter("ftp.memuse", FTPMemuseGlobalCounter); StatsRegisterGlobalCounter("ftp.memcap", FTPMemcapGlobalCounter); StatsRegisterGlobalCounter("app_layer.expectations", ExpectationGetCounter); + StatsRegisterGlobalCounter("http.byterange.memuse", HTPByteRangeMemuseGlobalCounter); + StatsRegisterGlobalCounter("http.byterange.memcap", HTPByteRangeMemcapGlobalCounter); + StatsRegisterGlobalCounter("ippair.memuse", IPPairGetMemuse); + StatsRegisterGlobalCounter("ippair.memcap", IPPairGetMemuse); + StatsRegisterGlobalCounter("host.memuse", HostGetMemuse); + StatsRegisterGlobalCounter("host.memcap", HostGetMemcap); } static bool IsAppLayerErrorExceptionPolicyStatsValid(enum ExceptionPolicy policy) diff --git a/src/runmode-unix-socket.c b/src/runmode-unix-socket.c index bdb9156d5b0d..3c390e99a6db 100644 --- a/src/runmode-unix-socket.c +++ b/src/runmode-unix-socket.c @@ -44,7 +44,9 @@ #include "defrag-hash.h" #include "ippair.h" #include "app-layer.h" +#include "app-layer-ftp.h" #include "app-layer-htp-mem.h" +#include "app-layer-htp-range.h" #include "host-bit.h" #include "util-misc.h" @@ -97,9 +99,12 @@ static MemcapCommand memcaps[] = { StreamTcpReassembleMemuseGlobalCounter }, { "flow", FlowSetMemcap, FlowGetMemcap, FlowGetMemuse }, { "applayer-proto-http", HTPSetMemcap, HTPGetMemcap, HTPMemuseGlobalCounter }, + { "applayer-proto-http-byterange", HTPByteRangeSetMemcap, HTPByteRangeMemcapGlobalCounter, + HTPByteRangeMemuseGlobalCounter }, { "defrag", DefragTrackerSetMemcap, DefragTrackerGetMemcap, DefragTrackerGetMemuse }, { "ippair", IPPairSetMemcap, IPPairGetMemcap, IPPairGetMemuse }, { "host", HostSetMemcap, HostGetMemcap, HostGetMemuse }, + { "ftp", FTPSetMemcap, FTPMemcapGlobalCounter, FTPMemuseGlobalCounter }, }; float MemcapsGetPressure(void) From fabd167652f133e9f252a600e1ad8f3b4f8ce1bb Mon Sep 17 00:00:00 2001 From: Jason Ish Date: Thu, 10 Oct 2024 16:06:09 -0600 Subject: [PATCH 04/11] flow: add callbacks for flow init and flow updates Adds user registerable callbacks for flow initialization, flow update and flow finish. Some plugins, such as other DPI libraries like nDPI need a way to hook into these flow lifecycle events. Ticket: #7319 Ticket: #7320 --- src/Makefile.am | 2 + src/flow-callbacks.c | 129 +++++++++++++++++++++++++++++++++++++++++++ src/flow-callbacks.h | 121 ++++++++++++++++++++++++++++++++++++++++ src/flow-hash.c | 8 ++- src/flow-manager.c | 3 +- src/flow-util.c | 5 +- src/flow-util.h | 2 +- src/flow.c | 3 + 8 files changed, 267 insertions(+), 6 deletions(-) create mode 100644 src/flow-callbacks.c create mode 100644 src/flow-callbacks.h diff --git a/src/Makefile.am b/src/Makefile.am index 7d05751ec254..f8e1e44d4aff 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -317,6 +317,7 @@ noinst_HEADERS = \ feature.h \ flow-bit.h \ flow-bypass.h \ + flow-callbacks.h \ flow.h \ flow-hash.h \ flow-manager.h \ @@ -875,6 +876,7 @@ libsuricata_c_a_SOURCES = \ feature.c \ flow-bit.c \ flow-bypass.c \ + flow-callbacks.c \ flow.c \ flow-hash.c \ flow-manager.c \ diff --git a/src/flow-callbacks.c b/src/flow-callbacks.c new file mode 100644 index 000000000000..30e703c3efb3 --- /dev/null +++ b/src/flow-callbacks.c @@ -0,0 +1,129 @@ +/* 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. + */ + +#include "flow-callbacks.h" + +typedef struct FlowInitCallback_ { + SCFlowInitCallbackFn Callback; + void *user; + struct FlowInitCallback_ *next; +} FlowInitCallback; + +static FlowInitCallback *init_callbacks = NULL; + +typedef struct FlowUpdateCallback_ { + SCFlowUpdateCallbackFn Callback; + void *user; + struct FlowUpdateCallback_ *next; +} FlowUpdateCallback; + +static FlowUpdateCallback *update_callbacks = NULL; + +typedef struct FlowFinishCallback_ { + SCFlowFinishCallbackFn Callback; + void *user; + struct FlowFinishCallback_ *next; +} FlowFinishCallback; + +static FlowFinishCallback *finish_callbacks = NULL; + +bool SCFlowRegisterInitCallback(SCFlowInitCallbackFn fn, void *user) +{ + FlowInitCallback *cb = SCCalloc(1, sizeof(*cb)); + if (cb == NULL) { + return false; + } + cb->Callback = fn; + cb->user = user; + if (init_callbacks == NULL) { + init_callbacks = cb; + } else { + FlowInitCallback *current = init_callbacks; + while (current->next != NULL) { + current = current->next; + } + current->next = cb; + } + return true; +} + +void SCFlowRunInitCallbacks(ThreadVars *tv, Flow *f, const Packet *p) +{ + FlowInitCallback *cb = init_callbacks; + while (cb != NULL) { + cb->Callback(tv, f, p, cb->user); + cb = cb->next; + } +} + +bool SCFlowRegisterUpdateCallback(SCFlowUpdateCallbackFn fn, void *user) +{ + FlowUpdateCallback *cb = SCCalloc(1, sizeof(*cb)); + if (cb == NULL) { + return false; + } + cb->Callback = fn; + cb->user = user; + if (update_callbacks == NULL) { + update_callbacks = cb; + } else { + FlowUpdateCallback *current = update_callbacks; + while (current->next != NULL) { + current = current->next; + } + current->next = cb; + } + return true; +} + +void SCFlowRunUpdateCallbacks(ThreadVars *tv, Flow *f, Packet *p) +{ + FlowUpdateCallback *cb = update_callbacks; + while (cb != NULL) { + cb->Callback(tv, f, p, cb->user); + cb = cb->next; + } +} + +bool SCFlowRegisterFinishCallback(SCFlowFinishCallbackFn fn, void *user) +{ + FlowFinishCallback *cb = SCCalloc(1, sizeof(*cb)); + if (cb == NULL) { + return false; + } + cb->Callback = fn; + cb->user = user; + if (finish_callbacks == NULL) { + finish_callbacks = cb; + } else { + FlowFinishCallback *current = finish_callbacks; + while (current->next != NULL) { + current = current->next; + } + current->next = cb; + } + return true; +} + +void SCFlowRunFinishCallbacks(ThreadVars *tv, Flow *f) +{ + FlowFinishCallback *cb = finish_callbacks; + while (cb != NULL) { + cb->Callback(tv, f, cb->user); + cb = cb->next; + } +} diff --git a/src/flow-callbacks.h b/src/flow-callbacks.h new file mode 100644 index 000000000000..4c694807753f --- /dev/null +++ b/src/flow-callbacks.h @@ -0,0 +1,121 @@ +/* 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. + */ + +#ifndef SURICATA_FLOW_CALLBACKS_H +#define SURICATA_FLOW_CALLBACKS_H + +#include "suricata-common.h" +#include "flow.h" + +/** \brief Function type for flow initialization callbacks. + * + * Once registered with SCFlowRegisterInitCallback, this function will + * be called every time a flow is initialized, or in other words, + * every time Suricata picks up a flow. + * + * \param tv The ThreadVars data structure for the thread creating the + * flow. + * \param f The newly initialized flow. + * \param p The packet related to creating the new flow. + * \param user The user data provided during callback registration. + */ +typedef void (*SCFlowInitCallbackFn)(ThreadVars *tv, Flow *f, const Packet *p, void *user); + +/** \brief Register a flow init callback. + * + * Register a user provided function to be called every time a flow is + * initialized for use. + * + * \param fn Pointer to function to be called + * \param user Additional user data to be passed to callback + * + * \returns true if callback was registered, otherwise false if the + * callback could not be registered due to memory allocation error. + */ +bool SCFlowRegisterInitCallback(SCFlowInitCallbackFn fn, void *user); + +/** \internal + * + * Run all registered flow init callbacks. + */ +void SCFlowRunInitCallbacks(ThreadVars *tv, Flow *f, const Packet *p); + +/** \brief Function type for flow update callbacks. + * + * Once registered with SCFlowRegisterUpdateCallback, this function + * will be called every time a flow is updated by a packet (basically + * everytime a packet is seen on a flow). + * + * \param tv The ThreadVars data structure for the thread updating the + * flow. + * \param f The flow being updated. + * \param p The packet responsible for the flow update. + * \param user The user data provided during callback registration. + */ +typedef void (*SCFlowUpdateCallbackFn)(ThreadVars *tv, Flow *f, Packet *p, void *user); + +/** \brief Register a flow update callback. + * + * Register a user provided function to be called everytime a flow is + * updated. + * + * \param fn Pointer to function to be called + * \param user Additional user data to be passed to callback + * + * \returns true if callback was registered, otherwise false if the + * callback could not be registered due to memory allocation error. + */ +bool SCFlowRegisterUpdateCallback(SCFlowUpdateCallbackFn fn, void *user); + +/** \internal + * + * Run all registered flow update callbacks. + */ +void SCFlowRunUpdateCallbacks(ThreadVars *tv, Flow *f, Packet *p); + +/** \brief Function type for flow finish callbacks. + * + * Once registered with SCFlowRegisterFinshCallback, this function + * will be called when Suricata is done with a flow. + * + * \param tv The ThreadVars data structure for the thread finishing + * the flow. + * \param f The flow being finshed. + * \param user The user data provided during callback registration. + */ +typedef void (*SCFlowFinishCallbackFn)(ThreadVars *tv, Flow *f, void *user); + +/** \brief Register a flow init callback. + * + * Register a user provided function to be called every time a flow is + * finished. + * + * \param fn Pointer to function to be called + * \param user Additional user data to be passed to callback + * + * \returns true if callback was registered, otherwise false if the + * callback could not be registered due to memory allocation error. + */ +bool SCFlowRegisterFinishCallback(SCFlowFinishCallbackFn fn, void *user); + +/** \internal + * + * Run all registered flow init callbacks. + */ +void SCFlowRunFinishCallbacks(ThreadVars *tv, Flow *f); + +#endif /* SURICATA_FLOW_CALLBACKS_H */ diff --git a/src/flow-hash.c b/src/flow-hash.c index ddab01cd5b69..fcd957c72e27 100644 --- a/src/flow-hash.c +++ b/src/flow-hash.c @@ -38,6 +38,7 @@ #include "flow-storage.h" #include "flow-timeout.h" #include "flow-spare-pool.h" +#include "flow-callbacks.h" #include "app-layer-parser.h" #include "util-time.h" @@ -781,7 +782,7 @@ static Flow *TcpReuseReplace(ThreadVars *tv, FlowLookupStruct *fls, FlowBucket * fb->head = f; /* initialize and return */ - FlowInit(f, p); + FlowInit(tv, f, p); f->flow_hash = hash; f->fb = fb; FlowUpdateState(f, FLOW_STATE_NEW); @@ -886,7 +887,7 @@ Flow *FlowGetFlowFromHash(ThreadVars *tv, FlowLookupStruct *fls, Packet *p, Flow fb->head = f; /* got one, now lock, initialize and return */ - FlowInit(f, p); + FlowInit(tv, f, p); f->flow_hash = hash; f->fb = fb; FlowUpdateState(f, FLOW_STATE_NEW); @@ -951,7 +952,7 @@ Flow *FlowGetFlowFromHash(ThreadVars *tv, FlowLookupStruct *fls, Packet *p, Flow fb->head = f; /* initialize and return */ - FlowInit(f, p); + FlowInit(tv, f, p); f->flow_hash = hash; f->fb = fb; FlowUpdateState(f, FLOW_STATE_NEW); @@ -1242,6 +1243,7 @@ static Flow *FlowGetUsedFlow(ThreadVars *tv, DecodeThreadVars *dtv, const SCTime } #endif + SCFlowRunFinishCallbacks(tv, f); FlowClearMemory(f, f->protomap); /* leave locked */ diff --git a/src/flow-manager.c b/src/flow-manager.c index 05b791ee612e..9da986b22df6 100644 --- a/src/flow-manager.c +++ b/src/flow-manager.c @@ -39,6 +39,7 @@ #include "flow-manager.h" #include "flow-storage.h" #include "flow-spare-pool.h" +#include "flow-callbacks.h" #include "stream-tcp.h" #include "stream-tcp-cache.h" @@ -1059,7 +1060,7 @@ static void Recycler(ThreadVars *tv, FlowRecyclerThreadData *ftd, Flow *f) StatsDecr(tv, ftd->counter_tcp_active_sessions); } StatsDecr(tv, ftd->counter_flow_active); - + SCFlowRunFinishCallbacks(tv, f); FlowClearMemory(f, f->protomap); FLOWLOCK_UNLOCK(f); } diff --git a/src/flow-util.c b/src/flow-util.c index 7e11da41f527..31e22b9341ac 100644 --- a/src/flow-util.c +++ b/src/flow-util.c @@ -29,6 +29,7 @@ #include "flow.h" #include "flow-private.h" #include "flow-util.h" +#include "flow-callbacks.h" #include "flow-var.h" #include "app-layer.h" @@ -142,7 +143,7 @@ static inline void FlowSetICMPv6CounterPart(Flow *f) /* initialize the flow from the first packet * we see from it. */ -void FlowInit(Flow *f, const Packet *p) +void FlowInit(ThreadVars *tv, Flow *f, const Packet *p) { SCEnter(); SCLogDebug("flow %p", f); @@ -203,6 +204,8 @@ void FlowInit(Flow *f, const Packet *p) FlowSetStorageById(f, MacSetGetFlowStorageID(), ms); } + SCFlowRunInitCallbacks(tv, f, p); + SCReturn; } diff --git a/src/flow-util.h b/src/flow-util.h index 2d813bd9ee4d..368c955d876a 100644 --- a/src/flow-util.h +++ b/src/flow-util.h @@ -140,7 +140,7 @@ Flow *FlowAlloc(void); void FlowFree(Flow *); uint8_t FlowGetProtoMapping(uint8_t); -void FlowInit(Flow *, const Packet *); +void FlowInit(ThreadVars *, Flow *, const Packet *); uint8_t FlowGetReverseProtoMapping(uint8_t rproto); /* flow end counter logic */ diff --git a/src/flow.c b/src/flow.c index 7bfa80ea0a9b..aea79d23bf08 100644 --- a/src/flow.c +++ b/src/flow.c @@ -44,6 +44,7 @@ #include "flow-storage.h" #include "flow-bypass.h" #include "flow-spare-pool.h" +#include "flow-callbacks.h" #include "stream-tcp-private.h" @@ -503,6 +504,8 @@ void FlowHandlePacketUpdate(Flow *f, Packet *p, ThreadVars *tv, DecodeThreadVars SCLogDebug("setting FLOW_NOPAYLOAD_INSPECTION flag on flow %p", f); DecodeSetNoPayloadInspectionFlag(p); } + + SCFlowRunUpdateCallbacks(tv, f, p); } /** \brief Entry point for packet flow handling From befc7fda0fffb3c01dd818a575094ac3147b68c5 Mon Sep 17 00:00:00 2001 From: Jason Ish Date: Fri, 11 Oct 2024 11:41:47 -0600 Subject: [PATCH 05/11] threads: add storage api, based on flow storage --- src/Makefile.am | 2 + src/thread-storage.c | 212 +++++++++++++++++++++++++++++++++++++++++++ src/thread-storage.h | 45 +++++++++ src/threads.c | 2 + src/threadvars.h | 2 + src/tm-threads.c | 5 +- src/util-storage.c | 2 + src/util-storage.h | 1 + 8 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 src/thread-storage.c create mode 100644 src/thread-storage.h diff --git a/src/Makefile.am b/src/Makefile.am index f8e1e44d4aff..6032c1962dcc 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -438,6 +438,7 @@ noinst_HEADERS = \ suricata-common.h \ suricata.h \ suricata-plugin.h \ + thread-storage.h \ threads-debug.h \ threads.h \ threads-profile.h \ @@ -990,6 +991,7 @@ libsuricata_c_a_SOURCES = \ stream-tcp-sack.c \ stream-tcp-util.c \ suricata.c \ + thread-storage.c \ threads.c \ tm-modules.c \ tmqh-flow.c \ diff --git a/src/thread-storage.c b/src/thread-storage.c new file mode 100644 index 000000000000..977f4fde9752 --- /dev/null +++ b/src/thread-storage.c @@ -0,0 +1,212 @@ +/* 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. + */ + +#include "suricata-common.h" +#include "thread-storage.h" +#include "util-storage.h" +#include "util-unittest.h" + +const StorageEnum storage_type = STORAGE_THREAD; + +unsigned int ThreadStorageSize(void) +{ + return StorageGetSize(storage_type); +} + +void *ThreadGetStorageById(const ThreadVars *tv, ThreadStorageId id) +{ + return StorageGetById(tv->storage, storage_type, id.id); +} + +int ThreadSetStorageById(ThreadVars *tv, ThreadStorageId id, void *ptr) +{ + return StorageSetById(tv->storage, storage_type, id.id, ptr); +} + +void *ThreadAllocStorageById(ThreadVars *tv, ThreadStorageId id) +{ + return StorageAllocByIdPrealloc(tv->storage, storage_type, id.id); +} + +void ThreadFreeStorageById(ThreadVars *tv, ThreadStorageId id) +{ + StorageFreeById(tv->storage, storage_type, id.id); +} + +void ThreadFreeStorage(ThreadVars *tv) +{ + if (ThreadStorageSize() > 0) + StorageFreeAll(tv->storage, storage_type); +} + +ThreadStorageId ThreadStorageRegister(const char *name, const unsigned int size, + void *(*Alloc)(unsigned int), void (*Free)(void *)) +{ + int id = StorageRegister(storage_type, name, size, Alloc, Free); + ThreadStorageId tsi = { .id = id }; + return tsi; +} + +#ifdef UNITTESTS + +static void *StorageTestAlloc(unsigned int size) +{ + return SCCalloc(1, size); +} + +static void StorageTestFree(void *x) +{ + SCFree(x); +} + +static int ThreadStorageTest01(void) +{ + StorageInit(); + + ThreadStorageId id1 = ThreadStorageRegister("test", 8, StorageTestAlloc, StorageTestFree); + FAIL_IF(id1.id < 0); + + ThreadStorageId id2 = ThreadStorageRegister("variable", 24, StorageTestAlloc, StorageTestFree); + FAIL_IF(id2.id < 0); + + ThreadStorageId id3 = + ThreadStorageRegister("store", sizeof(void *), StorageTestAlloc, StorageTestFree); + FAIL_IF(id3.id < 0); + + FAIL_IF(StorageFinalize() < 0); + + ThreadVars *tv = SCCalloc(1, sizeof(ThreadVars) + ThreadStorageSize()); + FAIL_IF_NULL(tv); + + void *ptr = ThreadGetStorageById(tv, id1); + FAIL_IF_NOT_NULL(ptr); + + ptr = ThreadGetStorageById(tv, id2); + FAIL_IF_NOT_NULL(ptr); + + ptr = ThreadGetStorageById(tv, id3); + FAIL_IF_NOT_NULL(ptr); + + void *ptr1a = ThreadAllocStorageById(tv, id1); + FAIL_IF_NULL(ptr1a); + + void *ptr2a = ThreadAllocStorageById(tv, id2); + FAIL_IF_NULL(ptr2a); + + void *ptr3a = ThreadAllocStorageById(tv, id3); + FAIL_IF_NULL(ptr3a); + + void *ptr1b = ThreadGetStorageById(tv, id1); + FAIL_IF(ptr1a != ptr1b); + + void *ptr2b = ThreadGetStorageById(tv, id2); + FAIL_IF(ptr2a != ptr2b); + + void *ptr3b = ThreadGetStorageById(tv, id3); + FAIL_IF(ptr3a != ptr3b); + + ThreadFreeStorage(tv); + StorageCleanup(); + SCFree(tv); + PASS; +} + +static int ThreadStorageTest02(void) +{ + StorageInit(); + + ThreadStorageId id1 = ThreadStorageRegister("test", sizeof(void *), NULL, StorageTestFree); + FAIL_IF(id1.id < 0); + + FAIL_IF(StorageFinalize() < 0); + + ThreadVars *tv = SCCalloc(1, sizeof(ThreadVars) + ThreadStorageSize()); + FAIL_IF_NULL(tv); + + void *ptr = ThreadGetStorageById(tv, id1); + FAIL_IF_NOT_NULL(ptr); + + void *ptr1a = SCMalloc(128); + FAIL_IF_NULL(ptr1a); + + ThreadSetStorageById(tv, id1, ptr1a); + + void *ptr1b = ThreadGetStorageById(tv, id1); + FAIL_IF(ptr1a != ptr1b); + + ThreadFreeStorage(tv); + StorageCleanup(); + PASS; +} + +static int ThreadStorageTest03(void) +{ + StorageInit(); + + ThreadStorageId id1 = ThreadStorageRegister("test1", sizeof(void *), NULL, StorageTestFree); + FAIL_IF(id1.id < 0); + + ThreadStorageId id2 = ThreadStorageRegister("test2", sizeof(void *), NULL, StorageTestFree); + FAIL_IF(id2.id < 0); + + ThreadStorageId id3 = ThreadStorageRegister("test3", 32, StorageTestAlloc, StorageTestFree); + FAIL_IF(id3.id < 0); + + FAIL_IF(StorageFinalize() < 0); + + ThreadVars *tv = SCCalloc(1, sizeof(ThreadVars) + ThreadStorageSize()); + FAIL_IF_NULL(tv); + + void *ptr = ThreadGetStorageById(tv, id1); + FAIL_IF_NOT_NULL(ptr); + + void *ptr1a = SCMalloc(128); + FAIL_IF_NULL(ptr1a); + + ThreadSetStorageById(tv, id1, ptr1a); + + void *ptr2a = SCMalloc(256); + FAIL_IF_NULL(ptr2a); + + ThreadSetStorageById(tv, id2, ptr2a); + + void *ptr3a = ThreadAllocStorageById(tv, id3); + FAIL_IF_NULL(ptr3a); + + void *ptr1b = ThreadGetStorageById(tv, id1); + FAIL_IF(ptr1a != ptr1b); + + void *ptr2b = ThreadGetStorageById(tv, id2); + FAIL_IF(ptr2a != ptr2b); + + void *ptr3b = ThreadGetStorageById(tv, id3); + FAIL_IF(ptr3a != ptr3b); + + ThreadFreeStorage(tv); + StorageCleanup(); + PASS; +} +#endif + +void RegisterThreadStorageTests(void) +{ +#ifdef UNITTESTS + UtRegisterTest("ThreadStorageTest01", ThreadStorageTest01); + UtRegisterTest("ThreadStorageTest02", ThreadStorageTest02); + UtRegisterTest("ThreadStorageTest03", ThreadStorageTest03); +#endif +} diff --git a/src/thread-storage.h b/src/thread-storage.h new file mode 100644 index 000000000000..5dd22570b0fa --- /dev/null +++ b/src/thread-storage.h @@ -0,0 +1,45 @@ +/* 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. + */ + +/** + * Thread wrapper around storage API. + */ + +#ifndef SURICATA_THREAD_STORAGE_H +#define SURICATA_THREAD_STORAGE_H + +#include "threadvars.h" + +typedef struct ThreadStorageId { + int id; +} ThreadStorageId; + +unsigned int ThreadStorageSize(void); + +void *ThreadGetStorageById(const ThreadVars *tv, ThreadStorageId id); +int ThreadSetStorageById(ThreadVars *tv, ThreadStorageId id, void *ptr); +void *ThreadAllocStorageById(ThreadVars *tv, ThreadStorageId id); + +void ThreadFreeStorageById(ThreadVars *tv, ThreadStorageId id); +void ThreadFreeStorage(ThreadVars *tv); + +void RegisterThreadStorageTests(void); + +ThreadStorageId ThreadStorageRegister(const char *name, const unsigned int size, + void *(*Alloc)(unsigned int), void (*Free)(void *)); + +#endif /* SURICATA_THREAD_STORAGE_H */ diff --git a/src/threads.c b/src/threads.c index 1708a8f5cd37..919e6422e32f 100644 --- a/src/threads.c +++ b/src/threads.c @@ -25,6 +25,7 @@ */ #include "suricata-common.h" +#include "thread-storage.h" #include "util-unittest.h" #include "util-debug.h" #include "threads.h" @@ -149,5 +150,6 @@ void ThreadMacrosRegisterTests(void) UtRegisterTest("ThreadMacrosTest03RWLocks", ThreadMacrosTest03RWLocks); UtRegisterTest("ThreadMacrosTest04RWLocks", ThreadMacrosTest04RWLocks); // UtRegisterTest("ThreadMacrosTest05RWLocks", ThreadMacrosTest05RWLocks); + RegisterThreadStorageTests(); #endif /* UNIT TESTS */ } diff --git a/src/threadvars.h b/src/threadvars.h index cebcdb4e3ac1..6f339e9839d5 100644 --- a/src/threadvars.h +++ b/src/threadvars.h @@ -28,6 +28,7 @@ #include "counters.h" #include "packet-queue.h" #include "util-atomic.h" +#include "util-storage.h" struct TmSlot_; @@ -135,6 +136,7 @@ typedef struct ThreadVars_ { struct FlowQueue_ *flow_queue; bool break_loop; + Storage storage[]; } ThreadVars; /** Thread setup flags: */ diff --git a/src/tm-threads.c b/src/tm-threads.c index b0d0f8686ba0..c65995ad351b 100644 --- a/src/tm-threads.c +++ b/src/tm-threads.c @@ -30,6 +30,7 @@ #include "stream.h" #include "runmodes.h" #include "threadvars.h" +#include "thread-storage.h" #include "tm-queues.h" #include "tm-queuehandlers.h" #include "tm-threads.h" @@ -919,7 +920,7 @@ ThreadVars *TmThreadCreate(const char *name, const char *inq_name, const char *i SCLogDebug("creating thread \"%s\"...", name); /* XXX create separate function for this: allocate a thread container */ - tv = SCCalloc(1, sizeof(ThreadVars)); + tv = SCCalloc(1, sizeof(ThreadVars) + ThreadStorageSize()); if (unlikely(tv == NULL)) goto error; @@ -1577,6 +1578,8 @@ static void TmThreadFree(ThreadVars *tv) SCLogDebug("Freeing thread '%s'.", tv->name); + ThreadFreeStorage(tv); + if (tv->flow_queue) { BUG_ON(tv->flow_queue->qlen != 0); SCFree(tv->flow_queue); diff --git a/src/util-storage.c b/src/util-storage.c index 02f69a568cd2..bae251432315 100644 --- a/src/util-storage.c +++ b/src/util-storage.c @@ -59,6 +59,8 @@ static const char *StoragePrintType(StorageEnum type) return "ippair"; case STORAGE_DEVICE: return "livedevice"; + case STORAGE_THREAD: + return "thread"; case STORAGE_MAX: return "max"; } diff --git a/src/util-storage.h b/src/util-storage.h index 11d64bdbecbd..fce1f964eb14 100644 --- a/src/util-storage.h +++ b/src/util-storage.h @@ -31,6 +31,7 @@ typedef enum StorageEnum_ { STORAGE_FLOW, STORAGE_IPPAIR, STORAGE_DEVICE, + STORAGE_THREAD, STORAGE_MAX, } StorageEnum; From e7fd6e6216ed0ccf3d3a7073c5e15d0c0a9acb70 Mon Sep 17 00:00:00 2001 From: Jason Ish Date: Fri, 11 Oct 2024 11:48:50 -0600 Subject: [PATCH 06/11] threads: add initialization callbacks For library users and plugins that need to hook into the thread life cycle, perhaps to initialize some thread storage. --- src/Makefile.am | 2 ++ src/thread-callbacks.c | 55 ++++++++++++++++++++++++++++++++++++++++++ src/thread-callbacks.h | 54 +++++++++++++++++++++++++++++++++++++++++ src/tm-threads.c | 3 +++ 4 files changed, 114 insertions(+) create mode 100644 src/thread-callbacks.c create mode 100644 src/thread-callbacks.h diff --git a/src/Makefile.am b/src/Makefile.am index 6032c1962dcc..aaca85965435 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -438,6 +438,7 @@ noinst_HEADERS = \ suricata-common.h \ suricata.h \ suricata-plugin.h \ + thread-callbacks.h \ thread-storage.h \ threads-debug.h \ threads.h \ @@ -991,6 +992,7 @@ libsuricata_c_a_SOURCES = \ stream-tcp-sack.c \ stream-tcp-util.c \ suricata.c \ + thread-callbacks.c \ thread-storage.c \ threads.c \ tm-modules.c \ diff --git a/src/thread-callbacks.c b/src/thread-callbacks.c new file mode 100644 index 000000000000..ede35d7107ce --- /dev/null +++ b/src/thread-callbacks.c @@ -0,0 +1,55 @@ +/* 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. + */ + +#include "thread-callbacks.h" + +typedef struct ThreadInitCallback_ { + SCThreadInitCallbackFn Callback; + void *user; + struct ThreadInitCallback_ *next; +} ThreadInitCallback; + +static ThreadInitCallback *init_callbacks = NULL; + +bool SCThreadRegisterInitCallback(SCThreadInitCallbackFn fn, void *user) +{ + ThreadInitCallback *cb = SCCalloc(1, sizeof(*cb)); + if (cb == NULL) { + return false; + } + cb->Callback = fn; + cb->user = user; + if (init_callbacks == NULL) { + init_callbacks = cb; + } else { + ThreadInitCallback *current = init_callbacks; + while (current->next != NULL) { + current = current->next; + } + current->next = cb; + } + return true; +} + +void SCThreadRunInitCallbacks(ThreadVars *tv) +{ + ThreadInitCallback *cb = init_callbacks; + while (cb != NULL) { + cb->Callback(tv, cb->user); + cb = cb->next; + } +} diff --git a/src/thread-callbacks.h b/src/thread-callbacks.h new file mode 100644 index 000000000000..5bcd638bf86b --- /dev/null +++ b/src/thread-callbacks.h @@ -0,0 +1,54 @@ +/* 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. + */ + +#ifndef SURICATA_THREAD_CALLBACKS_H +#define SURICATA_THREAD_CALLBACKS_H + +#include "suricata-common.h" +#include "threadvars.h" + +/** \brief Function type for thread intialization callbacks. + * + * Once registered by SCThreadRegisterInitCallback, this function will + * be called for every thread being initialized during Suricata + * startup. + * + * \param tv The ThreadVars struct that has just been initialized. + * \param user The user data provided when registering the callback. + */ +typedef void (*SCThreadInitCallbackFn)(ThreadVars *tv, void *user); + +/** \brief Register a thread init callback. + * + * Register a user provided function to be called every time a thread is + * initialized for use. + * + * \param fn Pointer to function to be called + * \param user Additional user data to be passed to callback + * + * \returns true if callback was registered, otherwise false if the + * callback could not be registered due to memory allocation error. + */ +bool SCThreadRegisterInitCallback(SCThreadInitCallbackFn fn, void *user); + +/** \internal + * + * Run all registered flow init callbacks. + */ +void SCThreadRunInitCallbacks(ThreadVars *tv); + +#endif /* SURICATA_THREAD_CALLBACKS_H */ diff --git a/src/tm-threads.c b/src/tm-threads.c index c65995ad351b..07f9a9390df0 100644 --- a/src/tm-threads.c +++ b/src/tm-threads.c @@ -29,6 +29,7 @@ #include "suricata.h" #include "stream.h" #include "runmodes.h" +#include "thread-callbacks.h" #include "threadvars.h" #include "thread-storage.h" #include "tm-queues.h" @@ -1012,6 +1013,8 @@ ThreadVars *TmThreadCreate(const char *name, const char *inq_name, const char *i if (mucond != 0) TmThreadInitMC(tv); + SCThreadRunInitCallbacks(tv); + return tv; error: From f28dd1189fd9cee3809007ab8b3781c87ad3e4ea Mon Sep 17 00:00:00 2001 From: Jason Ish Date: Fri, 11 Oct 2024 13:21:14 -0600 Subject: [PATCH 07/11] eve: user callbacks for adding additional data Provide a way for library/plugin users to register a callback that will be called prior to an EVE record being closed. The callback will be passed ThreadVars, Packet, and Flow pointers if available, as well as private user data. --- src/output-eve-stream.c | 2 +- src/output-eve.c | 39 ++++++++++++++++++++++++++++++++++ src/output-eve.h | 43 ++++++++++++++++++++++++++++++++++++++ src/output-json-alert.c | 6 +++--- src/output-json-anomaly.c | 21 +++++++++---------- src/output-json-arp.c | 2 +- src/output-json-dcerpc.c | 2 +- src/output-json-dhcp.c | 2 +- src/output-json-dnp3.c | 4 ++-- src/output-json-dns.c | 8 +++---- src/output-json-drop.c | 6 +++--- src/output-json-file.c | 8 +++---- src/output-json-flow.c | 2 +- src/output-json-frame.c | 10 ++++----- src/output-json-http.c | 2 +- src/output-json-ike.c | 2 +- src/output-json-metadata.c | 2 +- src/output-json-mqtt.c | 2 +- src/output-json-netflow.c | 4 ++-- src/output-json-nfs.c | 2 +- src/output-json-pgsql.c | 2 +- src/output-json-smb.c | 2 +- src/output-json-smtp.c | 2 +- src/output-json-tls.c | 2 +- src/output-json.c | 5 ++++- src/output-json.h | 3 ++- src/output.c | 2 +- 27 files changed, 136 insertions(+), 51 deletions(-) diff --git a/src/output-eve-stream.c b/src/output-eve-stream.c index fcdf0c2e5c0b..4b44d86835e7 100644 --- a/src/output-eve-stream.c +++ b/src/output-eve-stream.c @@ -425,7 +425,7 @@ static int EveStreamLogger(ThreadVars *tv, void *thread_data, const Packet *p) /* Close stream. */ jb_close(js); - OutputJsonBuilderBuffer(js, td->ctx); + OutputJsonBuilderBuffer(tv, p, p->flow, js, td->ctx); jb_free(js); return TM_ECODE_OK; diff --git a/src/output-eve.c b/src/output-eve.c index d0d775cba7f7..2c67f3b6c38a 100644 --- a/src/output-eve.c +++ b/src/output-eve.c @@ -15,11 +15,50 @@ * 02110-1301, USA. */ +#include "suricata-common.h" #include "output-eve.h" #include "util-debug.h" +#include "rust.h" + +typedef struct EveUserCallback_ { + SCEveUserCallbackFn Callback; + void *user; + struct EveUserCallback_ *next; +} EveUserCallback; + +static EveUserCallback *eve_user_callbacks = NULL; static TAILQ_HEAD(, SCEveFileType_) output_types = TAILQ_HEAD_INITIALIZER(output_types); +bool SCEveRegisterCallback(SCEveUserCallbackFn fn, void *user) +{ + EveUserCallback *cb = SCCalloc(1, sizeof(*cb)); + if (cb == NULL) { + return false; + } + cb->Callback = fn; + cb->user = user; + if (eve_user_callbacks == NULL) { + eve_user_callbacks = cb; + } else { + EveUserCallback *current = eve_user_callbacks; + while (current->next != NULL) { + current = current->next; + } + current->next = cb; + } + return true; +} + +void SCEveRunCallbacks(ThreadVars *tv, const Packet *p, Flow *f, JsonBuilder *jb) +{ + EveUserCallback *cb = eve_user_callbacks; + while (cb != NULL) { + cb->Callback(tv, p, f, jb, cb->user); + cb = cb->next; + } +} + static bool IsBuiltinTypeName(const char *name) { const char *builtin[] = { diff --git a/src/output-eve.h b/src/output-eve.h index 7046c7b98005..7e55ce28f8e2 100644 --- a/src/output-eve.h +++ b/src/output-eve.h @@ -31,6 +31,7 @@ #define SURICATA_OUTPUT_EVE_H #include "suricata-common.h" +#include "rust.h" #include "conf.h" typedef uint32_t ThreadId; @@ -173,4 +174,46 @@ bool SCRegisterEveFileType(SCEveFileType *); SCEveFileType *SCEveFindFileType(const char *name); +/** \brief Function type for EVE callbacks. + * + * The function type for callbacks registered with + * SCEveRegisterCallback. This function will be called with the + * JsonBuilder just prior to the top-level object being closed. New + * fields maybe added, however there is no way to alter existing + * objects already added to the JsonBuilder. + * + * \param tv The ThreadVars for the thread performing the logging. + * \param p Packet if available. + * \param f Flow if available. + * \param user User data provided during callback registration. + */ +typedef void (*SCEveUserCallbackFn)( + ThreadVars *tv, const Packet *p, Flow *f, JsonBuilder *jb, void *user); + +/** \brief Register a callback for adding extra information to EVE logs. + * + * Allow users to register a callback for each EVE log. The callback + * is called just before the root object on the JsonBuilder is to be + * closed. + * + * New objects and fields can be append, but exist entries cannot be modified. + * + * Packet and Flow will be provided if available, but will other be + * NULL. + * + * Limitations: At this time the callbacks will only be called for EVE + * loggers that use JsonBuilder, notably this means it won't be called + * for stats records at this time. + * + * \returns true if callback is registered, false is not due to memory + * allocation error. + */ +bool SCEveRegisterCallback(SCEveUserCallbackFn fn, void *user); + +/** \internal + * + * Run EVE callbacks. + */ +void SCEveRunCallbacks(ThreadVars *tv, const Packet *p, Flow *f, JsonBuilder *jb); + #endif diff --git a/src/output-json-alert.c b/src/output-json-alert.c index 7822cc798045..91a55828a7a1 100644 --- a/src/output-json-alert.c +++ b/src/output-json-alert.c @@ -757,7 +757,7 @@ static int AlertJson(ThreadVars *tv, JsonAlertLogThread *aft, const Packet *p) EveAddVerdict(jb, p); } - OutputJsonBuilderBuffer(jb, aft->ctx); + OutputJsonBuilderBuffer(tv, p, p->flow, jb, aft->ctx); jb_free(jb); } @@ -767,7 +767,7 @@ static int AlertJson(ThreadVars *tv, JsonAlertLogThread *aft, const Packet *p) CreateEveHeader(p, LOG_DIR_PACKET, "packet", NULL, json_output_ctx->eve_ctx); if (unlikely(packetjs != NULL)) { EvePacket(p, packetjs, 0); - OutputJsonBuilderBuffer(packetjs, aft->ctx); + OutputJsonBuilderBuffer(tv, p, p->flow, packetjs, aft->ctx); jb_free(packetjs); } } @@ -801,7 +801,7 @@ static int AlertJsonDecoderEvent(ThreadVars *tv, JsonAlertLogThread *aft, const AlertJsonHeader(p, pa, jb, json_output_ctx->flags, NULL, NULL); - OutputJsonBuilderBuffer(jb, aft->ctx); + OutputJsonBuilderBuffer(tv, p, p->flow, jb, aft->ctx); jb_free(jb); } diff --git a/src/output-json-anomaly.c b/src/output-json-anomaly.c index 241cb974a758..00f82fa3685e 100644 --- a/src/output-json-anomaly.c +++ b/src/output-json-anomaly.c @@ -143,16 +143,16 @@ static int AnomalyDecodeEventJson(ThreadVars *tv, JsonAnomalyLogThread *aft, EvePacket(p, js, GET_PKT_LEN(p) < 32 ? GET_PKT_LEN(p) : 32); } - OutputJsonBuilderBuffer(js, aft->ctx); + OutputJsonBuilderBuffer(tv, p, p->flow, js, aft->ctx); jb_free(js); } return TM_ECODE_OK; } -static int AnomalyAppLayerDecoderEventJson(JsonAnomalyLogThread *aft, - const Packet *p, AppLayerDecoderEvents *decoder_events, - bool is_pktlayer, const char *layer, uint64_t tx_id) +static int AnomalyAppLayerDecoderEventJson(ThreadVars *tv, JsonAnomalyLogThread *aft, + const Packet *p, AppLayerDecoderEvents *decoder_events, bool is_pktlayer, const char *layer, + uint64_t tx_id) { const char *alprotoname = AppLayerGetProtoName(p->flow->alproto); @@ -201,7 +201,7 @@ static int AnomalyAppLayerDecoderEventJson(JsonAnomalyLogThread *aft, /* anomaly */ jb_close(js); - OutputJsonBuilderBuffer(js, aft->ctx); + OutputJsonBuilderBuffer(tv, p, p->flow, js, aft->ctx); jb_free(js); /* Current implementation assumes a single owner for this value */ @@ -223,8 +223,7 @@ static int JsonAnomalyTxLogger(ThreadVars *tv, void *thread_data, const Packet * decoder_events = AppLayerParserGetEventsByTx(f->proto, f->alproto, tx); if (decoder_events && decoder_events->event_last_logged < decoder_events->cnt) { SCLogDebug("state %p, tx: %p, tx_id: %"PRIu64, state, tx, tx_id); - AnomalyAppLayerDecoderEventJson(aft, p, decoder_events, false, - "proto_parser", tx_id); + AnomalyAppLayerDecoderEventJson(tv, aft, p, decoder_events, false, "proto_parser", tx_id); } return TM_ECODE_OK; } @@ -255,8 +254,8 @@ static int AnomalyJson(ThreadVars *tv, JsonAnomalyLogThread *aft, const Packet * if (aft->json_output_ctx->flags & LOG_JSON_APPLAYER_TYPE) { /* app layer proto detect events */ if (rc == TM_ECODE_OK && AnomalyHasPacketAppLayerEvents(p)) { - rc = AnomalyAppLayerDecoderEventJson(aft, p, p->app_layer_events, - true, "proto_detect", TX_ID_UNUSED); + rc = AnomalyAppLayerDecoderEventJson( + tv, aft, p, p->app_layer_events, true, "proto_detect", TX_ID_UNUSED); } /* parser state events */ @@ -264,8 +263,8 @@ static int AnomalyJson(ThreadVars *tv, JsonAnomalyLogThread *aft, const Packet * SCLogDebug("Checking for anomaly events; alproto %d", p->flow->alproto); AppLayerDecoderEvents *parser_events = AppLayerParserGetDecoderEvents(p->flow->alparser); if (parser_events && (parser_events->event_last_logged < parser_events->cnt)) { - rc = AnomalyAppLayerDecoderEventJson(aft, p, parser_events, - false, "parser", TX_ID_UNUSED); + rc = AnomalyAppLayerDecoderEventJson( + tv, aft, p, parser_events, false, "parser", TX_ID_UNUSED); } } } diff --git a/src/output-json-arp.c b/src/output-json-arp.c index 0490c6b54d1e..87a80d8cdb0b 100644 --- a/src/output-json-arp.c +++ b/src/output-json-arp.c @@ -90,7 +90,7 @@ static int JsonArpLogger(ThreadVars *tv, void *thread_data, const Packet *p) JSONFormatAndAddMACAddr(jb, "dest_mac", arph->dest_mac, false); jb_set_string(jb, "dest_ip", dstip); jb_close(jb); /* arp */ - OutputJsonBuilderBuffer(jb, thread); + OutputJsonBuilderBuffer(tv, p, p->flow, jb, thread); jb_free(jb); return TM_ECODE_OK; diff --git a/src/output-json-dcerpc.c b/src/output-json-dcerpc.c index 17e0199ed727..3b3bff90feac 100644 --- a/src/output-json-dcerpc.c +++ b/src/output-json-dcerpc.c @@ -47,7 +47,7 @@ static int JsonDCERPCLogger(ThreadVars *tv, void *thread_data, jb_close(jb); MemBufferReset(thread->buffer); - OutputJsonBuilderBuffer(jb, thread); + OutputJsonBuilderBuffer(tv, p, p->flow, jb, thread); jb_free(jb); return TM_ECODE_OK; diff --git a/src/output-json-dhcp.c b/src/output-json-dhcp.c index 9c7d9dff9230..a4a4a29990e8 100644 --- a/src/output-json-dhcp.c +++ b/src/output-json-dhcp.c @@ -72,7 +72,7 @@ static int JsonDHCPLogger(ThreadVars *tv, void *thread_data, rs_dhcp_logger_log(ctx->rs_logger, tx, js); - OutputJsonBuilderBuffer(js, thread->thread); + OutputJsonBuilderBuffer(tv, p, p->flow, js, thread->thread); jb_free(js); return TM_ECODE_OK; diff --git a/src/output-json-dnp3.c b/src/output-json-dnp3.c index 53cecd78a1aa..ea557ff206a8 100644 --- a/src/output-json-dnp3.c +++ b/src/output-json-dnp3.c @@ -246,7 +246,7 @@ static int JsonDNP3LoggerToServer(ThreadVars *tv, void *thread_data, jb_open_object(js, "dnp3"); JsonDNP3LogRequest(js, tx); jb_close(js); - OutputJsonBuilderBuffer(js, thread->ctx); + OutputJsonBuilderBuffer(tv, p, p->flow, js, thread->ctx); jb_free(js); SCReturnInt(TM_ECODE_OK); @@ -267,7 +267,7 @@ static int JsonDNP3LoggerToClient(ThreadVars *tv, void *thread_data, jb_open_object(js, "dnp3"); JsonDNP3LogResponse(js, tx); jb_close(js); - OutputJsonBuilderBuffer(js, thread->ctx); + OutputJsonBuilderBuffer(tv, p, p->flow, js, thread->ctx); jb_free(js); SCReturnInt(TM_ECODE_OK); diff --git a/src/output-json-dns.c b/src/output-json-dns.c index 3954da2336dc..cb60a4509a32 100644 --- a/src/output-json-dns.c +++ b/src/output-json-dns.c @@ -331,7 +331,7 @@ static int JsonDoh2Logger(ThreadVars *tv, void *thread_data, const Packet *p, Fl } out: if (r || r2) { - OutputJsonBuilderBuffer(jb, td->ctx); + OutputJsonBuilderBuffer(tv, p, p->flow, jb, td->ctx); } jb_free(jb); return TM_ECODE_OK; @@ -363,7 +363,7 @@ static int JsonDnsLoggerToServer(ThreadVars *tv, void *thread_data, } jb_close(jb); - OutputJsonBuilderBuffer(jb, td->ctx); + OutputJsonBuilderBuffer(tv, p, p->flow, jb, td->ctx); jb_free(jb); } @@ -392,7 +392,7 @@ static int JsonDnsLoggerToClient(ThreadVars *tv, void *thread_data, jb_set_int(jb, "version", 2); SCDnsLogJsonAnswer(txptr, td->dnslog_ctx->flags, jb); jb_close(jb); - OutputJsonBuilderBuffer(jb, td->ctx); + OutputJsonBuilderBuffer(tv, p, p->flow, jb, td->ctx); jb_free(jb); } @@ -432,7 +432,7 @@ static int JsonDnsLogger(ThreadVars *tv, void *thread_data, const Packet *p, Flo } if (SCDnsLogJson(txptr, td->dnslog_ctx->flags, jb)) { - OutputJsonBuilderBuffer(jb, td->ctx); + OutputJsonBuilderBuffer(tv, p, p->flow, jb, td->ctx); } jb_free(jb); } diff --git a/src/output-json-drop.c b/src/output-json-drop.c index b82c632daf65..1ac27a209d2a 100644 --- a/src/output-json-drop.c +++ b/src/output-json-drop.c @@ -85,7 +85,7 @@ static int g_droplog_flows_start = 1; * * \return return TM_ECODE_OK on success */ -static int DropLogJSON (JsonDropLogThread *aft, const Packet *p) +static int DropLogJSON(ThreadVars *tv, JsonDropLogThread *aft, const Packet *p) { JsonDropOutputCtx *drop_ctx = aft->drop_ctx; @@ -191,7 +191,7 @@ static int DropLogJSON (JsonDropLogThread *aft, const Packet *p) } } - OutputJsonBuilderBuffer(js, aft->ctx); + OutputJsonBuilderBuffer(tv, p, p->flow, js, aft->ctx); jb_free(js); return TM_ECODE_OK; @@ -326,7 +326,7 @@ static OutputInitResult JsonDropLogInitCtxSub(ConfNode *conf, OutputCtx *parent_ static int JsonDropLogger(ThreadVars *tv, void *thread_data, const Packet *p) { JsonDropLogThread *td = thread_data; - int r = DropLogJSON(td, p); + int r = DropLogJSON(tv, td, p); if (r < 0) return -1; diff --git a/src/output-json-file.c b/src/output-json-file.c index 509ae488bbee..e1f33893806e 100644 --- a/src/output-json-file.c +++ b/src/output-json-file.c @@ -213,8 +213,8 @@ JsonBuilder *JsonBuildFileInfoRecord(const Packet *p, const File *ff, void *tx, * \internal * \brief Write meta data on a single line json record */ -static void FileWriteJsonRecord(JsonFileLogThread *aft, const Packet *p, const File *ff, void *tx, - const uint64_t tx_id, uint8_t dir, OutputJsonCtx *eve_ctx) +static void FileWriteJsonRecord(ThreadVars *tv, JsonFileLogThread *aft, const Packet *p, + const File *ff, void *tx, const uint64_t tx_id, uint8_t dir, OutputJsonCtx *eve_ctx) { HttpXFFCfg *xff_cfg = aft->filelog_ctx->xff_cfg != NULL ? aft->filelog_ctx->xff_cfg : aft->filelog_ctx->parent_xff_cfg; @@ -223,7 +223,7 @@ static void FileWriteJsonRecord(JsonFileLogThread *aft, const Packet *p, const F return; } - OutputJsonBuilderBuffer(js, aft->ctx); + OutputJsonBuilderBuffer(tv, p, p->flow, js, aft->ctx); jb_free(js); } @@ -237,7 +237,7 @@ static int JsonFileLogger(ThreadVars *tv, void *thread_data, const Packet *p, co SCLogDebug("ff %p", ff); - FileWriteJsonRecord(aft, p, ff, tx, tx_id, dir, aft->filelog_ctx->eve_ctx); + FileWriteJsonRecord(tv, aft, p, ff, tx, tx_id, dir, aft->filelog_ctx->eve_ctx); return 0; } diff --git a/src/output-json-flow.c b/src/output-json-flow.c index f7826734f0cb..051d530fb1ef 100644 --- a/src/output-json-flow.c +++ b/src/output-json-flow.c @@ -340,7 +340,7 @@ static int JsonFlowLogger(ThreadVars *tv, void *thread_data, Flow *f) EveFlowLogJSON(thread, jb, f); - OutputJsonBuilderBuffer(jb, thread); + OutputJsonBuilderBuffer(tv, NULL, f, jb, thread); jb_free(jb); SCReturnInt(TM_ECODE_OK); diff --git a/src/output-json-frame.c b/src/output-json-frame.c index 09ec4aaab110..90224240f43f 100644 --- a/src/output-json-frame.c +++ b/src/output-json-frame.c @@ -287,8 +287,8 @@ void FrameJsonLogOneFrame(const uint8_t ipproto, const Frame *frame, Flow *f, jb_close(jb); } -static int FrameJsonUdp( - JsonFrameLogThread *aft, const Packet *p, Flow *f, FramesContainer *frames_container) +static int FrameJsonUdp(ThreadVars *tv, JsonFrameLogThread *aft, const Packet *p, Flow *f, + FramesContainer *frames_container) { FrameJsonOutputCtx *json_output_ctx = aft->json_output_ctx; @@ -315,7 +315,7 @@ static int FrameJsonUdp( jb_set_string(jb, "app_proto", AppProtoToString(f->alproto)); FrameJsonLogOneFrame(IPPROTO_UDP, frame, p->flow, NULL, p, jb, aft->payload_buffer); - OutputJsonBuilderBuffer(jb, aft->ctx); + OutputJsonBuilderBuffer(tv, p, p->flow, jb, aft->ctx); jb_free(jb); frame->flags |= FRAME_FLAG_LOGGED; } @@ -333,7 +333,7 @@ static int FrameJson(ThreadVars *tv, JsonFrameLogThread *aft, const Packet *p) return TM_ECODE_OK; if (p->proto == IPPROTO_UDP) { - return FrameJsonUdp(aft, p, p->flow, frames_container); + return FrameJsonUdp(tv, aft, p, p->flow, frames_container); } BUG_ON(p->proto != IPPROTO_TCP); @@ -387,7 +387,7 @@ static int FrameJson(ThreadVars *tv, JsonFrameLogThread *aft, const Packet *p) jb_set_string(jb, "app_proto", AppProtoToString(p->flow->alproto)); FrameJsonLogOneFrame(IPPROTO_TCP, frame, p->flow, stream, p, jb, aft->payload_buffer); - OutputJsonBuilderBuffer(jb, aft->ctx); + OutputJsonBuilderBuffer(tv, p, p->flow, jb, aft->ctx); jb_free(jb); frame->flags |= FRAME_FLAG_LOGGED; } else if (frame != NULL) { diff --git a/src/output-json-http.c b/src/output-json-http.c index 0c5b875ee9ad..b45be9a45b6b 100644 --- a/src/output-json-http.c +++ b/src/output-json-http.c @@ -493,7 +493,7 @@ static int JsonHttpLogger(ThreadVars *tv, void *thread_data, const Packet *p, Fl } } - OutputJsonBuilderBuffer(js, jhl->ctx); + OutputJsonBuilderBuffer(tv, p, p->flow, js, jhl->ctx); jb_free(js); SCReturnInt(TM_ECODE_OK); diff --git a/src/output-json-ike.c b/src/output-json-ike.c index 470026fde13b..a13ef0e1d944 100644 --- a/src/output-json-ike.c +++ b/src/output-json-ike.c @@ -90,7 +90,7 @@ static int JsonIKELogger(ThreadVars *tv, void *thread_data, const Packet *p, Flo goto error; } - OutputJsonBuilderBuffer(jb, thread->ctx); + OutputJsonBuilderBuffer(tv, p, p->flow, jb, thread->ctx); jb_free(jb); return TM_ECODE_OK; diff --git a/src/output-json-metadata.c b/src/output-json-metadata.c index f97547c551b4..2602e4b9b3ef 100644 --- a/src/output-json-metadata.c +++ b/src/output-json-metadata.c @@ -74,7 +74,7 @@ static int MetadataJson(ThreadVars *tv, OutputJsonThreadCtx *aft, const Packet * if (!aft->ctx->cfg.include_metadata) { EveAddMetadata(p, p->flow, js); } - OutputJsonBuilderBuffer(js, aft); + OutputJsonBuilderBuffer(tv, p, p->flow, js, aft); jb_free(js); return TM_ECODE_OK; diff --git a/src/output-json-mqtt.c b/src/output-json-mqtt.c index 66cf67a0334b..c912ddcc1835 100644 --- a/src/output-json-mqtt.c +++ b/src/output-json-mqtt.c @@ -85,7 +85,7 @@ static int JsonMQTTLogger(ThreadVars *tv, void *thread_data, if (!rs_mqtt_logger_log(tx, thread->mqttlog_ctx->flags, thread->mqttlog_ctx->max_log_len, js)) goto error; - OutputJsonBuilderBuffer(js, thread->ctx); + OutputJsonBuilderBuffer(tv, p, p->flow, js, thread->ctx); jb_free(js); return TM_ECODE_OK; diff --git a/src/output-json-netflow.c b/src/output-json-netflow.c index 2e359bb909c5..e448ecd33bc4 100644 --- a/src/output-json-netflow.c +++ b/src/output-json-netflow.c @@ -276,7 +276,7 @@ static int JsonNetFlowLogger(ThreadVars *tv, void *thread_data, Flow *f) return TM_ECODE_OK; NetFlowLogEveToServer(jb, f); EveAddCommonOptions(&jhl->ctx->cfg, NULL, f, jb, LOG_DIR_FLOW_TOSERVER); - OutputJsonBuilderBuffer(jb, jhl); + OutputJsonBuilderBuffer(tv, NULL, f, jb, jhl); jb_free(jb); /* only log a response record if we actually have seen response packets */ @@ -286,7 +286,7 @@ static int JsonNetFlowLogger(ThreadVars *tv, void *thread_data, Flow *f) return TM_ECODE_OK; NetFlowLogEveToClient(jb, f); EveAddCommonOptions(&jhl->ctx->cfg, NULL, f, jb, LOG_DIR_FLOW_TOCLIENT); - OutputJsonBuilderBuffer(jb, jhl); + OutputJsonBuilderBuffer(tv, NULL, f, jb, jhl); jb_free(jb); } SCReturnInt(TM_ECODE_OK); diff --git a/src/output-json-nfs.c b/src/output-json-nfs.c index 72274a6b7865..0b08c0e5105d 100644 --- a/src/output-json-nfs.c +++ b/src/output-json-nfs.c @@ -94,7 +94,7 @@ static int JsonNFSLogger(ThreadVars *tv, void *thread_data, jb_close(jb); MemBufferReset(thread->buffer); - OutputJsonBuilderBuffer(jb, thread); + OutputJsonBuilderBuffer(tv, p, p->flow, jb, thread); jb_free(jb); return TM_ECODE_OK; } diff --git a/src/output-json-pgsql.c b/src/output-json-pgsql.c index 71bcd10f071d..9cba28d25d4e 100644 --- a/src/output-json-pgsql.c +++ b/src/output-json-pgsql.c @@ -80,7 +80,7 @@ static int JsonPgsqlLogger(ThreadVars *tv, void *thread_data, const Packet *p, F goto error; } - OutputJsonBuilderBuffer(jb, thread->ctx); + OutputJsonBuilderBuffer(tv, p, p->flow, jb, thread->ctx); jb_free(jb); return TM_ECODE_OK; diff --git a/src/output-json-smb.c b/src/output-json-smb.c index 279ee772e8f0..4be1fce93e72 100644 --- a/src/output-json-smb.c +++ b/src/output-json-smb.c @@ -59,7 +59,7 @@ static int JsonSMBLogger(ThreadVars *tv, void *thread_data, } jb_close(jb); - OutputJsonBuilderBuffer(jb, thread); + OutputJsonBuilderBuffer(tv, p, p->flow, jb, thread); jb_free(jb); return TM_ECODE_OK; diff --git a/src/output-json-smtp.c b/src/output-json-smtp.c index bddbc4a9fcc4..592645cb3c09 100644 --- a/src/output-json-smtp.c +++ b/src/output-json-smtp.c @@ -85,7 +85,7 @@ static int JsonSmtpLogger(ThreadVars *tv, void *thread_data, const Packet *p, Fl jb_close(jb); EveEmailLogJson(jhl, jb, p, f, state, tx, tx_id); - OutputJsonBuilderBuffer(jb, jhl->ctx); + OutputJsonBuilderBuffer(tv, p, p->flow, jb, jhl->ctx); jb_free(jb); diff --git a/src/output-json-tls.c b/src/output-json-tls.c index aa24b3380a0d..c4ba0e249e62 100644 --- a/src/output-json-tls.c +++ b/src/output-json-tls.c @@ -501,7 +501,7 @@ static int JsonTlsLogger(ThreadVars *tv, void *thread_data, const Packet *p, /* Close the tls object. */ jb_close(js); - OutputJsonBuilderBuffer(js, aft->ctx); + OutputJsonBuilderBuffer(tv, p, p->flow, js, aft->ctx); jb_free(js); return 0; diff --git a/src/output-json.c b/src/output-json.c index 1f411cc110b8..18376fd428a5 100644 --- a/src/output-json.c +++ b/src/output-json.c @@ -955,7 +955,8 @@ int OutputJSONBuffer(json_t *js, LogFileCtx *file_ctx, MemBuffer **buffer) return 0; } -int OutputJsonBuilderBuffer(JsonBuilder *js, OutputJsonThreadCtx *ctx) +int OutputJsonBuilderBuffer( + ThreadVars *tv, const Packet *p, Flow *f, JsonBuilder *js, OutputJsonThreadCtx *ctx) { LogFileCtx *file_ctx = ctx->file_ctx; MemBuffer **buffer = &ctx->buffer; @@ -967,6 +968,8 @@ int OutputJsonBuilderBuffer(JsonBuilder *js, OutputJsonThreadCtx *ctx) jb_set_string(js, "pcap_filename", PcapFileGetFilename()); } + SCEveRunCallbacks(tv, p, f, js); + jb_close(js); MemBufferReset(*buffer); diff --git a/src/output-json.h b/src/output-json.h index 761064f7e10a..89597e616a0f 100644 --- a/src/output-json.h +++ b/src/output-json.h @@ -103,7 +103,8 @@ JsonBuilder *CreateEveHeader(const Packet *p, enum OutputJsonLogDirection dir, JsonBuilder *CreateEveHeaderWithTxId(const Packet *p, enum OutputJsonLogDirection dir, const char *event_type, JsonAddrInfo *addr, uint64_t tx_id, OutputJsonCtx *eve_ctx); int OutputJSONBuffer(json_t *js, LogFileCtx *file_ctx, MemBuffer **buffer); -int OutputJsonBuilderBuffer(JsonBuilder *js, OutputJsonThreadCtx *ctx); +int OutputJsonBuilderBuffer( + ThreadVars *tv, const Packet *p, Flow *f, JsonBuilder *js, OutputJsonThreadCtx *ctx); OutputInitResult OutputJsonInitCtx(ConfNode *); OutputInitResult OutputJsonLogInitSub(ConfNode *conf, OutputCtx *parent_ctx); diff --git a/src/output.c b/src/output.c index 002f33b5abc6..b99897509c0f 100644 --- a/src/output.c +++ b/src/output.c @@ -927,7 +927,7 @@ static int JsonGenericLogger(ThreadVars *tv, void *thread_data, const Packet *p, goto error; } - OutputJsonBuilderBuffer(js, thread); + OutputJsonBuilderBuffer(tv, p, p->flow, js, thread); jb_free(js); return TM_ECODE_OK; From 63ce48fb4b760ab179e8b9f5efb55c524be9d939 Mon Sep 17 00:00:00 2001 From: Jason Ish Date: Tue, 15 Oct 2024 17:06:35 -0600 Subject: [PATCH 08/11] examples/plugin: update to find generated rust header Needed for changes to output-eve.h. --- examples/plugins/c-json-filetype/Makefile.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/plugins/c-json-filetype/Makefile.in b/examples/plugins/c-json-filetype/Makefile.in index 0d4ec381ee4a..4d85c8ee2d59 100644 --- a/examples/plugins/c-json-filetype/Makefile.in +++ b/examples/plugins/c-json-filetype/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 -I@top_srcdir@/rust/dist -DHAVE_CONFIG_H # Currently the Suricata logging system requires this to be even for # plugins. From 13485249b0e2e35409bc6ecf452397083f44ddc1 Mon Sep 17 00:00:00 2001 From: Jason Ish Date: Thu, 17 Oct 2024 13:16:38 -0600 Subject: [PATCH 09/11] detect: break apart sigtable setup and initialization Allows initialization to be done early, so the table is ready for dynamic registration by plugins which are loaded before signature setup. --- src/detect-engine-register.c | 6 ++++-- src/detect-engine-register.h | 1 + src/runmode-unittests.c | 1 + src/suricata.c | 2 ++ src/tests/fuzz/fuzz_siginit.c | 1 + 5 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/detect-engine-register.c b/src/detect-engine-register.c index 37fbc98d8597..903cc158cf5f 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -439,7 +439,7 @@ void SigTableCleanup(void) } } -void SigTableSetup(void) +void SigTableInit(void) { if (sigmatch_table == NULL) { DETECT_TBLSIZE = DETECT_TBLSIZE_STATIC + DETECT_TBLSIZE_STEP; @@ -447,10 +447,12 @@ void SigTableSetup(void) if (sigmatch_table == NULL) { DETECT_TBLSIZE = 0; FatalError("Could not allocate sigmatch_table"); - return; } } +} +void SigTableSetup(void) +{ DetectSidRegister(); DetectPriorityRegister(); DetectPrefilterRegister(); diff --git a/src/detect-engine-register.h b/src/detect-engine-register.h index c9134c77b83a..db4cd957af9d 100644 --- a/src/detect-engine-register.h +++ b/src/detect-engine-register.h @@ -338,6 +338,7 @@ extern int DETECT_TBLSIZE_IDX; #define DETECT_TBLSIZE_STEP 256 int SigTableList(const char *keyword); void SigTableCleanup(void); +void SigTableInit(void); void SigTableSetup(void); void SigTableRegisterTests(void); diff --git a/src/runmode-unittests.c b/src/runmode-unittests.c index e116e86d5be5..35780ab101a0 100644 --- a/src/runmode-unittests.c +++ b/src/runmode-unittests.c @@ -240,6 +240,7 @@ void RunUnittests(int list_unittests, const char *regex_arg) AppLayerSetup(); /* hardcoded initialization code */ + SigTableInit(); SigTableSetup(); /* load the rule keywords */ TmqhSetup(); diff --git a/src/suricata.c b/src/suricata.c index 6bdd6edb90f6..ee9dfc0b5b69 100644 --- a/src/suricata.c +++ b/src/suricata.c @@ -2670,6 +2670,8 @@ int PostConfLoadedSetup(SCInstance *suri) MacSetRegisterFlowStorage(); + SigTableInit(); + #ifdef HAVE_PLUGINS SCPluginsLoad(suri->capture_plugin_name, suri->capture_plugin_args); #endif diff --git a/src/tests/fuzz/fuzz_siginit.c b/src/tests/fuzz/fuzz_siginit.c index a98148cfac8d..a50e1fd67ebf 100644 --- a/src/tests/fuzz/fuzz_siginit.c +++ b/src/tests/fuzz/fuzz_siginit.c @@ -27,6 +27,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) MpmTableSetup(); SpmTableSetup(); EngineModeSetIDS(); + SigTableInit(); SigTableSetup(); } if (cnt++ == 1024) { From 2252caddd4298d8a596a34d4ae0c286d0779238a Mon Sep 17 00:00:00 2001 From: Jason Ish Date: Fri, 18 Oct 2024 08:46:42 -0600 Subject: [PATCH 10/11] make: install-headers: rust-bindings.h rust-bindings.h was not being installed with "make install-headers", and its now pulled in by a header used for plugin support, so make sure its installed. We first attempt to install the "dist" version if exists, otherwise install the "gen" one. Also install the "gen" even if the "dist" one exists, as its going to be newer. --- src/Makefile.am | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Makefile.am b/src/Makefile.am index aaca85965435..b0f841cfd0c2 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1174,6 +1174,12 @@ install-headers: for header in $(noinst_HEADERS); do \ $(INSTALL_DATA) $$header "$(DESTDIR)${includedir}/suricata"; \ done + if test -e ../rust/dist/rust-bindings.h; then \ + $(INSTALL_DATA) ../rust/dist/rust-bindings.h "$(DESTDIR)${includedir}/suricata"; \ + fi + if test -e ../rust/gen/rust-bindings.h; then \ + $(INSTALL_DATA) ../rust/gen/rust-bindings.h "$(DESTDIR)${includedir}/suricata"; \ + fi # Until we can remove autoconf.h from our headers, we need to to # provide this for library/plugin users. From 64a5c107f196c10375293cb3f09a09e239f1a4f2 Mon Sep 17 00:00:00 2001 From: Juliana Fajardini Date: Tue, 5 Nov 2024 22:54:50 -0300 Subject: [PATCH 11/11] userguide: explain rule types and categorization Add documentation about the rule types introduced by 2696fda04168cb82. Add doc tags around code definitions that are referenced in the docs. Task #https://redmine.openinfosecfoundation.org/issues/7031 --- doc/userguide/rules/intro.rst | 223 ++++++++++++++++++ .../OverallAlgoHorizontal-v1-20241108.png | Bin 0 -> 68631 bytes src/detect-engine.c | 2 + src/detect.h | 2 + 4 files changed, 227 insertions(+) create mode 100644 doc/userguide/rules/intro/OverallAlgoHorizontal-v1-20241108.png diff --git a/doc/userguide/rules/intro.rst b/doc/userguide/rules/intro.rst index 56df9ab49437..3af6be95c4bc 100644 --- a/doc/userguide/rules/intro.rst +++ b/doc/userguide/rules/intro.rst @@ -349,3 +349,226 @@ reassembled streams, TLS-, SSL-, SSH-, FTP- and dcerpc-buffers. Note that there are some exceptions, e.g. the ``http_raw_uri`` keyword. See :ref:`rules-http-uri-normalization` for more information. + + +Rule Types and Categorization +----------------------------- + +Once parsed, Suricata rules are categorized for performance and further +processing (as different rule types will be handled by specific engine modules). +The signature types are defined in `src/detect.h +`_: + +.. literalinclude:: ../../../src/detect.h + :caption: src/detect.h + :language: c + :start-after: // rule types documentation tag start: SignatureType + :end-before: // rule types documentation tag end: SignatureType + +The rule type will impact: + + - To what does the signature action apply, in case of a match (`Action Scope`) + - When is the rule matched against traffic (`Inspected`) + - Against what the rule matches (`Matches`) + +This categorization is done taking into consideration the presence or absence of +certain rule elements, as well as the type of keywords used. The categorization +currently takes place in `src/detect-engine-build.c:void SignatureSetType() +`_. + +The ``SignatureSetType()`` overall flow is described below: + +.. image:: intro/OverallAlgoHorizontal-v1-20241108.png + :width: 600 + :alt: A flowchart representing the SignatureSetType function. + +The following table lists all Suricata signature types, and how they impact the +aspects aforementioned. + +.. list-table:: Suricata Rule Types + :header-rows: 1 + + * - Type + - Action Scope + - Inspected + - Matches + - Keyword Examples (non-exhaustive) + * - Decoder Events Only + - Packet + - Per-packet basis + - Packets that are broken on an IP level + - 'decode-event' + * - Packet + - Packet + - Per-packet basis + - Packet-level info (e.g.: header info) + - 'itype', 'tcp.hdr', 'tcp.seq', 'ttl' etc. + * - IP Only + - Flow + - Once per direction + - On IP addresses on the flow + - Source/ Destination field of a rule + * - IP Only (contains a negated address)(*) + - Flow + - Once per direction + - On the flow, on IP address level (negated addresses) + - Source/ Destination field of a rule, containing negated address + * - Protocol Detection Only + - Flow + - Once per direction, when protocol detection is done + - On protocol detected for the flow + - 'app-layer-protocol' + * - Packet-Stream + - Flow, if stateful (**) + - Flow, if stateful, per-packet if not + - Against the reassembled stream. If stream unavailable, match per-packet + (packet payload and stream payload) + - 'content' with 'startswith' or 'depth' + * - Stream + - Flow, if stateful (**) + - Per stream chunk, if stateful, per-packet if not + - Against the reassembled stream. If stream unavailable, match per-packet + - 'tcp-stream' in protocol field; simple 'content'; 'byte_extract' + * - Application Layer Protocol + - Flow + - Per-packet basis + - On 'protocol' field + - `Protocol field `_ of a rule + * - Application Layer Protocol Transactions + - Flow + - Per transaction update + - On buffer keywords + - Application layer protocol-related, e.g. 'http.host', 'rfb.secresult', + 'dcerpc.stub_data', 'frame' keywords + +.. note:: + (*) IP Only signatures with negated addresses are `like` IP-only signatures, but + currently handled differently due to limitations of the algorithm processing + IP Only rules. + +.. note:: Action Scope: `Flow, if stateful` + + (**) Apply to the flow. If a segment isn't accepted into a stream for any + reason (such as packet anomalies, errors, memcap reached etc), the rule will + be applied on a packet level. + +Signature Properties +~~~~~~~~~~~~~~~~~~~~ + +The `Action Scope` mentioned above relates to the Signature Properties, as seen in +`src/detect-engine.c `_: + +.. literalinclude:: ../../../src/detect-engine.c + :caption: src/detect-engine.c + :language: c + :start-after: // rule types documentation tag start: SignatureProperties + :end-before: // rule types documentation tag end: SignatureProperties + +Signature Examples per Type +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Decoder Events Only +^^^^^^^^^^^^^^^^^^^ + +For more examples check https://github.com/OISF/suricata/blob/master/rules/decoder-events.rules. + +.. container:: example-rule + + alert pkthdr any any -> any any (msg:"SURICATA IPv4 malformed option"; :example-rule-emphasis:`decode-event:ipv4.opt_malformed;` classtype:protocol-command-decode; sid:2200006; rev:2;) + +Packet +^^^^^^ + +.. container:: example-rule + + alert udp any any -> any any (msg:"UDP with flow direction"; flow:to_server; sid:1001;) + +.. container:: example-rule + + alert tcp any any -> any any (msg:"ttl"; :example-rule-emphasis:`ttl:123;` sid:701;) + +IP Only +^^^^^^^ + +.. container:: example-rule + + alert tcp-stream any any -> any any (msg:"tcp-stream, no content"; sid:101;) + + +.. container:: example-rule + + alert tcp-pkt [192.168.0.0/16,10.0.0.0/8,172.16.0.0/12] any -> any any (msg:"tcp-pkt, no content"; sid:201;) + +IP Only (contains negated address) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. container:: example-rule + + alert tcp 192.168.0.0/16,10.0.0.0/8,172.16.0.0/12 any -> :example-rule-emphasis:`![192.168.0.0/16,10.0.0.0/8,172.16.0.0/12]` any (msg:"tcp, has negated IP address"; sid:304;) + +.. container:: example-rule + + alert tcp :example-rule-emphasis:`[10.0.0.0/8,!10.10.10.10]` any -> :example-rule-emphasis:`[10.0.0.0/8,!10.10.10.10]` any (msg:"tcp, has negated IP address"; sid:305;) + +Protocol Detection Only +^^^^^^^^^^^^^^^^^^^^^^^ + +.. container:: example-rule + + alert tcp any any -> any any (msg:"tcp, pd negated"; :example-rule-emphasis:`app-layer-protocol:!http;` sid:401;) + + +.. container:: example-rule + + alert tcp any any -> any any (msg:"tcp, pd positive"; :example-rule-emphasis:`app-layer-protocol:http;` sid:402;) + + +Packet-Stream +^^^^^^^^^^^^^ + +.. container:: example-rule + + alert tcp any any -> any any (msg:"tcp, anchored content"; :example-rule-emphasis:`content:"abc"; startswith;` sid:303;) + +.. container:: example-rule + + alert http any any -> any any (msg:"http, anchored content"; :example-rule-emphasis:`content:"abc"; startswith;` sid:603;) + + +Stream +^^^^^^ + +.. container:: example-rule + + alert :example-rule-emphasis:`tcp-stream` any any -> any any (msg:"tcp-stream, simple content"; :example-rule-emphasis:`content:"abc";` sid:102;) + +.. container:: example-rule + + alert :example-rule-emphasis:`http` any any -> any any (msg:"http, simple content"; :example-rule-emphasis:`content:"abc";` sid:602;) + +.. container:: example-rule + + alert tcp any any -> any any (msg:"byte_extract with dce"; byte_extract:4,0,var,dce; byte_test:4,>,var,4,little; sid:901;) + + +Application Layer Protocol +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. container:: example-rule + + alert :example-rule-emphasis:`http` any any -> any any (msg:"http, no content"; sid:601;) + +Application Layer Protocol Transactions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. container:: example-rule + + alert tcp any any -> any any (msg:"http, pos event"; :example-rule-emphasis:`app-layer-event:http.file_name_too_long;` sid:501;) + +.. container:: example-rule + + alert http any any -> any any (msg:"Test"; flow:established,to_server; :example-rule-emphasis:`http.method; content:"GET"; http.uri; content:".exe";` endswith; :example-rule-emphasis:`http.host; content:!".google.com";` endswith; sid:1102;) + +.. container:: example-rule + + alert udp any any -> any any (msg:"DNS UDP Frame"; flow:to_server; :example-rule-emphasis:`frame:dns.pdu;` content:"\|01 20 00 01\|"; offset:2; content:"suricata"; offset:13; sid:1402; rev:1;) diff --git a/doc/userguide/rules/intro/OverallAlgoHorizontal-v1-20241108.png b/doc/userguide/rules/intro/OverallAlgoHorizontal-v1-20241108.png new file mode 100644 index 0000000000000000000000000000000000000000..40ee2bf2188ff9e83a90f726d4e969ade5734a1b GIT binary patch literal 68631 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

jVCxKyMOU8v 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 WdLnhlRe}os(Nxt{c}chu{C@xhOMb%u literal 0 HcmV?d00001 diff --git a/src/detect-engine.c b/src/detect-engine.c index 77c25a1cf3a9..16241b4be777 100644 --- a/src/detect-engine.c +++ b/src/detect-engine.c @@ -110,6 +110,7 @@ static DetectEnginePktInspectionEngine *g_pkt_inspect_engines = NULL; static DetectEngineFrameInspectionEngine *g_frame_inspect_engines = NULL; // clang-format off +// rule types documentation tag start: SignatureProperties const struct SignatureProperties signature_properties[SIG_TYPE_MAX] = { /* SIG_TYPE_NOT_SET */ { SIG_PROP_FLOW_ACTION_PACKET, }, /* SIG_TYPE_IPONLY */ { SIG_PROP_FLOW_ACTION_FLOW, }, @@ -122,6 +123,7 @@ const struct SignatureProperties signature_properties[SIG_TYPE_MAX] = { /* SIG_TYPE_APPLAYER */ { SIG_PROP_FLOW_ACTION_FLOW, }, /* SIG_TYPE_APP_TX */ { SIG_PROP_FLOW_ACTION_FLOW, }, }; +// rule types documentation tag end: SignatureProperties // clang-format on /** \brief register inspect engine at start up time diff --git a/src/detect.h b/src/detect.h index fe755b7f0d14..1b6676480232 100644 --- a/src/detect.h +++ b/src/detect.h @@ -59,6 +59,7 @@ struct SCSigSignatureWrapper_; /* Forward declarations for structures from Rust. */ typedef struct SCDetectRequiresStatus SCDetectRequiresStatus; +// rule types documentation tag start: SignatureType enum SignatureType { SIG_TYPE_NOT_SET = 0, SIG_TYPE_IPONLY, // rule is handled by IPONLY engine @@ -76,6 +77,7 @@ enum SignatureType { SIG_TYPE_MAX, }; +// rule types documentation tag end: SignatureType enum SignaturePropertyFlowAction { SIG_PROP_FLOW_ACTION_PACKET,