From b800ace56fb08c29aadb0f68a3bad1fe3e4a0d67 Mon Sep 17 00:00:00 2001 From: Yash Datre Date: Wed, 8 Jul 2026 02:54:07 +0000 Subject: [PATCH] detect: add tcp.session keyword for unified TCP lifecycle matching Introduce the tcp.session: keyword that accepts a comma-separated subset of {setup, established, closing} and matches packets whose TCP session state falls within the named phases. This lets a rule writer cover the full TCP lifecycle in one rule instead of separate flow:not_established + flow:established rules. Changes: - New detect-tcp-session.{c,h} with parser, match function, unit tests - Register DETECT_TCP_SESSION in detect-engine-register.{c,h} - Add to Makefile.am source list - Extend engine-analyzer text output for tcp.session: - Extend firewall.json keyword_info with tcp_session phase values - Update flow-keywords.rst documentation - Require TCP protocol and SIG_FLAG_REQUIRE_PACKET at setup Ticket: 7704 (cherry picked from commit 398687d5d4a98c5a641dd77165766fd7732276e7) --- doc/userguide/rules/flow-keywords.rst | 95 ++++ src/Makefile.am | 2 + src/detect-engine-analyzer.c | 48 +++ src/detect-engine-register.c | 2 + src/detect-engine-register.h | 2 + src/detect-tcp-session.c | 598 ++++++++++++++++++++++++++ src/detect-tcp-session.h | 39 ++ 7 files changed, 786 insertions(+) create mode 100644 src/detect-tcp-session.c create mode 100644 src/detect-tcp-session.h diff --git a/doc/userguide/rules/flow-keywords.rst b/doc/userguide/rules/flow-keywords.rst index 20c6a068ac..f5bf3fcaf3 100644 --- a/doc/userguide/rules/flow-keywords.rst +++ b/doc/userguide/rules/flow-keywords.rst @@ -126,6 +126,101 @@ The determination of *established* depends on the protocol: .. image:: flow-keywords/Flow2.png +.. _tcp-session: + +tcp.session +----------- + +In firewall mode, allowing a TCP connection typically requires covering both +the handshake (``flow:not_established``) and the data transfer phase +(``flow:established``). Without ``tcp.session``, this means two separate rules. +The ``tcp.session`` keyword lets a rule writer collapse both into a single rule, +reducing boilerplate:: + + # Before: two rules needed + accept:hook tcp:all any any <> any any (flow:not_established; sid:1;) + accept:hook tcp:all any any <> any any (flow:established; sid:2;) + + # After: one rule + accept:hook tcp:all any any <> any any (tcp.session:setup,established; sid:1;) + +The ``tcp.session`` keyword matches on one or more phases of the TCP session +lifecycle. It works in all engine modes (IDS, IPS, and firewall). + +The keyword argument is a comma-separated subset of ``setup``, ``established`` +and ``closing``. Tokens are case-sensitive (lowercase only), may appear in any +order, and must not be duplicated. Multiple phases are evaluated as logical OR: +a packet matches if it belongs to *any* of the named phases. + +Format:: + + tcp.session:[,...]; + +The three phases map to the following TCP states: + +setup + ``TCP_NONE``, ``TCP_SYN_SENT``, ``TCP_SYN_RECV``. Equivalent to + ``flow:not_established`` on non-midstream flows. +established + ``TCP_ESTABLISHED``. Equivalent to ``flow:established`` on non-midstream + flows. +closing + ``TCP_FIN_WAIT1``, ``TCP_FIN_WAIT2``, ``TCP_TIME_WAIT``, ``TCP_LAST_ACK``, + ``TCP_CLOSE_WAIT``, ``TCP_CLOSING``. The terminal ``TCP_CLOSED`` state is + not part of any phase. + +The state sets are disjoint, but a single packet may match more than one phase +because ``FLOW_PKT_ESTABLISHED`` is sticky — it stays set during closing +states. A packet in ``TCP_FIN_WAIT1`` matches both ``tcp.session:established`` +and ``tcp.session:closing``. + +Examples:: + + tcp.session:setup + tcp.session:setup,established + tcp.session:established,closing + tcp.session:setup,established,closing + +.. container:: example-rule + + :example-rule-action:`alert` :example-rule-header:`tcp any any -> any any` (:example-rule-options:`tcp.session:setup,established;` sid:1;) + +In firewall mode it is typically used in an ``accept`` rule:: + + accept:hook tcp:all any any <> any any (tcp.session:setup,established; sid:1;) + +.. note:: + + When used in the ``pre_stream`` hook, the TCP session state has not yet + been updated for the current packet. The keyword reflects the state + *before* the stream engine processes the packet. For example, a SYN + packet in the ``pre_stream`` hook will see the state from the previous + packet (``TCP_NONE`` for the first packet of a flow). + +.. note:: + + Using ``tcp.session`` in the ``pre_flow`` hook is not meaningful because + no flow (and thus no TCP session) exists yet at that point. The keyword + will never match in ``pre_flow``. + +Midstream pickup +~~~~~~~~~~~~~~~~~ + +When a flow is picked up midstream, ``tcp.session`` evaluates the flow's +current TCP state directly. A midstream pickup in ``TCP_ESTABLISHED`` matches +``tcp.session:established`` and does **not** match ``tcp.session:setup``. + +Combining with the ``flow`` keyword +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``tcp.session`` may be combined with ``flow:not_established`` or +``flow:established`` in the same rule. Both keywords are evaluated +independently (logical AND). On non-midstream flows, ``tcp.session:setup`` is +equivalent to ``flow:not_established`` and ``tcp.session:established`` is +equivalent to ``flow:established``. + +.. note:: ``tcp.session`` is TCP-specific; it never matches on non-TCP flows. + .. _flowint: flowint diff --git a/src/Makefile.am b/src/Makefile.am index b2ab7a2746..3f78e26a44 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -299,6 +299,7 @@ noinst_HEADERS = \ detect-tcp-ack.h \ detect-tcp-flags.h \ detect-tcp-seq.h \ + detect-tcp-session.h \ detect-tcp-window.h \ detect-tcp-wscale.h \ detect-tcphdr.h \ @@ -900,6 +901,7 @@ libsuricata_c_a_SOURCES = \ detect-tcp-ack.c \ detect-tcp-flags.c \ detect-tcp-seq.c \ + detect-tcp-session.c \ detect-tcp-window.c \ detect-tcp-wscale.c \ detect-tcphdr.c \ diff --git a/src/detect-engine-analyzer.c b/src/detect-engine-analyzer.c index 01dbbebf83..6608119db6 100644 --- a/src/detect-engine-analyzer.c +++ b/src/detect-engine-analyzer.c @@ -55,6 +55,8 @@ #include "detect-icmp-id.h" #include "detect-tcp-window.h" #include "detect-app-layer-protocol.h" +#include "app-layer-parser.h" +#include "detect-tcp-session.h" static int rule_warnings_only = 0; @@ -991,6 +993,20 @@ static void DumpMatches(RuleAnalyzer *ctx, SCJsonBuilder *js, const SigMatchData SCJbClose(js); break; } + case DETECT_TCP_SESSION: { + const DetectTcpSessionData *tsd = (const DetectTcpSessionData *)smd->ctx; + SCJbOpenObject(js, "tcp_session"); + SCJbOpenArray(js, "phases"); + if (tsd->phase_flags & DETECT_TCP_SESSION_PHASE_SETUP) + SCJbAppendString(js, "setup"); + if (tsd->phase_flags & DETECT_TCP_SESSION_PHASE_ESTABLISHED) + SCJbAppendString(js, "established"); + if (tsd->phase_flags & DETECT_TCP_SESSION_PHASE_CLOSING) + SCJbAppendString(js, "closing"); + SCJbClose(js); // phases + SCJbClose(js); // tcp_session + break; + } } SCJbClose(js); @@ -2274,6 +2290,38 @@ int FirewallAnalyzer(const DetectEngineCtx *de_ctx) SCJbClose(ctx.js); // lists + /* Per-rule keyword metadata for tcp.session */ + SCJbOpenObject(ctx.js, "keyword_info"); + for (Signature *s = de_ctx->sig_list; s != NULL; s = s->next) { + const SigMatchData *smd = s->sm_arrays[DETECT_SM_LIST_MATCH]; + if (smd == NULL) + continue; + for (;;) { + if (smd->type == DETECT_TCP_SESSION) { + const DetectTcpSessionData *tsd = (const DetectTcpSessionData *)smd->ctx; + if (tsd != NULL) { + char sid_key[32]; + snprintf(sid_key, sizeof(sid_key), "%u", s->id); + SCJbOpenObject(ctx.js, sid_key); + SCJbOpenArray(ctx.js, "tcp_session"); + if (tsd->phase_flags & DETECT_TCP_SESSION_PHASE_SETUP) + SCJbAppendString(ctx.js, "setup"); + if (tsd->phase_flags & DETECT_TCP_SESSION_PHASE_ESTABLISHED) + SCJbAppendString(ctx.js, "established"); + if (tsd->phase_flags & DETECT_TCP_SESSION_PHASE_CLOSING) + SCJbAppendString(ctx.js, "closing"); + SCJbClose(ctx.js); // tcp_session + SCJbClose(ctx.js); // sid_key + } + break; + } + if (smd->is_last) + break; + smd++; + } + } + SCJbClose(ctx.js); // keyword_info + SCJbClose(ctx.js); // top level object const char *filename = "firewall.json"; diff --git a/src/detect-engine-register.c b/src/detect-engine-register.c index da576623e4..e8f84b3a3e 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -124,6 +124,7 @@ #include "detect-requires.h" #include "detect-tcp-window.h" #include "detect-tcp-wscale.h" +#include "detect-tcp-session.h" #include "detect-ftpbounce.h" #include "detect-ftp-dynamic-port.h" #include "detect-isdataat.h" @@ -626,6 +627,7 @@ void SigTableSetup(void) DetectOffsetRegister(); DetectReplaceRegister(); DetectFlowRegister(); + DetectTcpSessionRegister(); DetectFlowAgeRegister(); DetectFlowPktsRegister(); DetectFlowPktsToServerRegister(); diff --git a/src/detect-engine-register.h b/src/detect-engine-register.h index 0e5e52242c..a151975836 100644 --- a/src/detect-engine-register.h +++ b/src/detect-engine-register.h @@ -54,6 +54,8 @@ enum DetectKeywordId { DETECT_FLOW, /* end prefilter sort */ + DETECT_TCP_SESSION, + /* values used in util-var.c go here, to avoid int overflows */ DETECT_THRESHOLD, DETECT_FLOWBITS, diff --git a/src/detect-tcp-session.c b/src/detect-tcp-session.c new file mode 100644 index 0000000000..63df570b18 --- /dev/null +++ b/src/detect-tcp-session.c @@ -0,0 +1,598 @@ +/* Copyright (C) 2026 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** \file \brief tcp.session: keyword (Redmine #7704). */ + +#include "suricata-common.h" + +#include "detect.h" +#include "detect-engine.h" +#include "detect-parse.h" + +#include "flow.h" +#include "stream-tcp.h" +#include "stream-tcp-private.h" + +#include "util-debug.h" +#include "util-unittest.h" + +#include "detect-tcp-session.h" + +/** Upper bound on the total length of the keyword argument. */ +#define DETECT_TCP_SESSION_ARG_MAX_LEN 32 + +/* prototypes */ +static int DetectTcpSessionMatch( + DetectEngineThreadCtx *, Packet *, const Signature *, const SigMatchCtx *); +static int DetectTcpSessionSetup(DetectEngineCtx *, Signature *, const char *); +static void DetectTcpSessionFree(DetectEngineCtx *, void *); +#ifdef UNITTESTS +static void DetectTcpSessionRegisterTests(void); +#endif + +/** Map a single phase token to its phase flag. */ +static uint8_t DetectTcpSessionPhaseFlagFromToken(const char *token) +{ + if (strcmp(token, "setup") == 0) { + return DETECT_TCP_SESSION_PHASE_SETUP; + } + if (strcmp(token, "established") == 0) { + return DETECT_TCP_SESSION_PHASE_ESTABLISHED; + } + if (strcmp(token, "closing") == 0) { + return DETECT_TCP_SESSION_PHASE_CLOSING; + } + return 0; +} + +/** Parse a tcp.session: keyword argument into a DetectTcpSessionData. */ +static DetectTcpSessionData *DetectTcpSessionParse(const char *arg) +{ + if (arg == NULL || strlen(arg) == 0) { + SCLogError("tcp.session keyword requires a value: comma-separated " + "subset of {setup, established, closing}"); + return NULL; + } + + if (strlen(arg) > DETECT_TCP_SESSION_ARG_MAX_LEN) { + SCLogError("tcp.session argument too long (%zu > %d): accepted " + "tokens are {setup, established, closing}", + strlen(arg), DETECT_TCP_SESSION_ARG_MAX_LEN); + return NULL; + } + + /* Reject leading, trailing, or consecutive commas — strtok_r would + * silently skip them, but we treat these as syntax errors. */ + if (arg[0] == ',' || arg[strlen(arg) - 1] == ',') { + SCLogError("tcp.session: leading or trailing comma in argument " + "\"%s\"; accepted tokens are {setup, established, closing}", + arg); + return NULL; + } + if (strstr(arg, ",,") != NULL) { + SCLogError("tcp.session: consecutive commas in argument \"%s\"; " + "accepted tokens are {setup, established, closing}", + arg); + return NULL; + } + + uint8_t phase_flags = 0; + + /* Work on a mutable copy for strtok_r. */ + char *argdup = SCStrdup(arg); + if (argdup == NULL) { + return NULL; + } + + char *saveptr = NULL; + char *token = strtok_r(argdup, ",", &saveptr); + + while (token != NULL) { + if (strlen(token) == 0) { + SCLogError("tcp.session: empty token in argument \"%s\"; " + "accepted tokens are {setup, established, closing}", + arg); + SCFree(argdup); + return NULL; + } + + const uint8_t flag = DetectTcpSessionPhaseFlagFromToken(token); + if (flag == 0) { + SCLogError("tcp.session: unknown token \"%s\" in argument " + "\"%s\"; accepted tokens are {setup, established, " + "closing}", + token, arg); + SCFree(argdup); + return NULL; + } + + if (phase_flags & flag) { + SCLogError("tcp.session: duplicate token \"%s\" in argument " + "\"%s\"", + token, arg); + SCFree(argdup); + return NULL; + } + + phase_flags |= flag; + token = strtok_r(NULL, ",", &saveptr); + } + + SCFree(argdup); + + if (phase_flags == 0) { + SCLogError("tcp.session keyword requires a value: comma-separated " + "subset of {setup, established, closing}"); + return NULL; + } + + DetectTcpSessionData *data = SCMalloc(sizeof(*data)); + if (unlikely(data == NULL)) { + return NULL; + } + data->phase_flags = phase_flags; + return data; +} + +/** Per-packet match: returns 1 iff the packet's TCP session phase intersects phase_flags. */ +static int DetectTcpSessionMatch( + DetectEngineThreadCtx *det_ctx, Packet *p, const Signature *s, const SigMatchCtx *ctx) +{ + const DetectTcpSessionData *d = (const DetectTcpSessionData *)ctx; + const Flow *f = p->flow; + + if (f == NULL || f->proto != IPPROTO_TCP) { + return 0; + } + + uint8_t pkt_phase = 0; + + /* Setup vs established split via FLOW_PKT_ESTABLISHED. */ + if (p->flowflags & FLOW_PKT_ESTABLISHED) { + pkt_phase |= DETECT_TCP_SESSION_PHASE_ESTABLISHED; + } else { + pkt_phase |= DETECT_TCP_SESSION_PHASE_SETUP; + } + + /* Closing phase: consult TcpSession::state directly. */ + const TcpSession *ssn = (const TcpSession *)f->protoctx; + if (ssn != NULL) { + switch (ssn->state) { + case TCP_FIN_WAIT1: + case TCP_FIN_WAIT2: + case TCP_TIME_WAIT: + case TCP_LAST_ACK: + case TCP_CLOSE_WAIT: + case TCP_CLOSING: + pkt_phase |= DETECT_TCP_SESSION_PHASE_CLOSING; + break; + default: + break; + } + } + + return (pkt_phase & d->phase_flags) ? 1 : 0; +} + +/** Setup function: parse argument and append the SigMatch. */ +static int DetectTcpSessionSetup(DetectEngineCtx *de_ctx, Signature *s, const char *arg) +{ + if (!(DetectProtoContainsProto(&s->proto, IPPROTO_TCP))) { + SCLogError("tcp.session requires a TCP rule"); + return -1; + } + + DetectTcpSessionData *data = DetectTcpSessionParse(arg); + if (data == NULL) { + return -1; + } + + if (SCSigMatchAppendSMToList( + de_ctx, s, DETECT_TCP_SESSION, (SigMatchCtx *)data, DETECT_SM_LIST_MATCH) == NULL) { + DetectTcpSessionFree(de_ctx, data); + return -1; + } + + s->flags |= SIG_FLAG_REQUIRE_PACKET; + return 0; +} + +/** Free a DetectTcpSessionData allocation. */ +static void DetectTcpSessionFree(DetectEngineCtx *de_ctx, void *ptr) +{ + if (ptr != NULL) { + SCFree(ptr); + } +} + +/** Registration function for the tcp.session: keyword. */ +void DetectTcpSessionRegister(void) +{ + sigmatch_table[DETECT_TCP_SESSION].name = "tcp.session"; + sigmatch_table[DETECT_TCP_SESSION].desc = + "match TCP session lifecycle phase set (setup, established, closing)"; + sigmatch_table[DETECT_TCP_SESSION].url = "/rules/flow-keywords.html#tcp-session"; + sigmatch_table[DETECT_TCP_SESSION].Match = DetectTcpSessionMatch; + sigmatch_table[DETECT_TCP_SESSION].Setup = DetectTcpSessionSetup; + sigmatch_table[DETECT_TCP_SESSION].Free = DetectTcpSessionFree; + sigmatch_table[DETECT_TCP_SESSION].flags = SIGMATCH_SUPPORT_FIREWALL; +#ifdef UNITTESTS + sigmatch_table[DETECT_TCP_SESSION].RegisterTests = DetectTcpSessionRegisterTests; +#endif +} + +#ifdef UNITTESTS +#include "util-unittest.h" +#include "util-unittest-helper.h" +#include "detect-engine.h" +#include "detect-flow.h" + +int DetectFlowMatch(DetectEngineThreadCtx *, Packet *, const Signature *, const SigMatchCtx *); + +/** Test01: single-token parsing. */ +static int DetectTcpSessionTest01(void) +{ + DetectTcpSessionData *d = DetectTcpSessionParse("setup"); + FAIL_IF_NULL(d); + FAIL_IF_NOT(d->phase_flags == DETECT_TCP_SESSION_PHASE_SETUP); + DetectTcpSessionFree(NULL, d); + + d = DetectTcpSessionParse("established"); + FAIL_IF_NULL(d); + FAIL_IF_NOT(d->phase_flags == DETECT_TCP_SESSION_PHASE_ESTABLISHED); + DetectTcpSessionFree(NULL, d); + + d = DetectTcpSessionParse("closing"); + FAIL_IF_NULL(d); + FAIL_IF_NOT(d->phase_flags == DETECT_TCP_SESSION_PHASE_CLOSING); + DetectTcpSessionFree(NULL, d); + + PASS; +} + +/** Test02: multi-token parsing and order independence. */ +static int DetectTcpSessionTest02(void) +{ + DetectTcpSessionData *d = DetectTcpSessionParse("setup,established"); + FAIL_IF_NULL(d); + FAIL_IF_NOT(d->phase_flags == + (DETECT_TCP_SESSION_PHASE_SETUP | DETECT_TCP_SESSION_PHASE_ESTABLISHED)); + DetectTcpSessionFree(NULL, d); + + d = DetectTcpSessionParse("established,setup"); + FAIL_IF_NULL(d); + FAIL_IF_NOT(d->phase_flags == + (DETECT_TCP_SESSION_PHASE_SETUP | DETECT_TCP_SESSION_PHASE_ESTABLISHED)); + DetectTcpSessionFree(NULL, d); + + d = DetectTcpSessionParse("setup,established,closing"); + FAIL_IF_NULL(d); + FAIL_IF_NOT(d->phase_flags == + (DETECT_TCP_SESSION_PHASE_SETUP | DETECT_TCP_SESSION_PHASE_ESTABLISHED | + DETECT_TCP_SESSION_PHASE_CLOSING)); + DetectTcpSessionFree(NULL, d); + + PASS; +} + +/** Test03: closing token and closing combinations. */ +static int DetectTcpSessionTest03(void) +{ + DetectTcpSessionData *d = DetectTcpSessionParse("closing"); + FAIL_IF_NULL(d); + FAIL_IF_NOT(d->phase_flags == DETECT_TCP_SESSION_PHASE_CLOSING); + DetectTcpSessionFree(NULL, d); + + d = DetectTcpSessionParse("established,closing"); + FAIL_IF_NULL(d); + FAIL_IF_NOT(d->phase_flags == + (DETECT_TCP_SESSION_PHASE_ESTABLISHED | DETECT_TCP_SESSION_PHASE_CLOSING)); + DetectTcpSessionFree(NULL, d); + + PASS; +} + +/** Test04: empty/missing value is rejected. */ +static int DetectTcpSessionTest04(void) +{ + FAIL_IF_NOT_NULL(DetectTcpSessionParse(NULL)); + FAIL_IF_NOT_NULL(DetectTcpSessionParse("")); + FAIL_IF_NOT_NULL(DetectTcpSessionParse(",")); + FAIL_IF_NOT_NULL(DetectTcpSessionParse(",setup")); + FAIL_IF_NOT_NULL(DetectTcpSessionParse("setup,")); + FAIL_IF_NOT_NULL(DetectTcpSessionParse("setup,,established")); + PASS; +} + +/** Test05: unknown/whitespace/over-length tokens are rejected. */ +static int DetectTcpSessionTest05(void) +{ + FAIL_IF_NOT_NULL(DetectTcpSessionParse("bogus")); + FAIL_IF_NOT_NULL(DetectTcpSessionParse("setup,bogus")); + FAIL_IF_NOT_NULL(DetectTcpSessionParse("SETUP")); + FAIL_IF_NOT_NULL(DetectTcpSessionParse(" setup")); + FAIL_IF_NOT_NULL(DetectTcpSessionParse("setup ")); + FAIL_IF_NOT_NULL(DetectTcpSessionParse("setu p")); + char longarg[128]; + memset(longarg, 'a', sizeof(longarg) - 1); + longarg[sizeof(longarg) - 1] = '\0'; + FAIL_IF_NOT_NULL(DetectTcpSessionParse(longarg)); + PASS; +} + +/** Test06: duplicate tokens are rejected. */ +static int DetectTcpSessionTest06(void) +{ + FAIL_IF_NOT_NULL(DetectTcpSessionParse("setup,setup")); + FAIL_IF_NOT_NULL(DetectTcpSessionParse("established,established")); + FAIL_IF_NOT_NULL(DetectTcpSessionParse("setup,established,setup")); + PASS; +} + +/** Build a minimal TCP packet+flow harness for match tests. */ +static Packet *DetectTcpSessionTestBuildPacket( + Flow *f, TcpSession *ssn, TCPHdr *tcph, uint8_t state, bool established) +{ + Packet *p = PacketGetFromAlloc(); + if (p == NULL) + return NULL; + + memset(f, 0, sizeof(*f)); + memset(ssn, 0, sizeof(*ssn)); + memset(tcph, 0, sizeof(*tcph)); + + f->proto = IPPROTO_TCP; + ssn->state = state; + f->protoctx = ssn; + + p->flow = f; + PacketSetTCP(p, (uint8_t *)tcph); + p->flowflags |= FLOW_PKT_TOSERVER; + if (established) { + p->flowflags |= FLOW_PKT_ESTABLISHED; + } + return p; +} + +/** Test07: established flow matches tcp.session:established. */ +static int DetectTcpSessionTest07(void) +{ + Flow f; + TcpSession ssn; + TCPHdr tcph; + ThreadVars tv; + DetectEngineThreadCtx dtx; + Signature s; + memset(&tv, 0, sizeof(tv)); + memset(&dtx, 0, sizeof(dtx)); + memset(&s, 0, sizeof(s)); + + Packet *p = DetectTcpSessionTestBuildPacket(&f, &ssn, &tcph, TCP_ESTABLISHED, true); + FAIL_IF_NULL(p); + + DetectTcpSessionData *d = DetectTcpSessionParse("established"); + FAIL_IF_NULL(d); + + FAIL_IF_NOT(DetectTcpSessionMatch(&dtx, p, &s, (const SigMatchCtx *)d) == 1); + + DetectTcpSessionFree(NULL, d); + PacketFree(p); + PASS; +} + +/** Test08: SYN_SENT flow matches tcp.session:setup but not established. */ +static int DetectTcpSessionTest08(void) +{ + Flow f; + TcpSession ssn; + TCPHdr tcph; + ThreadVars tv; + DetectEngineThreadCtx dtx; + Signature s; + memset(&tv, 0, sizeof(tv)); + memset(&dtx, 0, sizeof(dtx)); + memset(&s, 0, sizeof(s)); + + Packet *p = DetectTcpSessionTestBuildPacket(&f, &ssn, &tcph, TCP_SYN_SENT, false); + FAIL_IF_NULL(p); + + DetectTcpSessionData *setup = DetectTcpSessionParse("setup"); + FAIL_IF_NULL(setup); + FAIL_IF_NOT(DetectTcpSessionMatch(&dtx, p, &s, (const SigMatchCtx *)setup) == 1); + DetectTcpSessionFree(NULL, setup); + + DetectTcpSessionData *est = DetectTcpSessionParse("established"); + FAIL_IF_NULL(est); + FAIL_IF_NOT(DetectTcpSessionMatch(&dtx, p, &s, (const SigMatchCtx *)est) == 0); + DetectTcpSessionFree(NULL, est); + + PacketFree(p); + PASS; +} + +/** Test09: midstream pickup in TCP_ESTABLISHED matches established, not setup. */ +static int DetectTcpSessionTest09(void) +{ + Flow f; + TcpSession ssn; + TCPHdr tcph; + ThreadVars tv; + DetectEngineThreadCtx dtx; + Signature s; + memset(&tv, 0, sizeof(tv)); + memset(&dtx, 0, sizeof(dtx)); + memset(&s, 0, sizeof(s)); + + Packet *p = DetectTcpSessionTestBuildPacket(&f, &ssn, &tcph, TCP_ESTABLISHED, true); + FAIL_IF_NULL(p); + ssn.flags |= STREAMTCP_FLAG_MIDSTREAM | STREAMTCP_FLAG_MIDSTREAM_ESTABLISHED; + + DetectTcpSessionData *est = DetectTcpSessionParse("established"); + FAIL_IF_NULL(est); + FAIL_IF_NOT(DetectTcpSessionMatch(&dtx, p, &s, (const SigMatchCtx *)est) == 1); + DetectTcpSessionFree(NULL, est); + + DetectTcpSessionData *setup = DetectTcpSessionParse("setup"); + FAIL_IF_NULL(setup); + FAIL_IF_NOT(DetectTcpSessionMatch(&dtx, p, &s, (const SigMatchCtx *)setup) == 0); + DetectTcpSessionFree(NULL, setup); + + PacketFree(p); + PASS; +} + +/** Test10: equivalence with flow:established / flow:not_established. */ +static int DetectTcpSessionTest10(void) +{ + ThreadVars tv; + DetectEngineThreadCtx dtx; + Signature s; + memset(&tv, 0, sizeof(tv)); + memset(&dtx, 0, sizeof(dtx)); + memset(&s, 0, sizeof(s)); + + DetectTcpSessionData *ts_setup = DetectTcpSessionParse("setup"); + FAIL_IF_NULL(ts_setup); + DetectTcpSessionData *ts_est = DetectTcpSessionParse("established"); + FAIL_IF_NULL(ts_est); + + DetectFlowData fd_not_est = { .flags = DETECT_FLOW_FLAG_NOT_ESTABLISHED, .match_cnt = 1 }; + DetectFlowData fd_est = { .flags = DETECT_FLOW_FLAG_ESTABLISHED, .match_cnt = 1 }; + + const uint8_t states[] = { TCP_NONE, TCP_SYN_SENT, TCP_SYN_RECV, TCP_ESTABLISHED }; + const bool est_flags[] = { false, true }; + + for (size_t si = 0; si < sizeof(states) / sizeof(states[0]); si++) { + for (size_t ei = 0; ei < sizeof(est_flags) / sizeof(est_flags[0]); ei++) { + Flow f; + TcpSession ssn; + TCPHdr tcph; + Packet *p = DetectTcpSessionTestBuildPacket(&f, &ssn, &tcph, states[si], est_flags[ei]); + FAIL_IF_NULL(p); + + int ts_setup_r = DetectTcpSessionMatch(&dtx, p, &s, (const SigMatchCtx *)ts_setup); + int flow_not_est_r = DetectFlowMatch(&dtx, p, &s, (const SigMatchCtx *)&fd_not_est); + FAIL_IF_NOT(ts_setup_r == flow_not_est_r); + + int ts_est_r = DetectTcpSessionMatch(&dtx, p, &s, (const SigMatchCtx *)ts_est); + int flow_est_r = DetectFlowMatch(&dtx, p, &s, (const SigMatchCtx *)&fd_est); + FAIL_IF_NOT(ts_est_r == flow_est_r); + + PacketFree(p); + } + } + + DetectTcpSessionFree(NULL, ts_setup); + DetectTcpSessionFree(NULL, ts_est); + PASS; +} + +static int DetectTcpSessionMatchExhaustive(void); + +static void DetectTcpSessionRegisterTests(void) +{ + UtRegisterTest("DetectTcpSessionTest01", DetectTcpSessionTest01); + UtRegisterTest("DetectTcpSessionTest02", DetectTcpSessionTest02); + UtRegisterTest("DetectTcpSessionTest03", DetectTcpSessionTest03); + UtRegisterTest("DetectTcpSessionTest04", DetectTcpSessionTest04); + UtRegisterTest("DetectTcpSessionTest05", DetectTcpSessionTest05); + UtRegisterTest("DetectTcpSessionTest06", DetectTcpSessionTest06); + UtRegisterTest("DetectTcpSessionTest07", DetectTcpSessionTest07); + UtRegisterTest("DetectTcpSessionTest08", DetectTcpSessionTest08); + UtRegisterTest("DetectTcpSessionTest09", DetectTcpSessionTest09); + UtRegisterTest("DetectTcpSessionTest10", DetectTcpSessionTest10); + UtRegisterTest("DetectTcpSessionMatchExhaustive", DetectTcpSessionMatchExhaustive); +} +#endif /* UNITTESTS */ + +#ifdef UNITTESTS + +/** Oracle: compute expected per-packet phase mask from state and flow flag. */ +static uint8_t DetectTcpSessionExpectedPhaseMask(enum TcpState state, bool flow_pkt_est) +{ + uint8_t mask = 0; + + if (flow_pkt_est) { + mask |= DETECT_TCP_SESSION_PHASE_ESTABLISHED; + } else { + mask |= DETECT_TCP_SESSION_PHASE_SETUP; + } + + switch (state) { + case TCP_FIN_WAIT1: + case TCP_FIN_WAIT2: + case TCP_TIME_WAIT: + case TCP_LAST_ACK: + case TCP_CLOSE_WAIT: + case TCP_CLOSING: + mask |= DETECT_TCP_SESSION_PHASE_CLOSING; + break; + default: + break; + } + + return mask; +} + +/** Exhaustive match test over all (state × flag × phase_flags) combinations. */ +static int DetectTcpSessionMatchExhaustive(void) +{ + /* Iterate all TcpState values (0..TCP_CLOSED). Value 1 is unused + * (TCP_LISTEN is commented out) but the match function handles it + * via the default case, so including it is harmless. */ + + for (uint8_t st = 0; st <= TCP_CLOSED; st++) { + for (int est = 0; est <= 1; est++) { + const bool flow_pkt_est = (est == 1); + + /* All 7 non-empty subsets of the 3-bit phase space. */ + for (uint8_t pf = 1; pf <= 0x7; pf++) { + ThreadVars tv; + DetectEngineThreadCtx dtx; + Signature s; + TcpSession ssn; + Flow f; + Packet *p = PacketGetFromAlloc(); + FAIL_IF_NULL(p); + memset(&tv, 0, sizeof(tv)); + memset(&dtx, 0, sizeof(dtx)); + memset(&s, 0, sizeof(s)); + memset(&ssn, 0, sizeof(ssn)); + memset(&f, 0, sizeof(f)); + f.proto = IPPROTO_TCP; + ssn.state = st; + f.protoctx = &ssn; + p->flow = &f; + p->flags |= PKT_HAS_FLOW; + if (flow_pkt_est) + p->flowflags |= FLOW_PKT_ESTABLISHED; + + DetectTcpSessionData d = { .phase_flags = pf }; + const uint8_t expected_mask = + DetectTcpSessionExpectedPhaseMask((enum TcpState)st, flow_pkt_est); + const int expected = (expected_mask & pf) ? 1 : 0; + const int actual = DetectTcpSessionMatch(&dtx, p, &s, (const SigMatchCtx *)&d); + PacketFree(p); + FAIL_IF_NOT(actual == expected); + } + } + } + + PASS; +} + +#endif /* UNITTESTS */ diff --git a/src/detect-tcp-session.h b/src/detect-tcp-session.h new file mode 100644 index 0000000000..d2150e8e4f --- /dev/null +++ b/src/detect-tcp-session.h @@ -0,0 +1,39 @@ +/* Copyright (C) 2026 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + * + * \brief tcp.session: keyword (Redmine #7704). + */ + +#ifndef SURICATA_DETECT_TCP_SESSION_H +#define SURICATA_DETECT_TCP_SESSION_H + +#include "suricata-common.h" + +#define DETECT_TCP_SESSION_PHASE_SETUP BIT_U8(0) +#define DETECT_TCP_SESSION_PHASE_ESTABLISHED BIT_U8(1) +#define DETECT_TCP_SESSION_PHASE_CLOSING BIT_U8(2) + +typedef struct DetectTcpSessionData_ { + uint8_t phase_flags; /**< OR of DETECT_TCP_SESSION_PHASE_* */ +} DetectTcpSessionData; + +void DetectTcpSessionRegister(void); + +#endif /* SURICATA_DETECT_TCP_SESSION_H */