]> git.ipfire.org Git - thirdparty/snort3.git/commitdiff
Pull request #4465: tcp_pdu: new inspector for simple length based flushing
authorRuss Combs (rucombs) <rucombs@cisco.com>
Fri, 4 Oct 2024 15:55:59 +0000 (15:55 +0000)
committerRuss Combs (rucombs) <rucombs@cisco.com>
Fri, 4 Oct 2024 15:55:59 +0000 (15:55 +0000)
Merge in SNORT/snort3 from ~RUCOMBS/snort3:tcp_pdu to master

Squashed commit of the following:

commit 58b1bc69c07c863d04c446207eb317d99ed1e7fd
Author: Russ Combs <rucombs@cisco.com>
Date:   Mon Sep 16 14:06:01 2024 -0400

    tcp_pdu: new inspector for simple length based flushing

    tcp_pdu provides a generic protocol-aware-flushing capability for PDUs
    that contain a length field. The field may be at a configurable offset
    from the start of the PDU, it has a configurable length, and may specify
    the total length of the PDU or the number of bytes following the length
    field.

src/service_inspectors/CMakeLists.txt
src/service_inspectors/service_inspectors.cc
src/service_inspectors/tcp_pdu/CMakeLists.txt [new file with mode: 0644]
src/service_inspectors/tcp_pdu/dev_notes.txt [new file with mode: 0644]
src/service_inspectors/tcp_pdu/tcp_pdu.cc [new file with mode: 0644]
src/service_inspectors/tcp_pdu/tcp_pdu.h [new file with mode: 0644]
src/service_inspectors/tcp_pdu/tcp_pdu_splitter.cc [new file with mode: 0644]
src/service_inspectors/tcp_pdu/test/CMakeLists.txt [new file with mode: 0644]
src/service_inspectors/tcp_pdu/test/tcp_pdu_test.cc [new file with mode: 0644]

index fa902c17059e0ecb2c8f7449d005f31aacad5a8d..55aafe03660b9d728ffe4f80e43f04e5b465efcf 100644 (file)
@@ -20,6 +20,7 @@ add_subdirectory(sip)
 add_subdirectory(smtp)
 add_subdirectory(ssh)
 add_subdirectory(ssl)
+add_subdirectory(tcp_pdu)
 add_subdirectory(wizard)
 
 if (STATIC_INSPECTORS)
@@ -41,6 +42,7 @@ if (STATIC_INSPECTORS)
         $<TARGET_OBJECTS:smtp>
         $<TARGET_OBJECTS:ssh>
         $<TARGET_OBJECTS:ssl>
+        $<TARGET_OBJECTS:tcp_pdu>
         $<TARGET_OBJECTS:wizard>
     )
 endif()
index 86a5b01db0394975a39aa3e410c722229fba66c4..d790acca3fd677bdf58ecbdf154a08f0be7a0dcc 100644 (file)
@@ -43,6 +43,7 @@ extern const BaseApi* sin_pop;
 extern const BaseApi* sin_rpc_decode;
 extern const BaseApi* sin_smtp;
 extern const BaseApi* sin_ssh;
+extern const BaseApi* sin_tcp_pdu;
 extern const BaseApi* sin_telnet;
 extern const BaseApi* sin_wizard;
 
@@ -71,6 +72,7 @@ const BaseApi* service_inspectors[] =
     sin_rpc_decode,
     sin_smtp,
     sin_ssh,
+    sin_tcp_pdu,
     sin_telnet,
     sin_wizard,
 #endif
diff --git a/src/service_inspectors/tcp_pdu/CMakeLists.txt b/src/service_inspectors/tcp_pdu/CMakeLists.txt
new file mode 100644 (file)
index 0000000..453b053
--- /dev/null
@@ -0,0 +1,17 @@
+
+set( FILE_LIST
+    tcp_pdu.cc
+    tcp_pdu.h
+    tcp_pdu_splitter.cc
+)
+
+if (STATIC_INSPECTORS)
+    add_library( tcp_pdu OBJECT ${FILE_LIST})
+
+else (STATIC_INSPECTORS)
+    add_dynamic_module(tcp_pdu inspectors ${FILE_LIST})
+
+endif (STATIC_INSPECTORS)
+
+add_subdirectory(test)
+
diff --git a/src/service_inspectors/tcp_pdu/dev_notes.txt b/src/service_inspectors/tcp_pdu/dev_notes.txt
new file mode 100644 (file)
index 0000000..c6a9a2e
--- /dev/null
@@ -0,0 +1,49 @@
+
+The TcpPdu splitter provides a generic TCP stream flush function to support
+IPS.  This works for PDUs that contain a length field at a fixed offset that
+can be extracted and used to set a flush point.
+
+The general format supported looks like this:
+
+<flow> = <PDU> | <PDU> | ...
+<PDU> ::= <header>[<data>]
+<header> ::= [<offset>]<length>[<skip>]
+
+Where:
+
+* [ X ] indicates that X is optional and | is the flush point.
+
+* <flow> refers to one side of a flow.
+
+* All <PDU>s in both directions have the same header structure as defined by
+  the configuration.
+
+* <length> is assumed to be in network byte order.
+
+So a PDU with a 4 byte length field in the middle of a 12 byte header would be
+configured with offset = size = skip = 4.
+
+tcp_pdu is not service specific. An appropriate wizard pattern must direct the
+paylaod to a tcp_pdu instance configured for the flow.
+
+The initial implementation supports these parameters:
+
+* int tcp_pdu.offset = 0: index to first byte of length field { 0:65535 }
+* int tcp_pdu.size = 4: number of bytes in length field { 1:4 }
+* int tcp_pdu.skip = 0: bytes after length field to end of header { 0:65535 }
+* bool tcp_pdu.relative = false: extracted length follows field (instead of whole PDU)
+
+Additional parameters that may be supported in the future if required:
+
+* int tcp_pdu.bitmask = 0xFFFFFFFF: applies as an AND to the extracted value to get length { 0x1:0xFFFFFFFF }
+* int tcp_pdu.multiplier = 1: scale extracted value by given amount after masking { 1:65535 }
+
+Still other possibilities:
+
+* bool tcp_pdu.big = false: big endian
+* bool tcp_pdu.little = false: little endian
+* bool tcp_pdu.string = false: convert from string
+* bool tcp_pdu.hex = false: convert from hex string
+* bool tcp_pdu.oct = false: convert from octal string
+* bool tcp_pdu.dec = false: convert from decimal string
+
diff --git a/src/service_inspectors/tcp_pdu/tcp_pdu.cc b/src/service_inspectors/tcp_pdu/tcp_pdu.cc
new file mode 100644 (file)
index 0000000..f70d91d
--- /dev/null
@@ -0,0 +1,196 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2024-2024 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// 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 along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// tcp_pdu.cc author Russ Combs <rucombs@cisco.com>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "framework/decode_data.h"
+#include "framework/inspector.h"
+#include "framework/module.h"
+#include "profiler/profiler.h"
+
+#include "tcp_pdu.h"
+
+using namespace snort;
+using namespace std;
+
+//-------------------------------------------------------------------------
+// common foo
+//-------------------------------------------------------------------------
+
+#define s_name "tcp_pdu"
+#define s_help "set TCP flush points based on PDU length field"
+
+static const PegInfo pdu_pegs[] =
+{
+    { CountType::SUM, "scans", "total segments scanned" },
+    { CountType::SUM, "flushes", "total PDUs flushed for detection" },
+    { CountType::SUM, "aborts", "total unrecoverable scan errors" },
+    { CountType::END, nullptr, nullptr }
+};
+
+THREAD_LOCAL PduCounts pdu_counts;
+
+static THREAD_LOCAL snort::ProfileStats pdu_prof;
+
+//-------------------------------------------------------------------------
+// module foo
+//-------------------------------------------------------------------------
+
+static const Parameter s_params[] =
+{
+    { "offset", Parameter::PT_INT, "0:65535", "0",
+      "index to first byte of length field" },
+
+    { "size", Parameter::PT_INT, "1:4", "4",
+      "number of bytes in length field" },
+
+    { "skip", Parameter::PT_INT, "0:65535", "0",
+      "bytes after length field to end of header" },
+
+    { "relative", Parameter::PT_BOOL, nullptr, "false",
+      "extracted length follows field (instead of whole PDU)" },
+
+    { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr }
+};
+
+class TcpPduModule : public snort::Module
+{
+public:
+    TcpPduModule() : Module(s_name, s_help, s_params)
+    { }
+
+    const PegInfo* get_pegs() const override
+    { return pdu_pegs; }
+
+    PegCount* get_counts() const override
+    { return (PegCount*)&pdu_counts; }
+
+    snort::ProfileStats* get_profile() const override
+    { return &pdu_prof; }
+
+    Usage get_usage() const override
+    { return INSPECT; }
+
+    bool is_bindable() const override
+    { return true; }
+
+    bool set(const char*, Value&, SnortConfig*) override;
+
+    TcpPduConfig& get_config()
+    { return config; }
+
+private:
+    TcpPduConfig config;
+};
+
+bool TcpPduModule::set(const char*, Value& v, SnortConfig*)
+{
+    if (v.is("offset"))
+        config.offset = v.get_int32();
+
+    else if (v.is("size"))
+        config.size = v.get_uint8();
+
+    else if (v.is("skip"))
+        config.skip = v.get_uint8();
+
+    else if (v.is("relative"))
+        config.relative = v.get_bool();
+
+    return true;
+}
+
+//-------------------------------------------------------------------------
+// inspector foo
+//-------------------------------------------------------------------------
+
+class TcpPdu : public Inspector
+{
+public:
+    TcpPdu(TcpPduConfig& c) : config(c) { }
+
+    StreamSplitter* get_splitter(bool c2s) override
+    { return new TcpPduSplitter(c2s, config); }
+
+private:
+    TcpPduConfig config;
+};
+
+//-------------------------------------------------------------------------
+// api foo
+//-------------------------------------------------------------------------
+
+static Module* mod_ctor()
+{ return new TcpPduModule; }
+
+static void mod_dtor(Module* m)
+{ delete m; }
+
+static Inspector* pdu_ctor(Module* m)
+{
+    TcpPduModule* tpm = (TcpPduModule*)m;
+    return new TcpPdu(tpm->get_config());
+}
+
+static void pdu_dtor(Inspector* p)
+{
+    delete p;
+}
+
+static const InspectApi pdu_api =
+{
+    {
+        PT_INSPECTOR,
+        sizeof(InspectApi),
+        INSAPI_VERSION,
+        0,
+        API_RESERVED,
+        API_OPTIONS,
+        s_name,
+        s_help,
+        mod_ctor,
+        mod_dtor
+    },
+    IT_SERVICE,
+    PROTO_BIT__PDU,
+    nullptr, // buffers
+    s_name,
+    nullptr, // init
+    nullptr, // pterm
+    nullptr, // tinit
+    nullptr, // tterm
+    pdu_ctor,
+    pdu_dtor,
+    nullptr, // ssn
+    nullptr  // reset
+};
+
+#ifdef BUILDING_SO
+SO_PUBLIC const BaseApi* snort_plugins[] =
+{
+    &pdu_api.base,
+    nullptr
+};
+#else
+const BaseApi* sin_tcp_pdu = &pdu_api.base;
+#endif
+
diff --git a/src/service_inspectors/tcp_pdu/tcp_pdu.h b/src/service_inspectors/tcp_pdu/tcp_pdu.h
new file mode 100644 (file)
index 0000000..5c32181
--- /dev/null
@@ -0,0 +1,64 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2024-2024 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// 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 along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// tcp_pdu.h author Russ Combs <rucombs@cisco.com>
+
+// provides a simple flush mechanism for TCP PDUs with
+// a fixed size header containing a length field
+
+#ifndef TCP_PDU_H
+#define TCP_PDU_H
+
+#include "framework/counts.h"
+#include "main/snort_types.h"
+#include "stream/stream_splitter.h"
+
+struct TcpPduConfig
+{
+    unsigned size = 0;
+    unsigned offset = 0;
+    unsigned skip = 0;
+    bool relative = false;
+};
+
+struct PduCounts
+{
+    PegCount scans;
+    PegCount flushes;
+    PegCount aborts;
+};
+
+extern THREAD_LOCAL PduCounts pdu_counts;
+
+class TcpPduSplitter : public snort::StreamSplitter
+{
+public:
+    TcpPduSplitter(bool b, TcpPduConfig& c) : snort::StreamSplitter(b), config(c) { }
+
+    bool is_paf() override { return true; }
+
+    Status scan(struct snort::Packet*, const uint8_t*, uint32_t, uint32_t, uint32_t*) override;
+
+private:
+    TcpPduConfig config;
+    unsigned index = 0;
+    uint32_t value = 0;
+};
+
+#endif
+
diff --git a/src/service_inspectors/tcp_pdu/tcp_pdu_splitter.cc b/src/service_inspectors/tcp_pdu/tcp_pdu_splitter.cc
new file mode 100644 (file)
index 0000000..5a3e3dc
--- /dev/null
@@ -0,0 +1,73 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2024-2024 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// 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 along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// tcp_pdu_splitter.cc author Russ Combs <rucombs@cisco.com>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "tcp_pdu.h"
+
+using namespace snort;
+
+//-------------------------------------------------------------------------
+// splitter foo
+//-------------------------------------------------------------------------
+
+StreamSplitter::Status TcpPduSplitter::scan(Packet*, const uint8_t* data, uint32_t len, uint32_t, uint32_t* fp)
+{
+    ++pdu_counts.scans;
+    unsigned prefix = config.offset + config.size;
+
+    for ( unsigned i = 0; i < len; ++i )
+    {
+        if ( index < config.offset )
+            ++index;
+
+        else if ( index < prefix )
+        {
+            ++index;
+            value <<= 8;
+            value |= data[i];
+        }
+        else
+            break;
+    }
+    if ( index == prefix )
+    {
+        unsigned header = config.offset + config.size + config.skip;
+
+        if ( config.relative )
+            value += header;
+
+        *fp = value;
+        value = 0;
+        index = 0;
+
+        if ( config.relative or (*fp >= header) )
+        {
+            ++pdu_counts.flushes;
+            return FLUSH;
+        }
+        ++pdu_counts.aborts;
+        return ABORT;
+    }
+    return SEARCH;
+}
+
diff --git a/src/service_inspectors/tcp_pdu/test/CMakeLists.txt b/src/service_inspectors/tcp_pdu/test/CMakeLists.txt
new file mode 100644 (file)
index 0000000..cea38ff
--- /dev/null
@@ -0,0 +1,7 @@
+
+add_cpputest( tcp_pdu_test
+    SOURCES
+        ../tcp_pdu_splitter.cc
+        ../../../stream/stream_splitter.cc
+)
+
diff --git a/src/service_inspectors/tcp_pdu/test/tcp_pdu_test.cc b/src/service_inspectors/tcp_pdu/test/tcp_pdu_test.cc
new file mode 100644 (file)
index 0000000..1973efc
--- /dev/null
@@ -0,0 +1,374 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2015-2024 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// 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 along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// tcp_pdu_test.cc author Russ Combs <rucombs@cisco.com>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <cstring>
+
+#include "detection/detection_engine.h"
+#include "main/snort_config.h"
+#include "stream/flush_bucket.h"
+#include "stream/stream.h"
+
+#include "../tcp_pdu.h"
+
+// must appear after snort_config.h to avoid broken c++ map include
+#include <CppUTest/CommandLineTestRunner.h>
+#include <CppUTest/TestHarness.h>
+
+using namespace snort;
+
+//-------------------------------------------------------------------------
+// stubs, spies, etc.
+//-------------------------------------------------------------------------
+
+const SnortConfig* SnortConfig::get_conf()
+{ return nullptr; }
+
+uint16_t FlushBucket::get_size()
+{ return 0; }
+
+uint8_t* DetectionEngine::get_next_buffer(unsigned int&)
+{ return nullptr; }
+
+Packet* DetectionEngine::get_current_packet()
+{ return nullptr; }
+
+StreamSplitter* Stream::get_splitter(Flow*, bool)
+{ return nullptr; }
+
+void Stream::flush_client(Packet*)
+{ }
+
+void Stream::flush_server(Packet*)
+{ }
+
+THREAD_LOCAL PduCounts pdu_counts;
+
+//-------------------------------------------------------------------------
+// 4 byte length followed by data, no offset, relative
+// check with scan sizes 1, 2, 3, 4, 5
+//-------------------------------------------------------------------------
+
+TEST_GROUP(relative_length_only)
+{
+    // 4 byte length followed by 3 bytes data
+    const uint8_t data[7] = { 0, 1, 2, 3, 4, 5, 6 };  // cppcheck-suppress unreadVariable
+    StreamSplitter* ss = nullptr;
+
+    void setup() override
+    {
+        TcpPduConfig c = { 4, 0, 0, true };
+        ss = new TcpPduSplitter(true, c);  // cppcheck-suppress unreadVariable
+    }
+    void teardown() override
+    { delete ss; }
+};
+
+TEST(relative_length_only, n1)
+{
+    uint32_t fp = 0;
+    StreamSplitter::Status result;
+
+    for ( auto i = 0; i < 3; ++i )
+    {
+        result = ss->scan(nullptr, data+i, 1, 0, &fp);
+        CHECK(result == StreamSplitter::SEARCH);
+    }
+
+    result = ss->scan(nullptr, data+3, 1, 0, &fp);
+    CHECK(result == StreamSplitter::FLUSH);
+    CHECK(fp == 4+0x10203);
+}
+
+TEST(relative_length_only, n2)
+{
+    uint32_t fp = 0;
+    StreamSplitter::Status result;
+
+    result = ss->scan(nullptr, data+0, 2, 0, &fp);
+    CHECK(result == StreamSplitter::SEARCH);
+
+    result = ss->scan(nullptr, data+2, 2, 0, &fp);
+    CHECK(result == StreamSplitter::FLUSH);
+    CHECK(fp == 4+0x10203);
+}
+
+TEST(relative_length_only, n3)
+{
+    uint32_t fp = 0;
+    StreamSplitter::Status result;
+
+    result = ss->scan(nullptr, data+0, 3, 0, &fp);
+    CHECK(result == StreamSplitter::SEARCH);
+
+    result = ss->scan(nullptr, data+3, 3, 0, &fp);
+    CHECK(result == StreamSplitter::FLUSH);
+    CHECK(fp == 4+0x10203);
+}
+
+TEST(relative_length_only, n4)
+{
+    uint32_t fp = 0;
+    StreamSplitter::Status result;
+
+    result = ss->scan(nullptr, data+0, 4, 0, &fp);
+    CHECK(result == StreamSplitter::FLUSH);
+    CHECK(fp == 4+0x10203);
+}
+
+TEST(relative_length_only, n5)
+{
+    uint32_t fp = 0;
+    StreamSplitter::Status result;
+
+    result = ss->scan(nullptr, data+0, 5, 0, &fp);
+    CHECK(result == StreamSplitter::FLUSH);
+    CHECK(fp == 4+0x10203);
+}
+
+//-------------------------------------------------------------------------
+// 3 byte offset, 4 byte length, 2 byte skip, relative
+// check with scan sizes 1, 2, 3, 4, 7, 8
+//-------------------------------------------------------------------------
+
+TEST_GROUP(relative_offset_length)
+{
+    const uint8_t data[10] = { 9, 8, 7, 0, 1, 2, 3, 4, 5, 6 };  // cppcheck-suppress unreadVariable
+    StreamSplitter* ss = nullptr;
+
+    void setup() override
+    {
+        TcpPduConfig c = { 4, 3, 2, true };
+        ss = new TcpPduSplitter(true, c);  // cppcheck-suppress unreadVariable
+    }
+    void teardown() override
+    { delete ss; }
+};
+
+TEST(relative_offset_length, n1)
+{
+    uint32_t fp = 0;
+    StreamSplitter::Status result;
+
+    for ( auto i = 0; i < 6; ++i )
+    {
+        result = ss->scan(nullptr, data+i, 1, 0, &fp);
+        CHECK(result == StreamSplitter::SEARCH);
+    }
+
+    result = ss->scan(nullptr, data+6, 1, 0, &fp);
+    CHECK(result == StreamSplitter::FLUSH);
+    CHECK(fp == 3+4+2+0x10203);
+}
+
+TEST(relative_offset_length, n2)
+{
+    uint32_t fp = 0;
+    StreamSplitter::Status result;
+
+    for ( auto i = 0; i < 6; i+=2 )
+    {
+        result = ss->scan(nullptr, data+i, 2, 0, &fp);
+        CHECK(result == StreamSplitter::SEARCH);
+    }
+
+    result = ss->scan(nullptr, data+6, 2, 0, &fp);
+    CHECK(result == StreamSplitter::FLUSH);
+    CHECK(fp == 3+4+2+0x10203);
+}
+
+TEST(relative_offset_length, n3)
+{
+    uint32_t fp = 0;
+    StreamSplitter::Status result;
+
+    result = ss->scan(nullptr, data+0, 3, 0, &fp);
+    CHECK(result == StreamSplitter::SEARCH);
+
+    result = ss->scan(nullptr, data+3, 3, 0, &fp);
+    CHECK(result == StreamSplitter::SEARCH);
+
+    result = ss->scan(nullptr, data+6, 3, 0, &fp);
+    CHECK(result == StreamSplitter::FLUSH);
+    CHECK(fp == 3+4+2+0x10203);
+}
+
+TEST(relative_offset_length, n4)
+{
+    uint32_t fp = 0;
+    StreamSplitter::Status result;
+
+    result = ss->scan(nullptr, data+0, 4, 0, &fp);
+    CHECK(result == StreamSplitter::SEARCH);
+
+    result = ss->scan(nullptr, data+4, 4, 0, &fp);
+    CHECK(result == StreamSplitter::FLUSH);
+    CHECK(fp == 3+4+2+0x10203);
+}
+
+TEST(relative_offset_length, n7)
+{
+    uint32_t fp = 0;
+    StreamSplitter::Status result;
+
+    result = ss->scan(nullptr, data+0, 7, 0, &fp);
+    CHECK(result == StreamSplitter::FLUSH);
+    CHECK(fp == 3+4+2+0x10203);
+}
+
+TEST(relative_offset_length, n8)
+{
+    uint32_t fp = 0;
+    StreamSplitter::Status result;
+
+    result = ss->scan(nullptr, data+0, 8, 0, &fp);
+    CHECK(result == StreamSplitter::FLUSH);
+    CHECK(fp == 3+4+2+0x10203);
+}
+
+//-------------------------------------------------------------------------
+// various
+//-------------------------------------------------------------------------
+
+TEST_GROUP(various)
+{
+    const uint8_t data[8] = { 9, 8, 0, 1, 2, 3, 4, 5 };  // cppcheck-suppress unreadVariable
+    StreamSplitter* ss = nullptr;
+
+    void teardown() override
+    { delete ss; }
+};
+
+TEST(various, absolute2)
+{
+    TcpPduConfig c = { 2, 3, 0, false };
+    ss = new TcpPduSplitter(true, c);
+
+    uint32_t fp = 0;
+    StreamSplitter::Status result;
+
+    result = ss->scan(nullptr, data+0, 3, 0, &fp);
+    CHECK(result == StreamSplitter::SEARCH);
+
+    result = ss->scan(nullptr, data+3, 3, 0, &fp);
+    CHECK(result == StreamSplitter::FLUSH);
+    CHECK(fp == 0x102);
+}
+
+TEST(various, absolute3)
+{
+    TcpPduConfig c = { 3, 2, 0, false };
+    ss = new TcpPduSplitter(true, c);
+
+    uint32_t fp = 0;
+    StreamSplitter::Status result;
+
+    result = ss->scan(nullptr, data+0, 3, 0, &fp);
+    CHECK(result == StreamSplitter::SEARCH);
+
+    result = ss->scan(nullptr, data+3, 3, 0, &fp);
+    CHECK(result == StreamSplitter::FLUSH);
+    CHECK(fp == 0x102);
+}
+
+TEST(various, abort)
+{
+    TcpPduConfig c = { 1, 2, 0, false };
+    ss = new TcpPduSplitter(true, c);
+
+    uint32_t fp = 0;
+    StreamSplitter::Status result;
+
+    result = ss->scan(nullptr, data+0, 3, 0, &fp);
+    CHECK(result == StreamSplitter::ABORT);
+}
+
+TEST(various, header_only)
+{
+    TcpPduConfig c = { 1, 2, 0, true };
+    ss = new TcpPduSplitter(true, c);
+
+    uint32_t fp = 0;
+    StreamSplitter::Status result;
+
+    result = ss->scan(nullptr, data+0, 3, 0, &fp);
+    CHECK(result == StreamSplitter::FLUSH);
+    CHECK(fp == 2+1+0);
+}
+
+//-------------------------------------------------------------------------
+// multiple PDUs flushed on same flow / direction
+//-------------------------------------------------------------------------
+
+TEST_GROUP(multi_flush)
+{
+// __STRDUMP_DISABLE__
+    // 2 byte offset ('O'), 1 byte length, data ('D')
+    // cppcheck-suppress unreadVariable
+    const uint8_t data[17] = { 'O', 'O', 3, 'D', 'D', 'D', 'O', 'O', 1, 'D', 'O', 'O', 4, 'D', 'D', 'D', 'D' };
+// __STRDUMP_ENABLE__
+    StreamSplitter* ss = nullptr;
+
+    void setup() override
+    {
+        TcpPduConfig c = { 1, 2, 0, true };
+        ss = new TcpPduSplitter(true, c);  // cppcheck-suppress unreadVariable
+    }
+    void teardown() override
+    { delete ss; }
+};
+
+TEST(multi_flush, pdu3)
+{
+    uint32_t fp = 0;
+    StreamSplitter::Status result;
+
+    // PDU 1
+    result = ss->scan(nullptr, data+0, 3, 0, &fp);
+    CHECK(result == StreamSplitter::FLUSH);
+    CHECK(fp == 6);
+
+    // PDU 2
+    result = ss->scan(nullptr, data+6, 4, 0, &fp);
+    CHECK(result == StreamSplitter::FLUSH);
+    CHECK(fp == 4);
+
+    // PDU 3
+    result = ss->scan(nullptr, data+10, 1, 0, &fp);
+    CHECK(result == StreamSplitter::SEARCH);
+
+    result = ss->scan(nullptr, data+11, 3, 0, &fp);
+    CHECK(result == StreamSplitter::FLUSH);
+    CHECK(fp == 7);
+}
+
+//-------------------------------------------------------------------------
+// main
+//-------------------------------------------------------------------------
+
+int main(int argc, char** argv)
+{
+    MemoryLeakWarningPlugin::turnOffNewDeleteOverloads();
+    return CommandLineTestRunner::RunAllTests(argc, argv);
+}
+