]> git.ipfire.org Git - thirdparty/snort3.git/commitdiff
Pull request #4675: extractor: extend dns logging
authorAdrian Mamolea (admamole) <admamole@cisco.com>
Fri, 4 Apr 2025 14:06:39 +0000 (14:06 +0000)
committerMaya Dagon (mdagon) <mdagon@cisco.com>
Fri, 4 Apr 2025 14:06:39 +0000 (14:06 +0000)
Merge in SNORT/snort3 from ~ADMAMOLE/snort3:extractor_dns2 to master

Squashed commit of the following:

commit 92b7e2c0ab8f1b0fba620f80a2882dea301cbc8c
Author: Adrian Mamolea <admamole@cisco.com>
Date:   Mon Mar 24 17:03:25 2025 -0400

    extractor: extend dns logging

doc/user/extractor.txt
src/network_inspectors/extractor/extractor_dns.cc
src/network_inspectors/extractor/extractor_service.cc
src/pub_sub/dns_events.cc
src/pub_sub/dns_events.h
src/service_inspectors/dns/CMakeLists.txt
src/service_inspectors/dns/dns.cc
src/service_inspectors/dns/dns.h
src/service_inspectors/dns/dns_rr_decoder.cc [new file with mode: 0644]

index 1dd5566b2ee161531a053649861aacace0f9aa08..bb866c7e16358cda222cfa61bb4d63bc38b8c280 100644 (file)
@@ -124,8 +124,14 @@ Fields supported for DNS:
 * `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
+* `answers` - The list of answers to the query
+* `TTLs` - The list of caching intervals for the corresponding answers
 * `rejected` - A boolean, true when the server responds with an error code and no query
+* `auth` - The list of authoritative responses
+* `addl` - The list of additional responses
+
+In the answers, auth, and addl lists the decoding of the following RR types is supported:
+A, AAAA, CNAME, DS, MX, NS, NSEC, PTR, RRSIG, SOA, TXT
 
 Fields supported for connection:
 
index 7176e566f9bd95c68ea12f76bc02f4841c647532..49bad0625d9e814dab505d2cce72ecdb00266221 100644 (file)
@@ -124,11 +124,26 @@ static const char* get_answers(const DataEvent* event, const Flow*)
     return ((const DnsResponseEvent*)event)->get_answers().c_str();
 }
 
+static const char* get_TTLs(const DataEvent* event, const Flow*)
+{
+    return ((const DnsResponseEvent*)event)->get_TTLs().c_str();
+}
+
 static const char* get_rejected(const DataEvent* event, const Flow*)
 {
     return ((const DnsResponseEvent*)event)->get_rejected() ? "T" : "F";
 }
 
+static const char* get_auth(const DataEvent* event, const Flow*)
+{
+    return ((const DnsResponseEvent*)event)->get_auth().c_str();
+}
+
+static const char* get_addl(const DataEvent* event, const Flow*)
+{
+    return ((const DnsResponseEvent*)event)->get_addl().c_str();
+}
+
 static const map<string, ExtractorEvent::BufGetFn> sub_buf_getters =
 {
     {"proto", get_proto},
@@ -141,7 +156,10 @@ static const map<string, ExtractorEvent::BufGetFn> sub_buf_getters =
     {"RD", get_RD},
     {"RA", get_RA},
     {"answers", get_answers},
-    {"rejected", get_rejected}
+    {"TTLs", get_TTLs},
+    {"rejected", get_rejected},
+    {"auth", get_auth},
+    {"addl", get_addl}
 };
 
 THREAD_LOCAL const snort::Connector::ID* DnsResponseExtractor::log_id = nullptr;
index 29e388986540d750c2c49a841d1546515786e031..5401977c6b3ca1726283902857d5fc5739eb552e 100644 (file)
@@ -401,7 +401,10 @@ const ServiceBlueprint DnsExtractorService::blueprint =
         "RA",
         "Z",
         "answers",
-        "rejected"
+        "TTLs",
+        "rejected",
+        "auth",
+        "addl"
     },
 };
 
index c6c52339969fd65f55ffff9752c4844e36260104..85990b59c847a5e0a30dc7383e3e019b1d7c5e05 100644 (file)
@@ -312,7 +312,22 @@ uint8_t DnsResponseEvent::get_Z() const
 
 const std::string& DnsResponseEvent::get_answers() const
 {
-    return session.answers;
+    if (!answers_set)
+    {
+        session.get_answers(packet, answers, ttls);
+        answers_set = true;
+    }
+    return answers;
+}
+
+const std::string& DnsResponseEvent::get_TTLs() const
+{
+    if (!answers_set)
+    {
+        session.get_answers(packet, answers, ttls);
+        answers_set = true;
+    }
+    return ttls;
 }
 
 bool DnsResponseEvent::get_rejected() const
@@ -321,3 +336,23 @@ bool DnsResponseEvent::get_rejected() const
         session.hdr.flags & DNS_HDR_FLAG_REPLY_CODE_MASK &&
         !session.hdr.questions;
 }
+
+const std::string& DnsResponseEvent::get_auth() const
+{
+    if (!auth_set)
+    {
+        session.get_auth(packet, auth);
+        auth_set = true;
+    }
+    return auth;
+}
+
+const std::string& DnsResponseEvent::get_addl() const
+{
+    if (!addl_set)
+    {
+        session.get_addl(packet, addl);
+        addl_set = true;
+    }
+    return addl;
+}
index dced26d2aaa7b7c80839e4ec6a18dd506fca136f..4c88e2aa8e4e3ceac08102c1d6092f378bc41857 100644 (file)
@@ -86,7 +86,7 @@ private:
 class SO_PUBLIC DnsResponseEvent : public snort::DataEvent
 {
 public:
-    DnsResponseEvent(const DNSData& ssn) : session(ssn) { }
+    DnsResponseEvent(const DNSData& ssn, Packet* p) : session(ssn), packet(p) { }
 
     uint16_t get_trans_id() const;
     const std::string& get_query() const;
@@ -102,10 +102,21 @@ public:
     bool get_RA() const;
     uint8_t get_Z() const;
     const std::string& get_answers() const;
+    const std::string& get_TTLs() const;
     bool get_rejected() const;
+    const std::string& get_auth() const;
+    const std::string& get_addl() const;
 
 private:
     const DNSData& session;
+    const Packet* packet = nullptr;
+    mutable std::string answers;
+    mutable std::string ttls;
+    mutable std::string auth;
+    mutable std::string addl;
+    mutable bool answers_set = false;
+    mutable bool auth_set = false;
+    mutable bool addl_set = false;
 };
 
 }
index 9a5ca65ed3c6cf8719cb7d8be55be46f6da3cb04..cc32d6af8fccad8912a927d24eea5d8058d1a4ed 100644 (file)
@@ -6,6 +6,7 @@ set( FILE_LIST
     dns_config.h
     dns_module.cc
     dns_module.h
+    dns_rr_decoder.cc
     dns_splitter.cc
     dns_splitter.h
 )
index 3bc1df31930f64b5951581c33e620b28489d4e4b..a4fa54ce6264ec40b87e9dcb5455e560125523d2 100644 (file)
@@ -40,7 +40,6 @@
 using namespace snort;
 
 #define MAX_UDP_PAYLOAD 0x1FFF
-#define DNS_RR_PTR 0xC0
 
 THREAD_LOCAL ProfileStats dnsPerfStats;
 THREAD_LOCAL DnsStats dnsstats;
@@ -127,13 +126,6 @@ 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;
@@ -512,7 +504,8 @@ static uint16_t ParseDNSQuestion(
 }
 
 static uint16_t ParseDNSAnswer(
-    const unsigned char* data, uint16_t bytes_unused, DNSData* dnsSessionData)
+    const unsigned char* data, uint16_t bytes_unused, DNSData* dnsSessionData,
+    const Packet* p, std::vector<uint16_t>& tabs)
 {
     if ( !bytes_unused )
         return 0;
@@ -540,6 +533,7 @@ static uint16_t ParseDNSAnswer(
     switch (dnsSessionData->curr_rec_state)
     {
     case DNS_RESP_STATE_RR_TYPE:
+        tabs.emplace_back(data - p->data);
         dnsSessionData->curr_rr.type = (uint8_t)*data << 8;
         data++;
 
@@ -756,8 +750,7 @@ static uint16_t SkipDNSRData(
 static uint16_t ParseDNSRData(
     const unsigned char* data,
     uint16_t bytes_unused,
-    DNSData* dnsSessionData,
-    void (DNSData::*save_rdata)(const char*) = nullptr)
+    DNSData* dnsSessionData)
 {
     if (bytes_unused == 0)
     {
@@ -789,23 +782,13 @@ static uint16_t ParseDNSRData(
         break;
     case DNS_RR_TYPE_A:
     case DNS_RR_TYPE_AAAA:
+        if (dnsSessionData->publish_response())
         {
-            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);
-            }
+            dnsSessionData->dns_events.add_fqdn(dnsSessionData->cur_fqdn_event, dnsSessionData->curr_rr.ttl);
+            dnsSessionData->dns_events.add_ip(DnsResponseIp(data, dnsSessionData->curr_rr.type));
         }
-        bytes_unused = SkipDNSRData(data, bytes_unused, dnsSessionData);
 
+        bytes_unused = SkipDNSRData(data, bytes_unused, dnsSessionData);
         break;
     case DNS_RR_TYPE_CNAME:
         if (dnsSessionData->publish_response())
@@ -819,6 +802,9 @@ static uint16_t ParseDNSRData(
     case DNS_RR_TYPE_PTR:
     case DNS_RR_TYPE_HINFO:
     case DNS_RR_TYPE_MX:
+    case DNS_RR_TYPE_RRSIG:
+    case DNS_RR_TYPE_NSEC:
+    case DNS_RR_TYPE_DS:
         bytes_unused = SkipDNSRData(data, bytes_unused, dnsSessionData);
         break;
     default:
@@ -916,7 +902,7 @@ static void ParseDNSResponseMessage(Packet* p, DNSData* dnsSessionData, bool& ne
         case DNS_RESP_STATE_ANS_RR: /* ANSWERS section */
             for (i=dnsSessionData->curr_rec; i<dnsSessionData->hdr.answers; i++)
             {
-                bytes_unused = ParseDNSAnswer(data, bytes_unused, dnsSessionData);
+                bytes_unused = ParseDNSAnswer(data, bytes_unused, dnsSessionData, p, dnsSessionData->answer_tabs);
 
                 if (bytes_unused == 0)
                 {
@@ -933,7 +919,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, &DNSData::add_answer);
+                    bytes_unused = ParseDNSRData(data, bytes_unused, dnsSessionData);
                     if (dnsSessionData->curr_rec_state != DNS_RESP_STATE_RR_COMPLETE)
                     {
                         needNextPacket = true;
@@ -961,7 +947,7 @@ static void ParseDNSResponseMessage(Packet* p, DNSData* dnsSessionData, bool& ne
         case DNS_RESP_STATE_AUTH_RR: /* AUTHORITIES section */
             for (i=dnsSessionData->curr_rec; i<dnsSessionData->hdr.authorities; i++)
             {
-                bytes_unused = ParseDNSAnswer(data, bytes_unused, dnsSessionData);
+                bytes_unused = ParseDNSAnswer(data, bytes_unused, dnsSessionData, p, dnsSessionData->auth_tabs);
 
                 if (bytes_unused == 0)
                 {
@@ -1006,7 +992,7 @@ static void ParseDNSResponseMessage(Packet* p, DNSData* dnsSessionData, bool& ne
         case DNS_RESP_STATE_ADD_RR: /* ADDITIONALS section */
             for (i=dnsSessionData->curr_rec; i<dnsSessionData->hdr.additionals; i++)
             {
-                bytes_unused = ParseDNSAnswer(data, bytes_unused, dnsSessionData);
+                bytes_unused = ParseDNSAnswer(data, bytes_unused, dnsSessionData, p, dnsSessionData->addl_tabs);
 
                 if (bytes_unused == 0)
                 {
@@ -1268,7 +1254,7 @@ 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);
+            DnsResponseEvent dns_response_event(*dnsSessionData, p);
             DataBus::publish(Dns::get_pub_id(), DnsEventIds::DNS_RESPONSE, dns_response_event, p->flow);
         }
 
index 6bf2fba6027171550732c781ebffa780b930daab..07de79843d5fc67a95c26f50d6a76fc393fdb0a9 100644 (file)
@@ -112,6 +112,11 @@ struct DNSNameState
 #define DNS_RR_TYPE_MX                      0x000f
 #define DNS_RR_TYPE_TXT                     0x0010
 #define DNS_RR_TYPE_AAAA                    0x001c
+#define DNS_RR_TYPE_RRSIG                   0x002e
+#define DNS_RR_TYPE_NSEC                    0x002f
+#define DNS_RR_TYPE_DS                      0x002b
+
+#define DNS_RR_PTR 0xC0
 
 #define DNS_FLAG_NOT_DNS                0x01
 
@@ -208,12 +213,22 @@ struct DNSData
     snort::DnsResponseDataEvents dns_events;
     DnsResponseFqdn cur_fqdn_event;
     std::string resp_query;
-    std::string answers;
+    std::vector<uint16_t> answer_tabs;
+    std::vector<uint16_t> auth_tabs;
+    std::vector<uint16_t> addl_tabs;
 
     bool publish_response() const;
     bool has_events() const;
     bool valid_dns(const DNSHdr&) const;
-    void add_answer(const char* answer);
+
+    void decode_rdata(const snort::Packet* p, const uint8_t* rdata, uint16_t rdlength,
+        uint16_t type, std::string& rdata_str) const;
+    void get_rr_data(const snort::Packet *p, const std::vector<uint16_t>& tabs,
+        std::string& rrs, std::string* ttls = nullptr) const;
+    void get_answers(const snort::Packet *p, std::string& answers, std::string& ttls) const
+    { get_rr_data(p, answer_tabs, answers, &ttls); }
+    void get_auth(const snort::Packet *p, std::string& auth) const { get_rr_data(p, auth_tabs, auth); }
+    void get_addl(const snort::Packet *p, std::string& addl) const { get_rr_data(p, addl_tabs, addl); }
 };
 
 DNSData* get_dns_session_data(snort::Packet* p, bool from_server, DNSData& udpSessionData);
diff --git a/src/service_inspectors/dns/dns_rr_decoder.cc b/src/service_inspectors/dns/dns_rr_decoder.cc
new file mode 100644 (file)
index 0000000..daaa2bc
--- /dev/null
@@ -0,0 +1,258 @@
+//--------------------------------------------------------------------------
+// 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.
+//--------------------------------------------------------------------------
+// dns.cc author Cisco
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "dns.h"
+
+#include <string>
+#include "sfip/sf_ip.h"
+
+using namespace snort;
+
+static void decode_ip(const uint8_t* rdata, uint16_t rdlength, std::string& rdata_str, uint16_t type)
+{
+    int family = (type == DNS_RR_TYPE_A) ? AF_INET : AF_INET6;
+    if (family == AF_INET && rdlength != 4)
+        return;
+    if (family == AF_INET6 && rdlength != 16)
+        return;
+
+    SfIpString ipbuf;
+    SfIp rr_ip(rdata, family);
+    rr_ip.ntop(ipbuf);
+
+    rdata_str = ipbuf;
+}
+
+static const std::string part_sep = " ";    // separates between parts of an item in a list
+
+static void decode_txt(const uint8_t* rdata, uint16_t rdlength, std::string& rdata_str)
+{
+    static const std::string txt_prefix = "TXT" + part_sep;
+
+    while (rdlength)
+    {
+        if (!rdata_str.empty())
+            rdata_str.append(part_sep);
+        rdata_str.append(txt_prefix);
+
+        uint8_t txt_len = *rdata;
+        rdata_str.append(std::to_string(txt_len));
+        rdata_str.append(part_sep);
+        rdata++;
+        rdlength--;
+
+        uint8_t actual_len = rdlength > txt_len ? txt_len : rdlength;
+        rdata_str.append((const char*)rdata, actual_len);
+        rdata += actual_len;
+        rdlength -= actual_len;
+    }
+}
+
+static void decode_domain_name(const uint8_t* rdata, uint16_t rdlength,
+    std::string& rdata_str, const Packet* p = nullptr)
+{
+    static const int MAX_LINKS = 10;
+    int link_count = 0;
+
+    bool first_label = true;
+    while (rdlength)
+    {
+        uint8_t label_len = *rdata;
+        rdata++;
+        rdlength--;
+
+        if (label_len == 0)
+            break;
+
+        if ((label_len & DNS_RR_PTR) == DNS_RR_PTR)
+        {
+            if (p == nullptr)
+                break;  // compression not supported
+
+            if (rdlength < 1)
+                break;  // incomplete offset
+
+            uint16_t offset = ((label_len & ~DNS_RR_PTR) << 8) | *rdata;
+            if (offset >= p->dsize)
+                break;  // invalid offset
+            if (link_count++ > MAX_LINKS)
+                break;  // too many links
+
+            rdata = p->data + offset;
+            rdlength = p->dsize - offset;
+            continue;
+        }
+
+        if (label_len & DNS_RR_PTR)
+            break;  // invalid label length
+
+        uint8_t actual_len = rdlength > label_len ? label_len : rdlength;
+        if (!first_label)
+            rdata_str.append(".");
+        first_label = false;
+        rdata_str.append((const char*)rdata, actual_len);
+        rdata += actual_len;
+        rdlength -= actual_len;
+    }
+}
+
+static void decode_mx(const uint8_t* rdata, uint16_t rdlength, std::string& rdata_str, const Packet* p)
+{
+    static const unsigned EXCHANGE_OFFSET = 2;
+    if (rdlength <= EXCHANGE_OFFSET)
+        return; // incomplete MX record
+
+    rdata += EXCHANGE_OFFSET;
+    rdlength -= EXCHANGE_OFFSET;
+    decode_domain_name(rdata, rdlength, rdata_str, p);
+}
+
+static void decode_rrsig(const uint8_t* rdata, uint16_t rdlength, std::string& rdata_str)
+{
+    static const std::string rrsig_prefix = "RRSIG" + part_sep;
+    static const unsigned SIGNER_NAME_OFFSET = 18;
+
+    if (rdlength <= SIGNER_NAME_OFFSET)
+        return; // incomplete RRSIG record
+
+    auto type_covered = (uint16_t)rdata[0] << 8 | rdata[1];
+    rdata_str.append(rrsig_prefix);
+    rdata_str.append(std::to_string(type_covered));
+    rdata_str.append(part_sep);
+
+    rdata += SIGNER_NAME_OFFSET;
+    rdlength -= SIGNER_NAME_OFFSET;
+    decode_domain_name(rdata, rdlength, rdata_str);
+}
+
+static void decode_nsec(const uint8_t* rdata, uint16_t rdlength, std::string& rdata_str,
+    const std::string& resp_query)
+{
+    static const std::string nsec_prefix = "NSEC" + part_sep;
+
+    rdata_str.append(nsec_prefix);
+    rdata_str.append(resp_query);
+    rdata_str.append(part_sep);
+    decode_domain_name(rdata, rdlength, rdata_str);
+}
+
+static void decode_ds(const uint8_t* rdata, uint16_t rdlength, std::string& rdata_str)
+{
+    static const std::string ds_prefix = "DS" + part_sep;
+    static const unsigned DIGEST_OFFSET = 4;
+
+    if (rdlength <= DIGEST_OFFSET)
+        return; // incomplete DS record
+
+    rdata_str.append(ds_prefix);
+    rdata_str.append(std::to_string(rdata[2])); // algorithm
+    rdata_str.append(part_sep);
+    rdata_str.append(std::to_string(rdata[3])); // digest type
+}
+
+void DNSData::decode_rdata(const Packet* p, const uint8_t* rdata, uint16_t rdlength, uint16_t type,
+    std::string& rdata_str) const
+{
+    assert(rdata < p->data + p->dsize);
+
+    if (rdata + rdlength > p->data + p->dsize)
+        rdlength = p->dsize - (rdata - p->data);
+
+    switch (type)
+    {
+    case DNS_RR_TYPE_A:
+    case DNS_RR_TYPE_AAAA:
+        decode_ip(rdata, rdlength, rdata_str, type);
+        break;
+
+    case DNS_RR_TYPE_TXT:
+        decode_txt(rdata, rdlength, rdata_str);
+        break;
+
+    case DNS_RR_TYPE_CNAME:
+    case DNS_RR_TYPE_MD:
+    case DNS_RR_TYPE_MF:
+    case DNS_RR_TYPE_MB:
+    case DNS_RR_TYPE_MG:
+    case DNS_RR_TYPE_MR:
+    case DNS_RR_TYPE_NS:
+    case DNS_RR_TYPE_PTR:
+    case DNS_RR_TYPE_SOA:
+        decode_domain_name(rdata, rdlength, rdata_str, p);
+        break;
+
+    case DNS_RR_TYPE_MX:
+        decode_mx(rdata, rdlength, rdata_str, p);
+        break;
+
+    case DNS_RR_TYPE_RRSIG:
+        decode_rrsig(rdata, rdlength, rdata_str);
+        break;
+
+    case DNS_RR_TYPE_NSEC:
+        decode_nsec(rdata, rdlength, rdata_str, resp_query);
+        break;
+
+    case DNS_RR_TYPE_DS:
+        decode_ds(rdata, rdlength, rdata_str);
+        break;
+
+    default:
+        break;
+    }
+}
+
+void DNSData::get_rr_data(const Packet *p, const std::vector<uint16_t>& tabs,
+    std::string& rrs, std::string* ttls) const
+{
+    static const std::string item_sep = " ";    // list item separator
+    static const unsigned RDATA_OFFSET = 10;
+    for (auto tab : tabs)
+    {
+        if (tab + RDATA_OFFSET >= p->dsize)
+            continue;
+
+        auto rdata = p->data + tab;
+
+        uint16_t type = (rdata[0] << 8) | rdata[1];
+        uint32_t ttl = (rdata[4] << 24) | (rdata[5] << 16) | (rdata[6] << 8) | rdata[7];
+
+        uint16_t rdlength = (rdata[8] << 8) | rdata[9];
+
+        std::string rdata_str;
+        decode_rdata(p, rdata + RDATA_OFFSET, rdlength, type, rdata_str);
+
+        if (rdata_str.empty())
+            continue;
+
+        if (!rrs.empty())
+        {
+            rrs.append(item_sep);
+            if (ttls)
+                ttls->append(item_sep);
+        }
+        rrs.append(rdata_str);
+        if (ttls)
+            ttls->append(std::to_string(ttl));
+    }
+}