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 398687d5d4)
pull/16202/head
Yash Datre 2 months ago committed by Victor Julien
parent 353885acc2
commit b800ace56f

@ -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:<phase>[,<phase>...];
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

@ -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 \

@ -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";

@ -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();

@ -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,

@ -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 */

@ -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 */
Loading…
Cancel
Save