You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
suricata/plugins/ndpi/ndpi.c

886 lines
28 KiB
C

/* Copyright (C) 2024-2025 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.
*/
/* License note: While this "glue" code to the nDPI library is GPLv2,
* nDPI is itself LGPLv3 which is known to be incompatible with the
* GPLv2. */
#include "suricata-common.h"
#include "suricata-plugin.h"
#include "conf.h"
#include "detect-engine-helper.h"
#include "detect-parse.h"
#include "flow.h"
#include "flow-callbacks.h"
#include "flow-storage.h"
#include "output-eve.h"
#include "thread-callbacks.h"
#include "thread-storage.h"
#include "util-debug.h"
#include "ndpi-compat.h"
static SCThreadStorageId thread_storage_id = { .id = -1 };
static SCFlowStorageId flow_storage_id = { .id = -1 };
static int ndpi_protocol_keyword_id = -1;
static int ndpi_risk_keyword_id = -1;
static struct ndpi_global_context *ndpi_g_ctx = NULL;
static NdpiCompatLicense ndpi_license = NDPI_COMPAT_LICENSE_NOT_FOR_PROFIT;
/* EVE objects dropped because nDPI produced malformed JSON */
static SC_ATOMIC_DECL_AND_INIT_WITH_VAL(unsigned int, ndpi_json_dropped, 0);
/* Initial size of the buffer ndpi_dpi2json() writes to. nDPI grows it by
* the unescaped length of a string only, while escaping can expand a byte
* to six; starting with a buffer large enough for the common case keeps
* its escaper from running out of room. */
#define NDPI_EVE_SERIALIZER_SIZE 4096
struct NdpiThreadContext {
struct ndpi_detection_module_struct *ndpi;
};
struct NdpiFlowContext {
struct ndpi_flow_struct *ndpi_flow;
ndpi_protocol detected_l7_protocol;
bool detection_completed;
};
typedef struct DetectnDPIProtocolData_ {
ndpi_master_app_protocol l7_protocol;
bool negated;
} DetectnDPIProtocolData;
typedef struct DetectnDPIRiskData_ {
ndpi_risk risk_mask; /* uint64 */
bool negated;
} DetectnDPIRiskData;
/**
* Safe helper to get nDPI thread context. Returns NULL if storage
* is not available (e.g. thread storage not yet initialized).
*/
static inline struct NdpiThreadContext *NdpiGetThreadContext(ThreadVars *tv)
{
if (unlikely(tv == NULL || thread_storage_id.id < 0))
return NULL;
return SCThreadGetStorageById(tv, thread_storage_id);
}
/**
* Safe helper to get nDPI flow context. Returns NULL if the flow
* context is not available.
*/
static inline struct NdpiFlowContext *NdpiGetFlowContext(const Flow *f)
{
if (unlikely(f == NULL || flow_storage_id.id < 0))
return NULL;
return SCFlowGetStorageById(f, flow_storage_id);
}
/**
* Resolve "ndpi.license". nDPI 6.0 does not load its dual-licensed
* dissectors (DHCP, DNS, QUIC and TLS as of 6.0) for for-profit use
* without an agreement with ntop. Not-for-profit is the default so the
* plugin works out of the box; an invalid value is fatal.
*/
static NdpiCompatLicense NdpiResolveLicense(void)
{
const char *license = NULL;
if (SCConfGet("ndpi.license", &license) != 1 || license == NULL)
return NDPI_COMPAT_LICENSE_NOT_FOR_PROFIT;
#if NDPI_COMPAT_HAS_LICENSE
if (strcmp(license, "not-for-profit") == 0)
return NDPI_COMPAT_LICENSE_NOT_FOR_PROFIT;
if (strcmp(license, "for-profit") == 0) {
SCLogWarning("ndpi.license is \"for-profit\": nDPI will not load its dual-licensed "
"dissectors (DHCP, DNS, QUIC and TLS as of nDPI 6.0)");
return NDPI_COMPAT_LICENSE_FOR_PROFIT;
}
if (strcmp(license, "for-profit-dual") == 0)
return NDPI_COMPAT_LICENSE_FOR_PROFIT_DUAL;
FatalError("invalid ndpi.license value \"%s\": expected \"not-for-profit\", "
"\"for-profit\" or \"for-profit-dual\"",
license);
#else
SCLogWarning("ndpi.license has no effect with nDPI %d.%d, the license declaration was "
"introduced in nDPI 6.0",
NDPI_MAJOR, NDPI_MINOR);
#endif
return NDPI_COMPAT_LICENSE_NOT_FOR_PROFIT;
}
/* nDPI correlates flows through LRU caches (DNS to TLS, STUN to RTP, ...)
* that are private to a detection module unless made global through a
* shared context. Suricata spreads a host's flows over all its workers,
* so without sharing a correlation never leaves the worker that saw the
* first flow. */
static const char *ndpi_shared_lru_caches[] = {
"lru.ookla.scope",
"lru.bittorrent.scope",
"lru.stun.scope",
"lru.tls_cert.scope",
"lru.mining.scope",
"lru.msteams.scope",
"lru.fpc_dns.scope",
"lru.signal.scope",
};
/**
* Allocate and finalize a detection module with every protocol enabled.
* Worker modules share their LRU caches through the global context, the
* throwaway modules used while parsing rules don't need to.
*
* nDPI creates a shared cache on the first finalization without locking,
* which is fine as thread init callbacks run sequentially from
* TmThreadCreate().
*/
static struct ndpi_detection_module_struct *NdpiModuleNew(bool shared_caches)
{
struct ndpi_global_context *g_ctx = shared_caches ? ndpi_g_ctx : NULL;
struct ndpi_detection_module_struct *ndpi = NdpiCompatInitModule(g_ctx, ndpi_license);
if (ndpi == NULL)
return NULL;
NdpiCompatEnableAllProtocols(ndpi);
if (g_ctx != NULL) {
for (size_t i = 0; i < ARRAY_SIZE(ndpi_shared_lru_caches); i++) {
if (ndpi_set_config(ndpi, NULL, ndpi_shared_lru_caches[i], "1") != NDPI_CFG_OK) {
SCLogWarning("Failed to share the nDPI \"%s\" cache between threads",
ndpi_shared_lru_caches[i]);
}
}
}
if (ndpi_finalize_initialization(ndpi) != 0) {
ndpi_exit_detection_module(ndpi);
return NULL;
}
return ndpi;
}
static void ThreadStorageFree(void *ptr)
{
SCLogDebug("Free'ing nDPI thread storage");
struct NdpiThreadContext *context = ptr;
if (context == NULL)
return;
if (context->ndpi != NULL)
ndpi_exit_detection_module(context->ndpi);
SCFree(context);
}
static void FlowStorageFree(void *ptr)
{
struct NdpiFlowContext *ctx = ptr;
if (ctx == NULL)
return;
if (ctx->ndpi_flow != NULL)
ndpi_flow_free(ctx->ndpi_flow);
SCFree(ctx);
}
static void OnFlowInit(ThreadVars *tv, Flow *f, const Packet *p, void *_data)
{
if (unlikely(f == NULL))
return;
struct NdpiFlowContext *flowctx = SCCalloc(1, sizeof(*flowctx));
if (flowctx == NULL) {
SCLogDebug("Failed to allocate nDPI flow context");
return;
}
flowctx->ndpi_flow = ndpi_flow_malloc(SIZEOF_FLOW_STRUCT);
if (flowctx->ndpi_flow == NULL) {
SCLogDebug("Failed to allocate nDPI flow");
SCFree(flowctx);
return;
}
memset(flowctx->ndpi_flow, 0, SIZEOF_FLOW_STRUCT);
flowctx->detection_completed = false;
if (SCFlowSetStorageById(f, flow_storage_id, flowctx) != 0) {
SCLogDebug("Failed to set nDPI flow storage");
FlowStorageFree(flowctx);
}
}
static void OnFlowUpdate(ThreadVars *tv, Flow *f, Packet *p, void *_data)
{
const uint8_t flow_proto = SCFlowGetIPProtocol(f);
/* Ignore packets that have a different protocol than the
* flow. This can happen with ICMP unreachable packets. */
if (p->proto != flow_proto) {
return;
}
uint16_t ip_len = 0;
void *ip_ptr = NULL;
struct NdpiThreadContext *threadctx = NdpiGetThreadContext(tv);
struct NdpiFlowContext *flowctx = NdpiGetFlowContext(f);
if (threadctx == NULL || threadctx->ndpi == NULL)
return;
if (flowctx == NULL || flowctx->ndpi_flow == NULL)
return;
if (PacketIsIPv4(p)) {
const IPV4Hdr *ip4h = PacketGetIPv4(p);
ip_len = IPV4_GET_RAW_IPLEN(ip4h);
ip_ptr = (void *)PacketGetIPv4(p);
} else if (PacketIsIPv6(p)) {
const IPV6Hdr *ip6h = PacketGetIPv6(p);
ip_len = IPV6_HEADER_LEN + IPV6_GET_RAW_PLEN(ip6h);
ip_ptr = (void *)PacketGetIPv6(p);
}
if (!flowctx->detection_completed && ip_ptr != NULL && ip_len > 0) {
uint64_t time_ms = ((uint64_t)p->ts.secs) * 1000 + p->ts.usecs / 1000;
struct ndpi_flow_input_info input_info;
SCLogDebug("Performing nDPI detection...");
/* Suricata knows the packet direction, spare nDPI from guessing it.
* Whether the flow beginning was seen is left unknown as the flow
* API exposes no reliable flag for it. */
memset(&input_info, 0, sizeof(input_info));
input_info.seen_flow_beginning = NDPI_FLOW_BEGINNING_UNKNOWN;
input_info.in_pkt_dir =
PKT_IS_TOSERVER(p) ? NDPI_IN_PKT_DIR_C_TO_S : NDPI_IN_PKT_DIR_S_TO_C;
flowctx->detected_l7_protocol = ndpi_detection_process_packet(
threadctx->ndpi, flowctx->ndpi_flow, ip_ptr, ip_len, time_ms, &input_info);
if (NdpiCompatClassificationFinal(
threadctx->ndpi, flowctx->ndpi_flow, &flowctx->detected_l7_protocol)) {
flowctx->detection_completed = true;
} else {
/* stop feeding nDPI after a few packets, taking its best guess
* for flows it could not classify */
const uint16_t max_num_pkts = (flow_proto == IPPROTO_UDP) ? 8 : 24;
if ((SCFlowGetToServerPacketCount(f) + SCFlowGetToClientPacketCount(f)) >
max_num_pkts) {
if (NdpiCompatIsUnclassified(&flowctx->detected_l7_protocol)) {
flowctx->detected_l7_protocol =
NdpiCompatGiveup(threadctx->ndpi, flowctx->ndpi_flow);
}
flowctx->detection_completed = true;
}
}
if (SCLogDebugEnabled() && flowctx->detection_completed) {
SCLogDebug("Detected protocol: %s | app protocol: %s | category: %s",
ndpi_get_proto_name(
threadctx->ndpi, flowctx->detected_l7_protocol.proto.master_protocol),
ndpi_get_proto_name(
threadctx->ndpi, flowctx->detected_l7_protocol.proto.app_protocol),
ndpi_category_get_name(
threadctx->ndpi, flowctx->detected_l7_protocol.category));
}
}
}
static void OnFlowFinish(ThreadVars *tv, Flow *f, void *_data)
{
/* Nothing to do here, the storage API has taken care of cleaning
* up storage, just here for example purposes. */
SCLogDebug("Flow %p is now finished", f);
}
static void OnThreadInit(ThreadVars *tv, void *_data)
{
struct NdpiThreadContext *context = SCCalloc(1, sizeof(*context));
if (context == NULL) {
FatalError("Failed to allocate nDPI thread context");
}
context->ndpi = NdpiModuleNew(true);
if (context->ndpi == NULL) {
FatalError("Failed to initialize nDPI detection module");
}
SCThreadSetStorageById(tv, thread_storage_id, context);
}
static int DetectnDPIProtocolPacketMatch(
DetectEngineThreadCtx *det_ctx, Packet *p, const Signature *s, const SigMatchCtx *ctx)
{
SCEnter();
const Flow *f = p->flow;
if (f == NULL) {
SCLogDebug("packet %" PRIu64 ": no flow", PcapPacketCntGet(p));
SCReturnInt(0);
}
struct NdpiFlowContext *flowctx = NdpiGetFlowContext(f);
if (flowctx == NULL) {
SCLogDebug("packet %" PRIu64 ": no flowctx", PcapPacketCntGet(p));
SCReturnInt(0);
}
const DetectnDPIProtocolData *data = (const DetectnDPIProtocolData *)ctx;
/* if the sig is PD-only we only match when PD packet flags are set */
/*
if (s->type == SIG_TYPE_PDONLY &&
(p->flags & (PKT_PROTO_DETECT_TS_DONE | PKT_PROTO_DETECT_TC_DONE)) == 0) {
SCLogDebug("packet %"PRIu64": flags not set", PcapPacketCntGet(p));
SCReturnInt(0);
}
*/
if (!flowctx->detection_completed) {
SCLogDebug("packet %" PRIu64 ": ndpi protocol not yet detected", PcapPacketCntGet(p));
SCReturnInt(0);
}
bool r = ndpi_is_proto_equals(flowctx->detected_l7_protocol.proto, data->l7_protocol, false);
r = r ^ data->negated;
if (r) {
SCLogDebug("ndpi protocol match on protocol = %u.%u (match %u)",
flowctx->detected_l7_protocol.proto.app_protocol,
flowctx->detected_l7_protocol.proto.master_protocol,
data->l7_protocol.app_protocol);
SCReturnInt(1);
}
SCReturnInt(0);
}
static DetectnDPIProtocolData *DetectnDPIProtocolParse(const char *arg, bool negate)
{
DetectnDPIProtocolData *data;
struct ndpi_detection_module_struct *ndpi_struct;
ndpi_master_app_protocol l7_protocol;
char *l7_protocol_name = (char *)arg;
/* convert protocol name (string) to ID */
ndpi_struct = NdpiModuleNew(false);
if (unlikely(ndpi_struct == NULL))
return NULL;
l7_protocol = ndpi_get_protocol_by_name(ndpi_struct, l7_protocol_name);
ndpi_exit_detection_module(ndpi_struct);
if (ndpi_is_proto_unknown(l7_protocol)) {
SCLogError("failure parsing nDPI protocol '%s'", l7_protocol_name);
return NULL;
}
data = SCMalloc(sizeof(DetectnDPIProtocolData));
if (unlikely(data == NULL))
return NULL;
memcpy(&data->l7_protocol, &l7_protocol, sizeof(ndpi_master_app_protocol));
data->negated = negate;
return data;
}
static bool nDPIProtocolDataHasConflicts(
const DetectnDPIProtocolData *us, const DetectnDPIProtocolData *them)
{
/* check for mix of negated and non negated */
if (them->negated ^ us->negated)
return true;
/* check for multiple non-negated */
if (!us->negated)
return true;
/* check for duplicate */
if (ndpi_is_proto_equals(us->l7_protocol, them->l7_protocol, true))
return true;
return false;
}
static int DetectnDPIProtocolSetup(DetectEngineCtx *de_ctx, Signature *s, const char *arg)
{
DetectnDPIProtocolData *data = DetectnDPIProtocolParse(arg, s->init_data->negated);
if (data == NULL)
goto error;
SigMatch *tsm = s->init_data->smlists[DETECT_SM_LIST_MATCH];
for (; tsm != NULL; tsm = tsm->next) {
if (tsm->type == ndpi_protocol_keyword_id) {
const DetectnDPIProtocolData *them = (const DetectnDPIProtocolData *)tsm->ctx;
if (nDPIProtocolDataHasConflicts(data, them)) {
SCLogError("can't mix "
"positive ndpi-protocol match with negated");
goto error;
}
}
}
if (SCSigMatchAppendSMToList(de_ctx, s, ndpi_protocol_keyword_id, (SigMatchCtx *)data,
DETECT_SM_LIST_MATCH) == NULL) {
goto error;
}
return 0;
error:
if (data != NULL)
SCFree(data);
return -1;
}
static void DetectnDPIProtocolFree(DetectEngineCtx *de_ctx, void *ptr)
{
SCFree(ptr);
}
static int DetectnDPIRiskPacketMatch(
DetectEngineThreadCtx *det_ctx, Packet *p, const Signature *s, const SigMatchCtx *ctx)
{
SCEnter();
const Flow *f = p->flow;
if (f == NULL) {
SCLogDebug("packet %" PRIu64 ": no flow", PcapPacketCntGet(p));
SCReturnInt(0);
}
struct NdpiFlowContext *flowctx = NdpiGetFlowContext(f);
if (flowctx == NULL) {
SCLogDebug("packet %" PRIu64 ": no flowctx", PcapPacketCntGet(p));
SCReturnInt(0);
}
const DetectnDPIRiskData *data = (const DetectnDPIRiskData *)ctx;
if (!flowctx->detection_completed) {
SCLogDebug("packet %" PRIu64 ": ndpi risks not yet detected", PcapPacketCntGet(p));
SCReturnInt(0);
}
if (flowctx->ndpi_flow == NULL) {
SCLogDebug("packet %" PRIu64 ": ndpi_flow is NULL", PcapPacketCntGet(p));
SCReturnInt(0);
}
bool r = ((flowctx->ndpi_flow->risk & data->risk_mask) == data->risk_mask);
r = r ^ data->negated;
if (r) {
SCLogDebug("ndpi risks match on risk bitmap = %" PRIu64 " (matching bitmap %" PRIu64 ")",
flowctx->ndpi_flow->risk, data->risk_mask);
SCReturnInt(1);
}
SCReturnInt(0);
}
static DetectnDPIRiskData *DetectnDPIRiskParse(const char *arg, bool negate)
{
DetectnDPIRiskData *data;
ndpi_risk risk_mask;
/* convert list of risk names (string) to mask */
if (isdigit(arg[0]))
risk_mask = atoll(arg);
else {
char *dup = SCStrdup(arg), *tmp, *token;
NDPI_ZERO_BIT(risk_mask);
if (dup != NULL) {
token = strtok_r(dup, ",", &tmp);
while (token != NULL) {
ndpi_risk_enum risk_id = ndpi_code2risk(token);
if (risk_id >= NDPI_MAX_RISK) {
SCLogError("unrecognized risk '%s', "
"please check ndpiReader -H for valid risk codes",
token);
SCFree(dup);
return NULL;
}
NDPI_SET_BIT(risk_mask, risk_id);
token = strtok_r(NULL, ",", &tmp);
}
SCFree(dup);
}
}
data = SCMalloc(sizeof(DetectnDPIRiskData));
if (unlikely(data == NULL))
return NULL;
data->risk_mask = risk_mask;
data->negated = negate;
return data;
}
static bool nDPIRiskDataHasConflicts(const DetectnDPIRiskData *us, const DetectnDPIRiskData *them)
{
/* check for duplicate */
if (us->risk_mask == them->risk_mask)
return true;
return false;
}
static int DetectnDPIRiskSetup(DetectEngineCtx *de_ctx, Signature *s, const char *arg)
{
DetectnDPIRiskData *data = DetectnDPIRiskParse(arg, s->init_data->negated);
if (data == NULL)
goto error;
SigMatch *tsm = s->init_data->smlists[DETECT_SM_LIST_MATCH];
for (; tsm != NULL; tsm = tsm->next) {
if (tsm->type == ndpi_risk_keyword_id) {
const DetectnDPIRiskData *them = (const DetectnDPIRiskData *)tsm->ctx;
if (nDPIRiskDataHasConflicts(data, them)) {
SCLogError("can't mix "
"positive ndpi-risk match with negated");
goto error;
}
}
}
if (SCSigMatchAppendSMToList(de_ctx, s, ndpi_risk_keyword_id, (SigMatchCtx *)data,
DETECT_SM_LIST_MATCH) == NULL) {
goto error;
}
return 0;
error:
if (data != NULL)
SCFree(data);
return -1;
}
static void DetectnDPIRiskFree(DetectEngineCtx *de_ctx, void *ptr)
{
SCFree(ptr);
}
/* Syntax check of the JSON nDPI produces for a flow. It is inserted into
* the EVE record as is, so a malformed fragment would take the whole
* record down with it, and nDPI has been seen producing one (6.0 corrupts
* its output when escaping strings with non printable characters). */
typedef struct NdpiJsonCursor_ {
const char *p;
const char *end;
} NdpiJsonCursor;
static bool NdpiJsonValue(NdpiJsonCursor *c, int depth);
static void NdpiJsonSkipSpace(NdpiJsonCursor *c)
{
while (c->p < c->end && (*c->p == ' ' || *c->p == '\t' || *c->p == '\n' || *c->p == '\r'))
c->p++;
}
static bool NdpiJsonString(NdpiJsonCursor *c)
{
if (c->p >= c->end || *c->p != '"')
return false;
c->p++;
while (c->p < c->end) {
const unsigned char ch = (unsigned char)*c->p++;
if (ch == '"')
return true;
if (ch < 0x20)
return false;
if (ch != '\\')
continue;
if (c->p >= c->end)
return false;
switch (*c->p++) {
case '"':
case '\\':
case '/':
case 'b':
case 'f':
case 'n':
case 'r':
case 't':
break;
case 'u':
for (int i = 0; i < 4; i++, c->p++) {
if (c->p >= c->end || !isxdigit((unsigned char)*c->p))
return false;
}
break;
default:
return false;
}
}
return false;
}
static bool NdpiJsonLiteral(NdpiJsonCursor *c, const char *lit)
{
const size_t len = strlen(lit);
if ((size_t)(c->end - c->p) < len || memcmp(c->p, lit, len) != 0)
return false;
c->p += len;
return true;
}
static bool NdpiJsonNumber(NdpiJsonCursor *c)
{
bool digits = false;
while (c->p < c->end && (isdigit((unsigned char)*c->p) || *c->p == '-' || *c->p == '+' ||
*c->p == '.' || *c->p == 'e' || *c->p == 'E')) {
digits |= isdigit((unsigned char)*c->p) != 0;
c->p++;
}
return digits;
}
/**
* Members of an object or elements of an array, up to the closing
* bracket, or up to the end of the input for a bare object body.
*/
static bool NdpiJsonMembers(NdpiJsonCursor *c, int depth, char closing)
{
NdpiJsonSkipSpace(c);
if (closing != '\0' && c->p < c->end && *c->p == closing) {
c->p++;
return true;
}
for (;;) {
if (closing != ']') {
if (!NdpiJsonString(c))
return false;
NdpiJsonSkipSpace(c);
if (c->p >= c->end || *c->p++ != ':')
return false;
}
if (!NdpiJsonValue(c, depth))
return false;
NdpiJsonSkipSpace(c);
if (c->p >= c->end)
return closing == '\0';
if (closing != '\0' && *c->p == closing) {
c->p++;
return true;
}
if (*c->p++ != ',')
return false;
NdpiJsonSkipSpace(c);
}
}
static bool NdpiJsonValue(NdpiJsonCursor *c, int depth)
{
if (depth > 32)
return false;
NdpiJsonSkipSpace(c);
if (c->p >= c->end)
return false;
switch (*c->p) {
case '{':
c->p++;
return NdpiJsonMembers(c, depth + 1, '}');
case '[':
c->p++;
return NdpiJsonMembers(c, depth + 1, ']');
case '"':
return NdpiJsonString(c);
case 't':
return NdpiJsonLiteral(c, "true");
case 'f':
return NdpiJsonLiteral(c, "false");
case 'n':
return NdpiJsonLiteral(c, "null");
default:
return NdpiJsonNumber(c);
}
}
/**
* \brief Whether buf holds well formed object members, i.e. the body of
* a JSON object without its braces, as SCJbSetFormatted() expects.
*/
static bool NdpiJsonFragmentValid(const char *buf, uint32_t len)
{
NdpiJsonCursor c = { .p = buf, .end = buf + len };
return NdpiJsonMembers(&c, 1, '\0');
}
static void EveCallback(ThreadVars *tv, const Packet *p, Flow *f, SCJsonBuilder *jb, void *data)
{
/* Adding ndpi info to EVE requires a flow. */
if (f == NULL) {
return;
}
struct NdpiThreadContext *threadctx = NdpiGetThreadContext(tv);
if (threadctx == NULL || threadctx->ndpi == NULL) {
return;
}
struct NdpiFlowContext *flowctx = NdpiGetFlowContext(f);
if (flowctx == NULL || flowctx->ndpi_flow == NULL) {
return;
}
ndpi_serializer serializer;
char *buffer;
uint32_t buffer_len;
SCLogDebug("EveCallback: tv=%p, p=%p, f=%p", tv, p, f);
if (ndpi_init_serializer_ll(&serializer, ndpi_serialization_format_inner_json,
NDPI_EVE_SERIALIZER_SIZE) != 0) {
SCLogDebug("Failed to initialize nDPI serializer");
return;
}
/* Use ndpi_dpi2json to get a JSON with nDPI metadata */
ndpi_dpi2json(threadctx->ndpi, flowctx->ndpi_flow, flowctx->detected_l7_protocol, &serializer);
buffer = ndpi_serializer_get_buffer(&serializer, &buffer_len);
if (buffer != NULL && buffer_len > 0) {
bool valid = NdpiJsonFragmentValid(buffer, buffer_len);
if (valid) {
/* SCJbSetFormatted() takes a C string and nDPI only terminates
* its buffer when there is room left for it */
char stack_json[2048];
char *json = buffer_len < sizeof(stack_json) ? stack_json : SCMalloc(buffer_len + 1);
if (json != NULL) {
memcpy(json, buffer, buffer_len);
json[buffer_len] = '\0';
/* also rejects invalid UTF-8, without inserting anything */
valid = SCJbSetFormatted(jb, json);
if (json != stack_json)
SCFree(json);
}
}
if (!valid && SC_ATOMIC_ADD(ndpi_json_dropped, 1) == 0) {
SCLogWarning("nDPI produced malformed JSON for a flow, dropping its EVE object "
"(only reported once)");
}
}
ndpi_term_serializer(&serializer);
}
static void NdpInitRiskKeyword(void)
{
/* SCSigTableAppLiteElmt and SCDetectHelperKeywordRegister don't yet
* support all the fields required to register the nDPI keywords,
* missing the (packet) Match callback,
* so we'll just register with an empty keyword specifier to get
* the ID, then fill in the ID. */
ndpi_protocol_keyword_id = SCDetectHelperNewKeywordId();
SCLogDebug("Registered new ndpi-protocol keyword with ID %" PRIu32, ndpi_protocol_keyword_id);
sigmatch_table[ndpi_protocol_keyword_id].name = "ndpi-protocol";
sigmatch_table[ndpi_protocol_keyword_id].desc = "match on the detected nDPI protocol";
sigmatch_table[ndpi_protocol_keyword_id].url = "/rules/ndpi-protocol.html";
sigmatch_table[ndpi_protocol_keyword_id].Match = DetectnDPIProtocolPacketMatch;
sigmatch_table[ndpi_protocol_keyword_id].Setup = DetectnDPIProtocolSetup;
sigmatch_table[ndpi_protocol_keyword_id].Free = DetectnDPIProtocolFree;
sigmatch_table[ndpi_protocol_keyword_id].flags =
(SIGMATCH_QUOTES_OPTIONAL | SIGMATCH_HANDLE_NEGATION);
ndpi_risk_keyword_id = SCDetectHelperNewKeywordId();
SCLogDebug("Registered new ndpi-risk keyword with ID %" PRIu32, ndpi_risk_keyword_id);
sigmatch_table[ndpi_risk_keyword_id].name = "ndpi-risk";
sigmatch_table[ndpi_risk_keyword_id].desc = "match on the detected nDPI risk";
sigmatch_table[ndpi_risk_keyword_id].url = "/rules/ndpi-risk.html";
sigmatch_table[ndpi_risk_keyword_id].Match = DetectnDPIRiskPacketMatch;
sigmatch_table[ndpi_risk_keyword_id].Setup = DetectnDPIRiskSetup;
sigmatch_table[ndpi_risk_keyword_id].Free = DetectnDPIRiskFree;
sigmatch_table[ndpi_risk_keyword_id].flags =
(SIGMATCH_QUOTES_OPTIONAL | SIGMATCH_HANDLE_NEGATION);
}
static void NdpiInit(void)
{
SCLogDebug("Initializing nDPI plugin");
ndpi_license = NdpiResolveLicense();
/* Global context shared by the worker modules, see NdpiModuleNew().
* NULL if nDPI was built without global context support. There is
* no plugin deinit hook, so it is never released. */
ndpi_g_ctx = ndpi_global_init();
if (ndpi_g_ctx == NULL) {
SCLogWarning("Failed to initialize the nDPI global context: "
"per-thread nDPI caches will not be shared");
}
/* Register thread storage. */
thread_storage_id = SCThreadStorageRegister("ndpi", ThreadStorageFree);
if (thread_storage_id.id < 0) {
FatalError("Failed to register nDPI thread storage");
}
/* Register flow storage. */
flow_storage_id = SCFlowStorageRegister("ndpi", FlowStorageFree);
if (flow_storage_id.id < 0) {
FatalError("Failed to register nDPI flow storage");
}
/* Register flow lifecycle callbacks. */
SCFlowRegisterInitCallback(OnFlowInit, NULL);
SCFlowRegisterUpdateCallback(OnFlowUpdate, NULL);
/* Not needed for nDPI, but exists for completeness. */
SCFlowRegisterFinishCallback(OnFlowFinish, NULL);
/* Register thread init callback. */
SCThreadRegisterInitCallback(OnThreadInit, NULL);
/* Register an EVE callback. */
SCEveRegisterCallback(EveCallback, NULL);
NdpInitRiskKeyword();
}
const SCPlugin PluginRegistration = {
.version = SC_API_VERSION,
.suricata_version = SC_PACKAGE_VERSION,
.name = "ndpi",
.author = "Luca Deri",
.license = "GPLv3",
.Init = NdpiInit,
};
const SCPlugin *SCPluginRegister()
{
return &PluginRegistration;
}