From: Adrian Mamolea (admamole) Date: Fri, 7 Mar 2025 18:57:44 +0000 (+0000) Subject: Pull request #4634: Extractor dns X-Git-Tag: 3.7.1.0~5 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=338455ed3302c95e9c19ad5fea72eac5ae182c29;p=thirdparty%2Fsnort3.git Pull request #4634: Extractor dns Merge in SNORT/snort3 from ~ADMAMOLE/snort3:extractor_dns to master Squashed commit of the following: commit eff76203471fb2129af3d0e1ecd04b6b946f88a6 Author: Adrian Mamolea Date: Fri Feb 14 12:28:13 2025 -0500 extractor: dns support --- diff --git a/doc/user/extractor.txt b/doc/user/extractor.txt index 8da1ad8ed..7a0143e98 100644 --- a/doc/user/extractor.txt +++ b/doc/user/extractor.txt @@ -32,7 +32,8 @@ things it allows tenants to get independent data logging configurations. { service = 'http', tenant_id = 1, on_events = 'eot', fields = 'ts, uri, host, method' }, { service = 'ftp', tenant_id = 1, on_events = 'request', fields = 'ts, command, arg' }, { service = 'http', tenant_id = 2, on_events = 'eot', fields = 'ts, uri' }, - { service = 'conn', tenant_id = 1, on_events = 'eof', fields = 'ts, uid, service' } + { service = 'conn', tenant_id = 1, on_events = 'eof', fields = 'ts, uid, service' }, + { service = 'dns', tenant_id = 1, on_events = 'response', fields = 'ts, uid, query, answers' } } } @@ -46,6 +47,8 @@ Services and their events: ** request ** response ** eot (a session defined by the following commands: APPE, DELE, RETR, STOR, STOU, ACCT, PORT, PASV, EPRT, EPSV) +* DNS + ** response * connection (conn) ** eof (end of flow) @@ -94,6 +97,25 @@ Fields supported for FTP: * `data_channel.resp_h` - IP address of data channel receiving point * `data_channel.resp_p` - TCP port of data channel receiving point +Fields supported for DNS: + +* `proto` - transport protocol for DNS connection +* `trans_id` - A 16 bit identifier assigned by the program that generates the query +* `query` - The domain name that is the subject of this DNS transaction +* `qclass` - A 16 bit integer that specifies the class of the query +* `qclass_name` - A descriptive name for the class of the query +* `qtype` - A 16 bit integer that specifies the type of the query +* `qtype_name` - A descriptive name for the type of the query +* `rcode` - A 16 bit integer that specifies the response code to the query +* `rcode_name` - A descriptive name for the response code to the query +* `AA` - A boolean, true when this is an Authoritative Answer to the query +* `TC` - A boolean, true when the message was truncated due to UDP PDU size limits +* `RD` - A boolean, true when the client asks the server to pursue the query recursively +* `RA` - A boolean, denotes the availability of recursive query support at the server +* `Z` - A 3 bit integer set to 0 unless DNSSEC is used (see RFC 2535) +* `answers` - The list of answers to the query, only A and AAAA types are currently supported +* `rejected` - A boolean, true when the server responds with an error code and no query + 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 23316a73b..756cceffb 100644 --- a/src/network_inspectors/extractor/CMakeLists.txt +++ b/src/network_inspectors/extractor/CMakeLists.txt @@ -12,6 +12,7 @@ set( FILE_LIST extractor_conn.cc extractor_csv_logger.cc extractor_csv_logger.h + extractor_dns.cc extractor_enums.h extractor_flow_data.cc extractor_flow_data.h diff --git a/src/network_inspectors/extractor/extractor.cc b/src/network_inspectors/extractor/extractor.cc index 6b168a865..9e11eeb0b 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 | conn", nullptr, + { "service", Parameter::PT_ENUM, "http | ftp | conn | dns", nullptr, "service to extract from" }, { "tenant_id", Parameter::PT_INT, "0:max32", "0", diff --git a/src/network_inspectors/extractor/extractor_dns.cc b/src/network_inspectors/extractor/extractor_dns.cc new file mode 100644 index 000000000..7176e566f --- /dev/null +++ b/src/network_inspectors/extractor/extractor_dns.cc @@ -0,0 +1,221 @@ +//-------------------------------------------------------------------------- +// 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_dns.cc author Cisco + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "extractor_dns.h" + +#include "flow/flow_key.h" +#include "profiler/profiler.h" +#include "pub_sub/dns_events.h" + +#include "extractor.h" +#include "extractor_enums.h" + +using namespace snort; +using namespace std; + +static uint64_t get_trans_id(const DataEvent* event, const Flow*) +{ + return ((const DnsResponseEvent*)event)->get_trans_id(); +} + +static uint64_t get_qclass(const DataEvent* event, const Flow*) +{ + return ((const DnsResponseEvent*)event)->get_query_class(); +} + +static uint64_t get_qtype(const DataEvent* event, const Flow*) +{ + return ((const DnsResponseEvent*)event)->get_query_type(); +} + +static uint64_t get_rcode(const DataEvent* event, const Flow*) +{ + return ((const DnsResponseEvent*)event)->get_rcode(); +} + +static uint64_t get_Z(const DataEvent* event, const Flow*) +{ + return ((const DnsResponseEvent*)event)->get_Z(); +} + +static const map sub_num_getters = +{ + {"trans_id", get_trans_id}, + {"qclass", get_qclass}, + {"qtype", get_qtype}, + {"rcode", get_rcode}, + {"Z", get_Z} +}; + +static const map pkttype_to_protocol = +{ + {PktType::TCP, "TCP"}, + {PktType::UDP, "UDP"} +}; + +static const char* get_proto(const DataEvent*, const Flow* f) +{ + const auto& iter = pkttype_to_protocol.find(f->pkt_type); + return (iter != pkttype_to_protocol.end()) ? iter->second.c_str() : ""; +} + +static const char* get_query(const DataEvent* event, const Flow*) +{ + return ((const DnsResponseEvent*)event)->get_query().c_str(); +} + +static const char* get_qclass_name(const DataEvent* event, const Flow*) +{ + return ((const DnsResponseEvent*)event)->get_query_class_name().c_str(); +} + +static const char* get_qtype_name(const DataEvent* event, const Flow*) +{ + return ((const DnsResponseEvent*)event)->get_query_type_name().c_str(); +} + +static const char* get_rcode_name(const DataEvent* event, const Flow*) +{ + return ((const DnsResponseEvent*)event)->get_rcode_name().c_str(); +} + +static const char* get_AA(const DataEvent* event, const Flow*) +{ + return ((const DnsResponseEvent*)event)->get_AA() ? "T" : "F"; +} + +static const char* get_TC(const DataEvent* event, const Flow*) +{ + return ((const DnsResponseEvent*)event)->get_TC() ? "T" : "F"; +} + +static const char* get_RD(const DataEvent* event, const Flow*) +{ + return ((const DnsResponseEvent*)event)->get_RD() ? "T" : "F"; +} + +static const char* get_RA(const DataEvent* event, const Flow*) +{ + return ((const DnsResponseEvent*)event)->get_RA() ? "T" : "F"; +} + +static const char* get_answers(const DataEvent* event, const Flow*) +{ + return ((const DnsResponseEvent*)event)->get_answers().c_str(); +} + +static const char* get_rejected(const DataEvent* event, const Flow*) +{ + return ((const DnsResponseEvent*)event)->get_rejected() ? "T" : "F"; +} + +static const map sub_buf_getters = +{ + {"proto", get_proto}, + {"query", get_query}, + {"qclass_name", get_qclass_name}, + {"qtype_name", get_qtype_name}, + {"rcode_name", get_rcode_name}, + {"AA", get_AA}, + {"TC", get_TC}, + {"RD", get_RD}, + {"RA", get_RA}, + {"answers", get_answers}, + {"rejected", get_rejected} +}; + +THREAD_LOCAL const snort::Connector::ID* DnsResponseExtractor::log_id = nullptr; + +DnsResponseExtractor::DnsResponseExtractor(Extractor& i, uint32_t t, const vector& fields) + : ExtractorEvent(ServiceType::DNS, i, t) +{ + 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(num_fields, sub_num_getters, f)) + continue; + if (append(buf_fields, sub_buf_getters, f)) + continue; + } + + DataBus::subscribe_global(dns_pub_key, DnsEventIds::DNS_RESPONSE, + new Resp(*this, S_NAME), i.get_snort_config()); +} + +void DnsResponseExtractor::internal_tinit(const snort::Connector::ID* id) +{ + log_id = id; +} + +void DnsResponseExtractor::handle(DataEvent& event, Flow* flow) +{ + // cppcheck-suppress unreadVariable + Profile profile(extractor_perf_stats); + + if (!filter(flow)) + return; + + extractor_stats.total_event++; + + logger->open_record(); + log(nts_fields, &event, flow); + log(sip_fields, &event, flow); + log(num_fields, &event, flow); + log(buf_fields, &event, flow); + logger->close_record(*log_id); +} + +//------------------------------------------------------------------------- +// Unit Tests +//------------------------------------------------------------------------- + +#ifdef UNIT_TEST + +#include "catch/snort_catch.h" + +#include + +TEST_CASE("DNS Proto", "[extractor]") +{ + Flow* flow = new Flow; + InspectionPolicy ins; + set_inspection_policy(&ins); + NetworkPolicy net; + set_network_policy(&net); + + SECTION("unknown") + { + flow->pkt_type = PktType::NONE; + const char* proto = get_proto(nullptr, flow); + CHECK_FALSE(strcmp("", proto)); + } + + delete flow; +} + +#endif diff --git a/src/network_inspectors/extractor/extractor_dns.h b/src/network_inspectors/extractor/extractor_dns.h new file mode 100644 index 000000000..e13989c0b --- /dev/null +++ b/src/network_inspectors/extractor/extractor_dns.h @@ -0,0 +1,40 @@ +//-------------------------------------------------------------------------- +// 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_dns.h author Cisco + +#ifndef EXTRACTOR_DNS_H +#define EXTRACTOR_DNS_H + +#include "extractors.h" + +class DnsResponseExtractor : public ExtractorEvent +{ +public: + DnsResponseExtractor(Extractor&, uint32_t tenant, const std::vector& fields); + + void handle(DataEvent&, Flow*); + +private: + using Resp = Handler; + + void internal_tinit(const snort::Connector::ID*) override; + + static THREAD_LOCAL const snort::Connector::ID* log_id; +}; + +#endif diff --git a/src/network_inspectors/extractor/extractor_enums.h b/src/network_inspectors/extractor/extractor_enums.h index 95aea348b..9d7bd15cd 100644 --- a/src/network_inspectors/extractor/extractor_enums.h +++ b/src/network_inspectors/extractor/extractor_enums.h @@ -30,6 +30,7 @@ public: HTTP, FTP, CONN, + DNS, ANY, MAX }; @@ -51,6 +52,8 @@ public: return "ftp"; case CONN: return "conn"; + case DNS: + return "dns"; case ANY: // fallthrough case MAX: // fallthrough default: diff --git a/src/network_inspectors/extractor/extractor_service.cc b/src/network_inspectors/extractor/extractor_service.cc index ec9f1e428..ffdbe2506 100644 --- a/src/network_inspectors/extractor/extractor_service.cc +++ b/src/network_inspectors/extractor/extractor_service.cc @@ -27,6 +27,7 @@ #include "extractor.h" #include "extractor_conn.h" +#include "extractor_dns.h" #include "extractor_ftp.h" #include "extractor_http.h" @@ -123,6 +124,10 @@ ExtractorService* ExtractorService::make_service(Extractor& ins, const ServiceCo srv = new ConnExtractorService(cfg.tenant_id, cfg.fields, cfg.on_events, cfg.service, ins); break; + case ServiceType::DNS: + srv = new DnsExtractorService(cfg.tenant_id, cfg.fields, cfg.on_events, cfg.service, ins); + break; + case ServiceType::ANY: // fallthrough default: ErrorMessage("Extractor: '%s' service is not supported\n", cfg.service.c_str()); @@ -215,6 +220,11 @@ void ExtractorService::validate(const ServiceConfig& cfg) validate_fields(ConnExtractorService::blueprint, cfg.fields); break; + case ServiceType::DNS: + validate_events(DnsExtractorService::blueprint, cfg.on_events); + validate_fields(DnsExtractorService::blueprint, cfg.fields); + break; + case ServiceType::ANY: // fallthrough default: ParseError("'%s' service is not supported", cfg.service.c_str()); @@ -363,6 +373,56 @@ const snort::Connector::ID& ConnExtractorService::internal_tinit() const snort::Connector::ID& ConnExtractorService::get_log_id() { return log_id; } +//------------------------------------------------------------------------- +// DnsExtractorService +//------------------------------------------------------------------------- + +const ServiceBlueprint DnsExtractorService::blueprint = +{ + // events + { + "response", + }, + // fields + { + "proto", + "trans_id", + "query", + "qclass", + "qclass_name", + "qtype", + "qtype_name", + "rcode", + "rcode_name", + "AA", + "TC", + "RD", + "RA", + "Z", + "answers", + "rejected" + }, +}; + +THREAD_LOCAL Connector::ID DnsExtractorService::log_id; + +DnsExtractorService::DnsExtractorService(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("response", event.c_str())) + handlers.push_back(new DnsResponseExtractor(ins, tenant_id, get_fields())); + } +} + +const snort::Connector::ID& DnsExtractorService::internal_tinit() +{ return log_id = logger->get_id(type.c_str()); } + +const snort::Connector::ID& DnsExtractorService::get_log_id() +{ return log_id; } + //------------------------------------------------------------------------- // Unit Tests //------------------------------------------------------------------------- @@ -380,12 +440,14 @@ TEST_CASE("Service Type", "[extractor]") ServiceType http = ServiceType::HTTP; ServiceType ftp = ServiceType::FTP; ServiceType conn = ServiceType::CONN; + ServiceType dns = ServiceType::DNS; ServiceType any = ServiceType::ANY; ServiceType max = ServiceType::MAX; CHECK_FALSE(strcmp("http", http.c_str())); CHECK_FALSE(strcmp("ftp", ftp.c_str())); CHECK_FALSE(strcmp("conn", conn.c_str())); + CHECK_FALSE(strcmp("dns", dns.c_str())); CHECK_FALSE(strcmp("(not set)", any.c_str())); CHECK_FALSE(strcmp("(not set)", max.c_str())); } diff --git a/src/network_inspectors/extractor/extractor_service.h b/src/network_inspectors/extractor/extractor_service.h index eb8b2fd05..c0bd0896e 100644 --- a/src/network_inspectors/extractor/extractor_service.h +++ b/src/network_inspectors/extractor/extractor_service.h @@ -137,5 +137,20 @@ private: static THREAD_LOCAL snort::Connector::ID log_id; }; +class DnsExtractorService : public ExtractorService +{ +public: + static const ServiceBlueprint blueprint; + + DnsExtractorService(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; +}; + #endif diff --git a/src/pub_sub/dns_events.cc b/src/pub_sub/dns_events.cc index 9ea0b291a..c6c523399 100644 --- a/src/pub_sub/dns_events.cc +++ b/src/pub_sub/dns_events.cc @@ -24,11 +24,174 @@ #include "dns_events.h" #include +#include +#include #include "service_inspectors/dns/dns.h" using namespace snort; +static const std::string& class_name(uint16_t query_class) +{ + static const std::map class_names = + { + {1, "C_INTERNET"}, // RFC 1035 + {2, "C_CSNET"}, // RFC 1035 + {3, "C_CHAOS"}, // RFC 1035 + {4, "C_HESIOD"}, // RFC 1035 + {254, "C_NONE"}, // RFC 2136 + {255, "ANY"} // RFC 1035 + // Add more classes as needed + }; + + static const std::string unknown = "UNKNOWN"; + + auto it = class_names.find(query_class); + return it != class_names.end() ? it->second : unknown; +} + +static const std::string& qtype_name(uint16_t query_type) +{ + static const std::map qtype_names = + { + {1, "A"}, // RFC 1035 + {2, "NS"}, // RFC 1035 + {3, "MD"}, // RFC 1035 + {4, "MF"}, // RFC 1035 + {5, "CNAME"}, // RFC 1035 + {6, "SOA"}, // RFC 1035 + {7, "MB"}, // RFC 1035 + {8, "MG"}, // RFC 1035 + {9, "MR"}, // RFC 1035 + {10, "NULL"}, // RFC 1035 + {11, "WKS"}, // RFC 1035 + {12, "PTR"}, // RFC 1035 + {13, "HINFO"}, // RFC 1035 + {14, "MINFO"}, // RFC 1035 + {15, "MX"}, // RFC 1035 + {16, "TXT"}, // RFC 1035 + {17, "RP"}, // RFC 1183 + {18, "AFSDB"}, // RFC 1183 + {19, "X25"}, // RFC 1183 + {20, "ISDN"}, // RFC 1183 + {21, "RT"}, // RFC 1183 + {22, "NSAP"}, // RFC 1706 + {23, "NSAP_PTR"}, // RFC 1348 + {24, "SIG"}, // RFC 2536 + {25, "KEY"}, // RFC 2536 + {26, "PX"}, // RFC 2163 + {27, "GPOS"}, // RFC 1712 + {28, "AAAA"}, // RFC 3596 + {29, "LOC"}, // RFC 1876 + {30, "NXT"}, // RFC 2535 + {31, "EID"}, + {32, "NIMLOC"}, + {33, "SRV"}, // RFC 2782 + {34, "ATMA"}, + {35, "NAPTR"}, // RFC 3403 + {36, "KX"}, // RFC 2230 + {37, "CERT"}, // RFC 4398 + {38, "A6"}, // RFC 2874 + {39, "DNAME"}, // RFC 6672 + {40, "SINK"}, + {41, "OPT"}, // RFC 6891 + {42, "APL"}, // RFC 3123 + {43, "DS"}, // RFC 4034 + {44, "SSHFP"}, // RFC 4255 + {45, "IPSECKEY"}, // RFC 4025 + {46, "RRSIG"}, // RFC 4034 + {47, "NSEC"}, // RFC 4034 + {48, "DNSKEY"}, // RFC 4034 + {49, "DHCID"}, // RFC 4701 + {50, "NSEC3"}, // RFC 5155 + {51, "NSEC3PARAM"}, // RFC 5155 + {52, "TLSA"}, // RFC 6698 + {53, "SMIMEA"}, // RFC 8162 + {55, "HIP"}, // RFC 8005 + {56, "NINFO"}, + {57, "RKEY"}, + {58, "TALINK"}, + {59, "CDS"}, // RFC 7344 + {60, "CDNSKEY"}, // RFC 7344 + {61, "OPENPGPKEY"}, // RFC 7929 + {62, "CSYNC"}, // RFC 7477 + {63, "ZONEMD"}, // RFC 8976 + {64, "SVCB"}, // RFC 9460 + {65, "HTTPS"}, // RFC 9460 + {66, "DSYNC"}, + {99, "SPF"}, // RFC 7208 + {100, "UINFO"}, + {101, "UID"}, + {102, "GID"}, + {103, "UNSPEC"}, + {104, "NID"}, // RFC 6742 + {105, "L32"}, // RFC 6742 + {106, "L64"}, // RFC 6742 + {107, "LP"}, // RFC 6742 + {108, "EUI48"}, // RFC 7043 + {109, "EUI64"}, // RFC 7043 + {249, "TKEY"}, // RFC 2930 + {250, "TSIG"}, // RFC 8945 + {251, "IXFR"}, // RFC 1995 + {252, "AXFR"}, // RFC 1035 + {253, "MAILB"}, // RFC 1035 + {254, "MAILA"}, // RFC 1035 + {255, "*"}, // RFC 1035, also known as ANY + {256, "URI"}, // RFC 7553 + {257, "CAA"}, // RFC 8659 + {32768, "TA"}, + {32769, "DLV"}, // RFC 4431 + {65281, "WINS"}, // Microsoft + {65282, "WINS-R"}, // Microsoft + {65521, "INTEGRITY"} // Chromium Design Doc: Querying HTTPSSVC + // Add more QTYPEs as needed + }; + + static const std::string unknown = "UNKNOWN"; + + auto it = qtype_names.find(query_type); + return it != qtype_names.end() ? it->second : unknown; +} + +static const std::string& rcode_name(uint16_t rcode) +{ + static const std::map rcode_names = + { + {0, "NOERROR"}, // RFC 1035 + {1, "FORMERR"}, // RFC 1035 + {2, "SERVFAIL"}, // RFC 1035 + {3, "NXDOMAIN"}, // RFC 1035, also referred as "Name Error" + {4, "NOTIMP"}, // RFC 1035 + {5, "REFUSED"}, // RFC 1035 + {6, "YXDOMAIN"}, // RFC 2136, RFC 6672 + {7, "YXRRSET"}, // RFC 2136 + {8, "NXRRSET"}, // RFC 2136 + {9, "NOTAUTH"}, // RFC 2136, RFC 8945 + {10, "NOTZONE"}, // RFC 2136 + {11, "DSOTYPENI"}, // RFC 8490 + {12, "unassigned-12"}, + {13, "unassigned-13"}, + {14, "unassigned-14"}, + {15, "unassigned-15"}, + // extended RCODEs below, see + // https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-6 + {16, "BADVERS"}, // RFC 6891, also BADSIG RFC 8945 + {17, "BADKEY"}, // RFC 8945 + {18, "BADTIME"}, // RFC 8945 + {19, "BADMODE"}, // RFC 2930 + {20, "BADNAME"}, // RFC 2930 + {21, "BADALG"}, // RFC 2930 + {22, "BADTRUNC"}, // RFC 8945 + {23, "BADCOOKIE"} // RFC 7873 + // Add more RCODEs as needed + }; + + static const std::string unknown = "UNKNOWN"; + + auto it = rcode_names.find(rcode); + return it != rcode_names.end() ? it->second : unknown; +} + void IPFqdnCacheItem::add_ip(const SfIp& ip) { if (ip.is_set()) @@ -75,3 +238,86 @@ bool DnsResponseDataEvents::empty() const { return (dns_ips.empty() or dns_fqdns.empty()); } + +uint16_t DnsResponseEvent::get_trans_id() const +{ + return session.hdr.id; +} + +const std::string& DnsResponseEvent::get_query() const +{ + return session.resp_query; +} + +uint16_t DnsResponseEvent::get_query_class() const +{ + return session.curr_q.dns_class; +} + +const std::string& DnsResponseEvent::get_query_class_name() const +{ + static const std::string empty = ""; + if (session.hdr.questions == 0) + return empty; + return class_name(session.curr_q.dns_class); +} + +uint16_t DnsResponseEvent::get_query_type() const +{ + return session.curr_q.type; +} + +const std::string& DnsResponseEvent::get_query_type_name() const +{ + static const std::string empty = ""; + if (session.hdr.questions == 0) + return empty; + return qtype_name(get_query_type()); +} + +uint8_t DnsResponseEvent::get_rcode() const +{ + return session.hdr.flags & DNS_HDR_FLAG_REPLY_CODE_MASK; +} + +const std::string& DnsResponseEvent::get_rcode_name() const +{ + return rcode_name(get_rcode()); +} + +bool DnsResponseEvent::get_AA() const +{ + return session.hdr.flags & DNS_HDR_FLAG_AUTHORITATIVE; +} + +bool DnsResponseEvent::get_TC() const +{ + return session.hdr.flags & DNS_HDR_FLAG_TRUNCATED; +} + +bool DnsResponseEvent::get_RD() const +{ + return session.hdr.flags & DNS_HDR_FLAG_RECURSION_DESIRED; +} + +bool DnsResponseEvent::get_RA() const +{ + return session.hdr.flags & DNS_HDR_FLAG_RECURSION_AVAIL; +} + +uint8_t DnsResponseEvent::get_Z() const +{ + return (session.hdr.flags & DNS_HDR_FLAG_Z) >> DNS_HDR_FLAG_Z_SHIFT; +} + +const std::string& DnsResponseEvent::get_answers() const +{ + return session.answers; +} + +bool DnsResponseEvent::get_rejected() const +{ + return session.hdr.flags & DNS_HDR_FLAG_RESPONSE && + session.hdr.flags & DNS_HDR_FLAG_REPLY_CODE_MASK && + !session.hdr.questions; +} diff --git a/src/pub_sub/dns_events.h b/src/pub_sub/dns_events.h index 6ec7383da..809985d4b 100644 --- a/src/pub_sub/dns_events.h +++ b/src/pub_sub/dns_events.h @@ -50,6 +50,7 @@ struct DnsEventIds enum : unsigned { DNS_RESPONSE_DATA, + DNS_RESPONSE, num_ids }; }; @@ -58,6 +59,7 @@ const snort::PubKey dns_pub_key { "dns", DnsEventIds::num_ids }; class DnsResponseIp; class DnsResponseFqdn; +struct DNSData; namespace snort { @@ -73,6 +75,32 @@ private: std::vector dns_ips; std::vector dns_fqdns; }; + +class SO_PUBLIC DnsResponseEvent : public snort::DataEvent +{ +public: + DnsResponseEvent(const DNSData& ssn) : session(ssn) { } + + uint16_t get_trans_id() const; + const std::string& get_query() const; + uint16_t get_query_class() const; + const std::string& get_query_class_name() const; + uint16_t get_query_type() const; + const std::string& get_query_type_name() const; + uint8_t get_rcode() const; + const std::string& get_rcode_name() const; + bool get_AA() const; + bool get_TC() const; + bool get_RD() const; + bool get_RA() const; + uint8_t get_Z() const; + const std::string& get_answers() const; + bool get_rejected() const; + +private: + const DNSData& session; +}; + } #endif diff --git a/src/service_inspectors/dns/dns.cc b/src/service_inspectors/dns/dns.cc index 045d8b227..335219176 100644 --- a/src/service_inspectors/dns/dns.cc +++ b/src/service_inspectors/dns/dns.cc @@ -122,6 +122,13 @@ bool DNSData::valid_dns(const DNSHdr& dns_header) const return true; } +void DNSData::add_answer(const char* answer) +{ + if (!answers.empty()) + answers.append(" "); + answers.append(answer); +} + DNSData* get_dns_session_data(Packet* p, bool from_server, DNSData& udpSessionData) { DnsFlowData* fd; @@ -428,11 +435,12 @@ static uint16_t ParseDNSQuestion( if (dnsSessionData->curr_rec_state < DNS_RESP_STATE_Q_NAME_COMPLETE) { - uint16_t new_bytes_unused = ParseDNSName(data, bytes_unused, dnsSessionData); + uint16_t new_bytes_unused = ParseDNSName(data, bytes_unused, dnsSessionData, true); uint16_t bytes_used = bytes_unused - new_bytes_unused; if (dnsSessionData->curr_txt.name_state == DNS_RESP_STATE_NAME_COMPLETE) { + dnsSessionData->resp_query = dnsSessionData->curr_txt.dns_name; dnsSessionData->curr_rec_state = DNS_RESP_STATE_Q_TYPE; dnsSessionData->curr_txt = DNSNameState(); data = data + bytes_used; @@ -738,7 +746,8 @@ static uint16_t SkipDNSRData( static uint16_t ParseDNSRData( const unsigned char* data, uint16_t bytes_unused, - DNSData* dnsSessionData) + DNSData* dnsSessionData, + void (DNSData::*save_rdata)(const char*) = nullptr) { if (bytes_unused == 0) { @@ -770,13 +779,23 @@ static uint16_t ParseDNSRData( break; case DNS_RR_TYPE_A: case DNS_RR_TYPE_AAAA: - if (dnsSessionData->publish_response()) { - dnsSessionData->dns_events.add_fqdn(dnsSessionData->cur_fqdn_event, dnsSessionData->curr_rr.ttl); - dnsSessionData->dns_events.add_ip(DnsResponseIp(data, dnsSessionData->curr_rr.type)); - } + auto resp_ip = DnsResponseIp(data, dnsSessionData->curr_rr.type); + if (dnsSessionData->publish_response()) + { + dnsSessionData->dns_events.add_fqdn(dnsSessionData->cur_fqdn_event, dnsSessionData->curr_rr.ttl); + dnsSessionData->dns_events.add_ip(resp_ip); + } + if (save_rdata) + { + SfIpString ipbuf; + resp_ip.get_ip().ntop(ipbuf); + (dnsSessionData->*save_rdata)(ipbuf); + } + } bytes_unused = SkipDNSRData(data, bytes_unused, dnsSessionData); + break; case DNS_RR_TYPE_CNAME: if (dnsSessionData->publish_response()) @@ -904,7 +923,7 @@ static void ParseDNSResponseMessage(Packet* p, DNSData* dnsSessionData, bool& ne case DNS_RESP_STATE_RR_RDATA_MID: /* Data now points to the beginning of the RDATA */ data = p->data + (p->dsize - bytes_unused); - bytes_unused = ParseDNSRData(data, bytes_unused, dnsSessionData); + bytes_unused = ParseDNSRData(data, bytes_unused, dnsSessionData, &DNSData::add_answer); if (dnsSessionData->curr_rec_state != DNS_RESP_STATE_RR_COMPLETE) { needNextPacket = true; @@ -1178,6 +1197,9 @@ static void snort_dns(Packet* p, const DnsConfig* dns_config) if (!needNextPacket and dnsSessionData->has_events()) DataBus::publish(Dns::get_pub_id(), DnsEventIds::DNS_RESPONSE_DATA, dnsSessionData->dns_events); + DnsResponseEvent dns_response_event(*dnsSessionData); + DataBus::publish(Dns::get_pub_id(), DnsEventIds::DNS_RESPONSE, dns_response_event, p->flow); + if (p->type() == PktType::UDP) p->flow->session_state |= STREAM_STATE_CLOSED; } diff --git a/src/service_inspectors/dns/dns.h b/src/service_inspectors/dns/dns.h index 450e82c72..9b09961bf 100644 --- a/src/service_inspectors/dns/dns.h +++ b/src/service_inspectors/dns/dns.h @@ -55,6 +55,9 @@ struct DNSHdr #define DNS_HDR_FLAG_OPCODE_MASK 0x7800 #define DNS_HDR_FLAG_RESPONSE 0x8000 +#define DNS_HDR_FLAG_Z (DNS_HDR_FLAG_NON_AUTHENTICATED_OK | DNS_HDR_FLAG_ANS_AUTHENTICATED | DNS_HDR_FLAG_RESERVED) +#define DNS_HDR_FLAG_Z_SHIFT 4 + struct DNSQuestion { uint16_t type = 0; @@ -202,10 +205,13 @@ struct DNSData const DnsConfig* dns_config = nullptr; snort::DnsResponseDataEvents dns_events; DnsResponseFqdn cur_fqdn_event; + std::string resp_query; + std::string answers; bool publish_response() const; bool has_events() const; bool valid_dns(const DNSHdr&) const; + void add_answer(const char* answer); }; DNSData* get_dns_session_data(snort::Packet* p, bool from_server, DNSData& udpSessionData);