]> git.ipfire.org Git - thirdparty/snort3.git/commitdiff
Pull request #4991: extractor: add quic extractor
authorVolodymyr Shpyrka -X (vshpyrka - SOFTSERVE INC at Cisco) <vshpyrka@cisco.com>
Tue, 18 Nov 2025 07:34:20 +0000 (07:34 +0000)
committerOleksii Shumeiko -X (oshumeik - SOFTSERVE INC at Cisco) <oshumeik@cisco.com>
Tue, 18 Nov 2025 07:34:20 +0000 (07:34 +0000)
Merge in SNORT/snort3 from ~VSHPYRKA/snort3:quic_extractor_implementation to master

Squashed commit of the following:

commit f0020563a960fdf16c09af4454b7c80cd4073da7
Author: Volodymyr Shpyrka -X (vshpyrka - SOFTSERVE INC at Cisco) <vshpyrka@cisco.com>
Date:   Thu Oct 16 12:02:12 2025 +0300

    extractor: add quic extractor

doc/user/extractor.txt
src/network_inspectors/extractor/CMakeLists.txt
src/network_inspectors/extractor/extractor.cc
src/network_inspectors/extractor/extractor_enums.h
src/network_inspectors/extractor/extractor_quic.cc [new file with mode: 0644]
src/network_inspectors/extractor/extractor_quic.h [new file with mode: 0644]
src/network_inspectors/extractor/extractor_service.cc
src/network_inspectors/extractor/extractor_service.h

index 7c1b73904bae57f7c83ee426480e6f85f1c7023b..4fa4d59bfddba6c657aed763a32f615bcf428b11 100644 (file)
@@ -70,6 +70,8 @@ Services and their events:
 * triggered IPS rule, whether built-in or text or SO (notice)
   ** `ips_logging` (matched rules sent to IPS logging)
   ** `context_logging` (matched rule in an IPS logger)
+* QUIC
+  ** `handshake` (log on handshake completion)
 
 Common fields available for every service:
 
@@ -158,6 +160,16 @@ contains only the name of the RR type. This is also the default decoding applied
 a type specific decoder. When the name of the type is not known it is decoded as UNKNOWN-N, where N is RR type
 numeric value.
 
+Fields supported for QUIC:
+
+* `version` - QUIC version
+* `client_initial_dcid` - client initial destination connection ID
+* `client_scid` - client source connection ID
+* `server_scid` - server source connection ID
+* `server_name` - server name indication (SNI) from client hello
+* `client_protocol` - application protocol requested by client
+* `history` - connection history string
+
 Fields supported for connection:
 
 * `duration` - connection duration in seconds
index 51e4835a5eb5000c3d0e11fefc43251a768f013d..911e60a046b273f256f638adeb6200ab8c5cd430 100644 (file)
@@ -25,6 +25,8 @@ set( FILE_LIST
     extractor_logger.cc
     extractor_logger.h
     extractor_null_conn.h
+    extractor_quic.cc
+    extractor_quic.h
     extractor_service.cc
     extractor_service.h
     extractor_ssl.cc
index adde8964a8cbddc8fb7ce76644bac5b3d80f4080..608a6954b33e6f18b9f8bcd98d97ad28d97695bb 100644 (file)
@@ -50,7 +50,7 @@ THREAD_LOCAL ExtractorLogger* Extractor::logger = nullptr;
 
 static const Parameter extractor_proto_params[] =
 {
-    { "service", Parameter::PT_ENUM, "http | ftp | ssl | conn | dns | weird | notice", nullptr,
+    { "service", Parameter::PT_ENUM, "http | ftp | ssl | conn | dns | quic | weird | notice", nullptr,
       "service to extract from" },
 
     { "tenant_id", Parameter::PT_INT, "0:max32", "0",
index 3e586b65285ebcf6fe55edc3680f0e7eed8b46a1..ede15c936b112848850adcbdd71b36f8c6d4e050 100644 (file)
@@ -32,6 +32,7 @@ public:
         SSL,
         CONN,
         DNS,
+        QUIC,
         IPS_BUILTIN,
         IPS_USER,
         ANY,
@@ -59,6 +60,8 @@ public:
             return "conn";
         case DNS:
             return "dns";
+        case QUIC:
+            return "quic";
         case IPS_BUILTIN:
             return "weird";
         case IPS_USER:
diff --git a/src/network_inspectors/extractor/extractor_quic.cc b/src/network_inspectors/extractor/extractor_quic.cc
new file mode 100644 (file)
index 0000000..50629ff
--- /dev/null
@@ -0,0 +1,274 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2025-2025 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.
+//--------------------------------------------------------------------------
+// extractor_quic.cc author Volodymyr Shpyrka <vshpyrka@cisco.com>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "extractor_quic.h"
+
+#include <sys/time.h>
+
+#include "detection/detection_engine.h"
+#include "flow/flow_key.h"
+#include "log/messages.h"
+#include "profiler/profiler.h"
+#include "pub_sub/quic_events.h"
+#include "utils/util.h"
+#include "utils/util_net.h"
+
+#include "extractor.h"
+#include "extractor_enums.h"
+#include "extractor_flow_data.h"
+
+using namespace snort;
+using namespace std;
+
+class QuicExtractorFlowData : public ExtractorFlowData
+{
+public:
+    static constexpr ServiceType type_id = ServiceType::QUIC;
+
+    QuicExtractorFlowData(QuicExtractor& owner)
+        : ExtractorFlowData(type_id, owner.get_inspector()), owner(owner) {}
+    
+    ~QuicExtractorFlowData() override
+    {
+        if (has_data)
+            owner.dump(*this);
+    }
+
+    void reset()
+    {
+        version.clear();
+        client_initial_dcid.clear();
+        client_scid.clear();
+        server_name.clear();
+        client_protocol.clear();
+        server_scid.clear();
+        history.clear();
+        ts = {};
+        has_data = false;
+    }
+
+    std::string version;
+    std::string client_initial_dcid;
+    std::string client_scid;
+    std::string server_name;
+    std::string client_protocol;
+
+    std::string server_scid;
+    std::string history;
+
+    struct timeval ts = {};
+
+    bool has_data = false;
+
+private:
+    QuicExtractor& owner;
+};
+
+namespace flow
+{
+static const char* get_version(const QuicExtractorFlowData& fd)
+{
+    return fd.version.c_str();
+}
+
+static const char* get_client_initial_dcid(const QuicExtractorFlowData& fd)
+{
+    return fd.client_initial_dcid.c_str();
+}
+
+static const char* get_client_scid(const QuicExtractorFlowData& fd)
+{
+    return fd.client_scid.c_str();
+}
+
+static const char* get_server_name(const QuicExtractorFlowData& fd)
+{
+    return fd.server_name.c_str();
+}
+
+static const char* get_client_protocol(const QuicExtractorFlowData& fd)
+{
+    return fd.client_protocol.c_str();
+}
+
+static const char* get_server_scid(const QuicExtractorFlowData& fd)
+{
+    return fd.server_scid.c_str();
+}
+
+static const char* get_history(const QuicExtractorFlowData& fd)
+{
+    return fd.history.c_str();
+}
+
+static const map<string, QuicExtractor::FdStrGetFn> fd_str_getters =
+{
+    {"version", get_version},
+    {"client_initial_dcid", get_client_initial_dcid},
+    {"client_scid", get_client_scid},
+    {"server_name", get_server_name},
+    {"client_protocol", get_client_protocol},
+    {"server_scid", get_server_scid},
+    {"history", get_history}
+};
+}
+
+THREAD_LOCAL const snort::Connector::ID* QuicExtractor::log_id = nullptr;
+
+void QuicExtractor::internal_tinit(const snort::Connector::ID* service_id)
+{ log_id = service_id; }
+
+QuicExtractor::QuicExtractor(Extractor& extractor, uint32_t tenant, const std::vector<std::string>& fields)
+    : ExtractorEvent(ServiceType::QUIC, extractor, tenant)
+{
+    for (const auto& f : fields)
+    {
+        if (append(nts_fields, nts_getters, f))
+            continue;
+        if (append(sip_fields, sip_getters, f))
+            continue;
+        if (append(num_fields, num_getters, f))
+            continue;
+        if (append(fd_str_fields, flow::fd_str_getters, f))
+            continue;
+    }
+
+    DataBus::subscribe_global(quic_logging_pub_key, QuicLoggingEventIds::QUIC_CLIENT_HELLO_EVENT,
+        new ClientHello(*this, S_NAME), extractor.get_snort_config());
+    DataBus::subscribe_global(quic_logging_pub_key, QuicLoggingEventIds::QUIC_HANDSHAKE_COMPLETE_EVENT,
+        new HandshakeComplete(*this, S_NAME), extractor.get_snort_config());
+}
+
+std::vector<const char*> QuicExtractor::get_field_names() const
+{
+    vector<const char*> res = ExtractorEvent::get_field_names();
+
+    for (auto& f : fd_str_fields)
+        res.push_back(f.name);
+
+    return res;
+}
+
+template<>
+// Passing QuicExtractorFlowData as a pointer.
+// Unfortunately, template expansion is confused if we pass an object (a reference).
+void ExtractorEvent::log<vector<QuicExtractor::FdStrField>, const QuicExtractorFlowData*>(
+    const vector<QuicExtractor::FdStrField>& fields, const QuicExtractorFlowData* fd, bool strict)
+{
+    for (const auto& f : fields)
+    {
+        auto d = f.get(*fd);
+        if (d && std::strlen(d) > 0)
+            logger->add_field(f.name, d);
+        else if (strict)
+            logger->add_field(f.name, "");
+    }
+}
+
+void QuicExtractor::dump(const QuicExtractorFlowData& fd)
+{
+    Profile profile(extractor_perf_stats);
+
+    logger->open_record();
+
+    for (const auto& f : nts_fields)
+        logger->add_field(f.name, fd.ts);
+    for (const auto& f : sip_fields)
+        logger->add_field(f.name, "");
+    for (const auto& f : num_fields)
+        logger->add_field(f.name, (uint64_t)0);
+
+    log(fd_str_fields, &fd, logger->is_strict());
+    
+    logger->close_record(*log_id);
+}
+
+void QuicExtractor::ClientHello::handle(DataEvent& event, Flow* flow)
+{
+    Profile profile(extractor_perf_stats);
+
+    if (!owner.filter(flow))
+        return;
+
+    extractor_stats.total_events++;
+    auto fd = ExtractorFlowData::get<QuicExtractorFlowData>(flow);
+
+    if (!fd)
+        flow->set_flow_data(fd = new QuicExtractorFlowData(owner));
+    else if (fd->has_data)
+    {
+        // log existing flow data
+        owner.logger->open_record();
+        owner.log(owner.nts_fields, &event, flow);
+        owner.log(owner.sip_fields, &event, flow);
+        owner.log(owner.num_fields, &event, flow);
+        owner.log(owner.fd_str_fields, (const QuicExtractorFlowData*)fd, owner.logger->is_strict());
+        owner.logger->close_record(*log_id);
+
+        fd->reset();
+    }
+
+    const auto& quic_event = static_cast<const QuicClientHelloEvent&>(event);
+
+    fd->version = quic_event.get_version();
+    fd->client_initial_dcid = quic_event.get_client_initial_dcid();
+    fd->client_scid = quic_event.get_client_scid();
+    fd->server_name = quic_event.get_server_name();
+    fd->client_protocol = quic_event.get_client_protocol();
+
+    const Packet* packet = ExtractorEvent::get_packet();
+    if (packet)
+        fd->ts = packet->pkth->ts;
+    else
+        snort::packet_gettimeofday(&fd->ts);
+
+    fd->has_data = true;
+}
+
+void QuicExtractor::HandshakeComplete::handle(DataEvent& event, Flow* flow)
+{
+    Profile profile(extractor_perf_stats);
+
+    if (!owner.filter(flow))
+        return;
+
+    extractor_stats.total_events++;
+    auto fd = ExtractorFlowData::get<QuicExtractorFlowData>(flow);
+
+    if (!fd)
+        flow->set_flow_data(fd = new QuicExtractorFlowData(owner));
+
+    const auto& quic_event = static_cast<const QuicHandshakeCompleteEvent&>(event);
+
+    fd->server_scid = quic_event.get_server_scid();
+    fd->history = quic_event.get_history();
+
+    owner.logger->open_record();
+    owner.log(owner.nts_fields, &event, flow);
+    owner.log(owner.sip_fields, &event, flow);
+    owner.log(owner.num_fields, &event, flow);
+    owner.log(owner.fd_str_fields, (const QuicExtractorFlowData*)fd, owner.logger->is_strict());
+    owner.logger->close_record(*log_id);
+
+    fd->reset();
+}
diff --git a/src/network_inspectors/extractor/extractor_quic.h b/src/network_inspectors/extractor/extractor_quic.h
new file mode 100644 (file)
index 0000000..acfafeb
--- /dev/null
@@ -0,0 +1,62 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2025-2025 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.
+//--------------------------------------------------------------------------
+// extractor_quic.h author Volodymyr Shpyrka <vshpyrka@cisco.com>
+
+#ifndef EXTRACTOR_QUIC_H
+#define EXTRACTOR_QUIC_H
+
+#include "extractors.h"
+
+class QuicExtractorFlowData;
+
+class QuicExtractor : public ExtractorEvent
+{
+public:
+    using FdStrGetFn = const char* (*) (const QuicExtractorFlowData&);
+    using FdStrField = DataField<const char*, const QuicExtractorFlowData&>;
+
+    QuicExtractor(Extractor&, uint32_t tenant, const std::vector<std::string>& fields);
+
+    std::vector<const char*> get_field_names() const override;
+    void dump(const QuicExtractorFlowData&);
+
+private:
+    struct ClientHello : public DataHandler
+    {
+        ClientHello(QuicExtractor& owner, const char* name)
+            : DataHandler(name), owner(owner) {}
+        void handle(DataEvent&, Flow*) override;
+        QuicExtractor& owner;
+    };
+
+    struct HandshakeComplete : public DataHandler
+    {
+        HandshakeComplete(QuicExtractor& owner, const char* name)
+            : DataHandler(name), owner(owner) {}
+        void handle(DataEvent&, Flow*) override;
+        QuicExtractor& owner;
+    };
+
+    void internal_tinit(const snort::Connector::ID*) override;
+
+    std::vector<FdStrField> fd_str_fields;
+
+    static THREAD_LOCAL const snort::Connector::ID* log_id;
+};
+
+#endif
index 2b4495792b111ea28cd67b78aeae765f3b1d6b94..f7759e67fe3713ea06c2f0faa55d8b3cc4c4123c 100644 (file)
@@ -31,6 +31,7 @@
 #include "extractor_dns.h"
 #include "extractor_ftp.h"
 #include "extractor_http.h"
+#include "extractor_quic.h"
 #include "extractor_ssl.h"
 
 using namespace snort;
@@ -135,6 +136,10 @@ ExtractorService* ExtractorService::make_service(Extractor& ins, const ServiceCo
         srv = new DnsExtractorService(cfg.tenant_id, cfg.fields, cfg.on_events, cfg.service, ins);
         break;
 
+    case ServiceType::QUIC:
+        srv = new QuicExtractorService(cfg.tenant_id, cfg.fields, cfg.on_events, cfg.service, ins);
+        break;
+
     case ServiceType::IPS_BUILTIN:
         srv = new BuiltinExtractorService(cfg.tenant_id, cfg.fields, cfg.on_events, cfg.service, ins);
         break;
@@ -245,6 +250,11 @@ void ExtractorService::validate(const ServiceConfig& cfg)
         validate_fields(DnsExtractorService::blueprint, cfg.fields);
         break;
 
+    case ServiceType::QUIC:
+        validate_events(QuicExtractorService::blueprint, cfg.on_events);
+        validate_fields(QuicExtractorService::blueprint, cfg.fields);
+        break;
+
     case ServiceType::IPS_BUILTIN:
         validate_fields(BuiltinExtractorService::blueprint, cfg.fields);
         validate_events(BuiltinExtractorService::blueprint, cfg.on_events);
@@ -502,6 +512,47 @@ const snort::Connector::ID& DnsExtractorService::internal_tinit()
 const snort::Connector::ID& DnsExtractorService::get_log_id()
 { return log_id; }
 
+//-------------------------------------------------------------------------
+//  QuicExtractorService
+//-------------------------------------------------------------------------
+
+const ServiceBlueprint QuicExtractorService::blueprint =
+{
+    // events
+    {
+        "handshake",
+    },
+    // fields
+    {
+        "version",
+        "client_initial_dcid",
+        "client_scid",
+        "server_name",
+        "client_protocol",
+        "server_scid",
+        "history"
+    },
+};
+
+THREAD_LOCAL Connector::ID QuicExtractorService::log_id;
+
+QuicExtractorService::QuicExtractorService(uint32_t tenant, const std::vector<std::string>& srv_fields,
+    const std::vector<std::string>& srv_events, ServiceType s_type, Extractor& ins)
+    : ExtractorService(tenant, srv_fields, srv_events, blueprint, s_type, ins)
+{
+    for (const auto& event : get_events())
+    {
+        if (!strcmp("handshake", event.c_str()))
+            handlers.push_back(new QuicExtractor(ins, tenant_id, get_fields()));
+    }
+}
+
+const snort::Connector::ID& QuicExtractorService::internal_tinit()
+{ return log_id = logger->get_id(type.c_str()); }
+
+const snort::Connector::ID& QuicExtractorService::get_log_id()
+{ return log_id; }
+
 //-------------------------------------------------------------------------
 //  IpsUserExtractorService
 //-------------------------------------------------------------------------
@@ -604,6 +655,7 @@ TEST_CASE("Service Type", "[extractor]")
         ServiceType ssl = ServiceType::SSL;
         ServiceType conn = ServiceType::CONN;
         ServiceType dns = ServiceType::DNS;
+        ServiceType quic = ServiceType::QUIC;
         ServiceType weird = ServiceType::IPS_BUILTIN;
         ServiceType notice = ServiceType::IPS_USER;
         ServiceType any = ServiceType::ANY;
@@ -614,6 +666,7 @@ TEST_CASE("Service Type", "[extractor]")
         CHECK_FALSE(strcmp("ssl", ssl.c_str()));
         CHECK_FALSE(strcmp("conn", conn.c_str()));
         CHECK_FALSE(strcmp("dns", dns.c_str()));
+        CHECK_FALSE(strcmp("quic", quic.c_str()));
         CHECK_FALSE(strcmp("weird", weird.c_str()));
         CHECK_FALSE(strcmp("notice", notice.c_str()));
         CHECK_FALSE(strcmp("(not set)", any.c_str()));
index de9d046eec57ca7286aba6f644f907f3ea1501e7..52deff845ae96dfe2b04cd5db48ba5ef63445903 100644 (file)
@@ -167,6 +167,20 @@ private:
     static THREAD_LOCAL snort::Connector::ID log_id;
 };
 
+class QuicExtractorService : public ExtractorService
+{
+public:
+    static const ServiceBlueprint blueprint;
+    QuicExtractorService(uint32_t tenant, const std::vector<std::string>& fields,
+        const std::vector<std::string>& events, ServiceType, Extractor&);
+
+private:
+    const snort::Connector::ID& internal_tinit() override;
+    const snort::Connector::ID& get_log_id() override;
+
+    static THREAD_LOCAL snort::Connector::ID log_id;
+};
+
 class BuiltinExtractorService : public ExtractorService
 {
 public: