From: Volodymyr Shpyrka -X (vshpyrka - SOFTSERVE INC at Cisco) Date: Tue, 18 Nov 2025 07:34:20 +0000 (+0000) Subject: Pull request #4991: extractor: add quic extractor X-Git-Tag: 3.10.0.0~9 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=a0512e1ab159a74303af272a2b9c054dd44b8f36;p=thirdparty%2Fsnort3.git Pull request #4991: extractor: add quic extractor 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) Date: Thu Oct 16 12:02:12 2025 +0300 extractor: add quic extractor --- diff --git a/doc/user/extractor.txt b/doc/user/extractor.txt index 7c1b73904..4fa4d59bf 100644 --- a/doc/user/extractor.txt +++ b/doc/user/extractor.txt @@ -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 diff --git a/src/network_inspectors/extractor/CMakeLists.txt b/src/network_inspectors/extractor/CMakeLists.txt index 51e4835a5..911e60a04 100644 --- a/src/network_inspectors/extractor/CMakeLists.txt +++ b/src/network_inspectors/extractor/CMakeLists.txt @@ -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 diff --git a/src/network_inspectors/extractor/extractor.cc b/src/network_inspectors/extractor/extractor.cc index adde8964a..608a6954b 100644 --- a/src/network_inspectors/extractor/extractor.cc +++ b/src/network_inspectors/extractor/extractor.cc @@ -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", diff --git a/src/network_inspectors/extractor/extractor_enums.h b/src/network_inspectors/extractor/extractor_enums.h index 3e586b652..ede15c936 100644 --- a/src/network_inspectors/extractor/extractor_enums.h +++ b/src/network_inspectors/extractor/extractor_enums.h @@ -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 index 000000000..50629fffc --- /dev/null +++ b/src/network_inspectors/extractor/extractor_quic.cc @@ -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 + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "extractor_quic.h" + +#include + +#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 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& 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 QuicExtractor::get_field_names() const +{ + vector 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, const QuicExtractorFlowData*>( + const vector& 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(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(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(flow); + + if (!fd) + flow->set_flow_data(fd = new QuicExtractorFlowData(owner)); + + const auto& quic_event = static_cast(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 index 000000000..acfafeb58 --- /dev/null +++ b/src/network_inspectors/extractor/extractor_quic.h @@ -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 + +#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; + + QuicExtractor(Extractor&, uint32_t tenant, const std::vector& fields); + + std::vector 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 fd_str_fields; + + static THREAD_LOCAL const snort::Connector::ID* log_id; +}; + +#endif diff --git a/src/network_inspectors/extractor/extractor_service.cc b/src/network_inspectors/extractor/extractor_service.cc index 2b4495792..f7759e67f 100644 --- a/src/network_inspectors/extractor/extractor_service.cc +++ b/src/network_inspectors/extractor/extractor_service.cc @@ -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& srv_fields, + const std::vector& 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())); diff --git a/src/network_inspectors/extractor/extractor_service.h b/src/network_inspectors/extractor/extractor_service.h index de9d046ee..52deff845 100644 --- a/src/network_inspectors/extractor/extractor_service.h +++ b/src/network_inspectors/extractor/extractor_service.h @@ -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& fields, + const std::vector& 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: