]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: add Lua parsers for A, AAAA and CNAME records
authorEnsar Sarajčić <dev@ensarsarajcic.com>
Wed, 30 Jul 2025 09:24:35 +0000 (11:24 +0200)
committerEnsar Sarajčić <dev@ensarsarajcic.com>
Wed, 30 Jul 2025 09:24:35 +0000 (11:24 +0200)
Adds 4 new Lua functions:
- parseARecord
- parseAAAARecord
- parseAddressRecord (supports both A and AAAA)
- parseCNAMERecord

This should make it easier to parse out data in Lua actions, while still
not doing complete response parsing.

Related: #6759

pdns/dnsdistdist/dnsdist-dnsparser.cc
pdns/dnsdistdist/dnsdist-dnsparser.hh
pdns/dnsdistdist/dnsdist-lua-bindings-dnsparser.cc
pdns/dnsdistdist/dnsdist-lua-ffi-interface.h
pdns/dnsdistdist/dnsdist-lua-ffi.cc
pdns/dnsdistdist/test-dnsdist-dnsparser.cc
regression-tests.dnsdist/test_DNSParser.py

index 2e3ce153184c015c95f58bdb9d69f2a84c868940..cfcd5ce86d8dd44aa9e6693d0501ea6602a44952 100644 (file)
@@ -21,6 +21,7 @@
  */
 #include "dnsdist-dnsparser.hh"
 #include "dnsparser.hh"
+#include "iputils.hh"
 
 namespace dnsdist
 {
@@ -238,6 +239,54 @@ namespace PacketMangling
 
 }
 
+namespace RecordParsers
+{
+  std::optional<ComboAddress> parseARecord(const std::string_view& packet, const DNSPacketOverlay::Record& record)
+  {
+    if (record.d_type != QType::A || record.d_contentLength != 4) {
+      return {};
+    }
+
+    // NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage): length is passed in and used to read data
+    return makeComboAddressFromRaw(4, packet.substr(record.d_contentOffset, record.d_contentOffset + 4).data(), record.d_contentLength);
+  }
+
+  std::optional<ComboAddress> parseAAAARecord(const std::string_view& packet, const DNSPacketOverlay::Record& record)
+  {
+    if (record.d_type != QType::AAAA || record.d_contentLength != 16) {
+      return {};
+    }
+
+    // NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage): length is passed in and used to read data
+    return makeComboAddressFromRaw(6, packet.substr(record.d_contentOffset, record.d_contentOffset + 16).data(), record.d_contentLength);
+  }
+
+  std::optional<ComboAddress> parseAddressRecord(const std::string_view& packet, const DNSPacketOverlay::Record& record)
+  {
+    if (record.d_type == QType::A && record.d_contentLength == 4) {
+      // NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage): length is passed in and used to read data
+      return makeComboAddressFromRaw(4, packet.substr(record.d_contentOffset, record.d_contentOffset + 4).data(), record.d_contentLength);
+    }
+
+    if (record.d_type == QType::AAAA && record.d_contentLength == 16) {
+      // NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage): length is passed in and used to read data
+      return makeComboAddressFromRaw(6, packet.substr(record.d_contentOffset, record.d_contentOffset + 16).data(), record.d_contentLength);
+    }
+
+    return {};
+  }
+
+  std::optional<DNSName> parseCNAMERecord(const std::string_view& packet, const DNSPacketOverlay::Record& record)
+  {
+    if (record.d_type != QType::CNAME) {
+      return {};
+    }
+
+    // NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage): length is passed in and used to read data
+    return DNSName(packet.data(), record.d_contentOffset + record.d_contentLength, record.d_contentOffset, true);
+  }
+}
+
 void setResponseHeadersFromConfig(dnsheader& dnsheader, const ResponseConfig& config)
 {
   if (config.setAA) {
index cbb0ff79ea808dbb0d8f1aea75ee84989592c99c..e9bb9691cd9b4c79c47d01d1c751ddc275deae9b 100644 (file)
@@ -22,6 +22,7 @@
 #pragma once
 
 #include "dnsparser.hh"
+#include "iputils.hh"
 
 namespace dnsdist
 {
@@ -62,6 +63,14 @@ namespace PacketMangling
   void restrictDNSPacketTTLs(PacketBuffer& packet, uint32_t minimumValue, uint32_t maximumValue = std::numeric_limits<uint32_t>::max(), const std::unordered_set<QType>& types = {});
 }
 
+namespace RecordParsers
+{
+  std::optional<ComboAddress> parseARecord(const std::string_view& packet, const DNSPacketOverlay::Record& record);
+  std::optional<ComboAddress> parseAAAARecord(const std::string_view& packet, const DNSPacketOverlay::Record& record);
+  std::optional<ComboAddress> parseAddressRecord(const std::string_view& packet, const DNSPacketOverlay::Record& record);
+  std::optional<DNSName> parseCNAMERecord(const std::string_view& packet, const DNSPacketOverlay::Record& record);
+}
+
 struct ResponseConfig
 {
   boost::optional<bool> setAA{boost::none};
index 96053144904f174048b6660fd1a6b8e16d24016d..72d2835a0e403226e4be8822888fff5180ccea96 100644 (file)
@@ -62,5 +62,17 @@ void setupLuaBindingsDNSParser(LuaContext& luaCtx)
   luaCtx.registerMember<uint16_t(dnsdist::DNSPacketOverlay::Record::*)>(std::string("contentLength"), [](const dnsdist::DNSPacketOverlay::Record& record) { return record.d_contentLength; });
   luaCtx.registerMember<uint16_t(dnsdist::DNSPacketOverlay::Record::*)>(std::string("contentOffset"), [](const dnsdist::DNSPacketOverlay::Record& record) { return record.d_contentOffset; });
 
+  luaCtx.writeFunction("parseARecord", [](const std::string& packet, const dnsdist::DNSPacketOverlay::Record& record) {
+    return dnsdist::RecordParsers::parseARecord(packet, record);
+  });
+  luaCtx.writeFunction("parseAAAARecord", [](const std::string& packet, const dnsdist::DNSPacketOverlay::Record& record) {
+    return dnsdist::RecordParsers::parseAAAARecord(packet, record);
+  });
+  luaCtx.writeFunction("parseAddressRecord", [](const std::string& packet, const dnsdist::DNSPacketOverlay::Record& record) {
+    return dnsdist::RecordParsers::parseAddressRecord(packet, record);
+  });
+  luaCtx.writeFunction("parseCNAMERecord", [](const std::string& packet, const dnsdist::DNSPacketOverlay::Record& record) {
+    return dnsdist::RecordParsers::parseCNAMERecord(packet, record);
+  });
 #endif /* DISABLE_DNSPACKET_BINDINGS */
 }
index 7d5d8bee76664e15025ce10dff04e12648c1f811..dfbce9262124f3b3506a2a83784f2f2cd27b2722 100644 (file)
@@ -247,6 +247,10 @@ uint32_t dnsdist_ffi_dnspacket_get_record_ttl(const dnsdist_ffi_dnspacket_t* pac
 uint16_t dnsdist_ffi_dnspacket_get_record_content_length(const dnsdist_ffi_dnspacket_t* packet, size_t idx) __attribute__ ((visibility ("default")));
 uint16_t dnsdist_ffi_dnspacket_get_record_content_offset(const dnsdist_ffi_dnspacket_t* packet, size_t idx) __attribute__ ((visibility ("default")));
 size_t dnsdist_ffi_dnspacket_get_name_at_offset_raw(const char* packet, size_t packetSize, size_t offset, char* name, size_t nameSize) __attribute__ ((visibility ("default")));
+bool dnsdist_ffi_dnspacket_parse_a_record(const char* raw, const dnsdist_ffi_dnspacket_t* packet, size_t idx, char* addr, size_t* addrSize) __attribute__ ((visibility ("default")));
+bool dnsdist_ffi_dnspacket_parse_aaaa_record(const char* raw, const dnsdist_ffi_dnspacket_t* packet, size_t idx, char* addr, size_t* addrSize) __attribute__ ((visibility ("default")));
+bool dnsdist_ffi_dnspacket_parse_address_record(const char* raw, const dnsdist_ffi_dnspacket_t* packet, size_t idx, char* addr, size_t* addrSize) __attribute__ ((visibility ("default")));
+bool dnsdist_ffi_dnspacket_parse_cname_record(const char* raw, const dnsdist_ffi_dnspacket_t* packet, size_t idx, char* name, size_t* nameSize) __attribute__ ((visibility ("default")));
 void dnsdist_ffi_dnspacket_free(dnsdist_ffi_dnspacket_t*) __attribute__ ((visibility ("default")));
 
 bool dnsdist_ffi_metric_declare(const char* name, size_t nameLen, const char* type, const char* description, const char* customName) __attribute__ ((visibility ("default")));
index 0ffa484e84b7d2d5847241d8e621eb9cfe3ce99d..f6df4a0e655d3111f42c46fa104760c5b4797f4a 100644 (file)
@@ -1851,6 +1851,87 @@ size_t dnsdist_ffi_dnspacket_get_name_at_offset_raw(const char* packet, size_t p
   return 0;
 }
 
+bool dnsdist_ffi_dnspacket_parse_a_record(const char* raw, const dnsdist_ffi_dnspacket_t* packet, size_t idx, char* addr, size_t* addrSize)
+{
+  if (raw == nullptr || packet == nullptr || addr == nullptr || addrSize == nullptr || idx >= packet->overlay.d_records.size()) {
+    return false;
+  }
+
+  auto record = packet->overlay.d_records.at(idx);
+  if (record.d_type != QType::A || record.d_contentLength != 4) {
+    return false;
+  }
+
+  // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic): this is a C API
+  memcpy(addr, &raw[record.d_contentOffset], 4);
+  *addrSize = record.d_contentLength;
+
+  return true;
+}
+
+bool dnsdist_ffi_dnspacket_parse_aaaa_record(const char* raw, const dnsdist_ffi_dnspacket_t* packet, size_t idx, char* addr, size_t* addrSize)
+{
+  if (raw == nullptr || packet == nullptr || addr == nullptr || addrSize == nullptr || idx >= packet->overlay.d_records.size()) {
+    return false;
+  }
+
+  auto record = packet->overlay.d_records.at(idx);
+  if (record.d_type != QType::AAAA || record.d_contentLength != 16) {
+    return false;
+  }
+
+  // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic): this is a C API
+  memcpy(addr, &raw[record.d_contentOffset], 16);
+  *addrSize = record.d_contentLength;
+
+  return true;
+}
+
+bool dnsdist_ffi_dnspacket_parse_address_record(const char* raw, const dnsdist_ffi_dnspacket_t* packet, size_t idx, char* addr, size_t* addrSize)
+{
+  if (raw == nullptr || packet == nullptr || addr == nullptr || addrSize == nullptr || idx >= packet->overlay.d_records.size()) {
+    return false;
+  }
+
+  auto record = packet->overlay.d_records.at(idx);
+  if (record.d_type == QType::A && record.d_contentLength == 4) {
+    // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic): this is a C API
+    memcpy(addr, &raw[record.d_contentOffset], 4);
+    *addrSize = record.d_contentLength;
+
+    return true;
+  }
+
+  if (record.d_type == QType::AAAA && record.d_contentLength == 16) {
+    // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic): this is a C API
+    memcpy(addr, &raw[record.d_contentOffset], 16);
+    *addrSize = record.d_contentLength;
+
+    return true;
+  }
+
+  return false;
+}
+
+bool dnsdist_ffi_dnspacket_parse_cname_record(const char* raw, const dnsdist_ffi_dnspacket_t* packet, size_t idx, char* name, size_t* nameSize)
+{
+  if (raw == nullptr || packet == nullptr || name == nullptr || nameSize == nullptr || idx >= packet->overlay.d_records.size()) {
+    return false;
+  }
+
+  auto record = packet->overlay.d_records.at(idx);
+  if (record.d_type != QType::CNAME) {
+    return false;
+  }
+
+  DNSName parsed(raw, record.d_contentOffset + record.d_contentLength, record.d_contentOffset, true);
+  const auto& storage = parsed.getStorage();
+  memcpy(name, storage.data(), storage.size());
+  *nameSize = storage.size();
+
+  return true;
+}
+
 void dnsdist_ffi_dnspacket_free(dnsdist_ffi_dnspacket_t* packet)
 {
   if (packet != nullptr) {
index caf5d42c5b90142c67bad557e09051a28ab02558..6b5397ce784f389869c192c3f7d5007e742d0761 100644 (file)
@@ -386,6 +386,7 @@ BOOST_AUTO_TEST_CASE(test_Response)
 BOOST_AUTO_TEST_CASE(test_Overlay)
 {
   const DNSName target("powerdns.com.");
+  const DNSName notTheTarget("not-powerdns.com.");
 
   {
     PacketBuffer response;
@@ -460,6 +461,56 @@ BOOST_AUTO_TEST_CASE(test_Overlay)
       lastOffset = record.d_contentOffset + record.d_contentLength;
     }
   }
+
+  {
+    /* response with A and AAAA records, using parsers */
+    PacketBuffer response;
+    GenericDNSPacketWriter<PacketBuffer> pwR(response, target, QType::A, QClass::IN, 0);
+    pwR.getHeader()->qr = 1;
+    pwR.getHeader()->rd = 1;
+    pwR.getHeader()->ra = 1;
+    pwR.getHeader()->id = htons(42);
+    pwR.startRecord(target, QType::A, 7200, QClass::IN, DNSResourceRecord::ANSWER);
+    ComboAddress v4("192.0.2.1");
+    pwR.xfrCAWithoutPort(4, v4);
+    pwR.commit();
+    pwR.startRecord(target, QType::AAAA, 7200, QClass::IN, DNSResourceRecord::ADDITIONAL);
+    ComboAddress v6("2001:db8::1");
+    pwR.xfrCAWithoutPort(6, v6);
+    pwR.commit();
+    pwR.addOpt(4096, 0, 0);
+    pwR.commit();
+
+    auto packet = std::string_view(reinterpret_cast<const char*>(response.data()), response.size());
+    dnsdist::DNSPacketOverlay overlay(packet);
+    BOOST_CHECK_EQUAL(overlay.d_records[0].d_type, QType::A);
+    BOOST_CHECK(*dnsdist::RecordParsers::parseARecord(packet, overlay.d_records[0]) == v4);
+    BOOST_CHECK(*dnsdist::RecordParsers::parseAddressRecord(packet, overlay.d_records[0]) == v4);
+
+    BOOST_CHECK_EQUAL(overlay.d_records[1].d_type, QType::AAAA);
+    BOOST_CHECK(*dnsdist::RecordParsers::parseAAAARecord(packet, overlay.d_records[1]) == v6);
+    BOOST_CHECK(*dnsdist::RecordParsers::parseAddressRecord(packet, overlay.d_records[1]) == v6);
+  }
+
+  {
+    /* response with CNAME record, using parser */
+    PacketBuffer response;
+    GenericDNSPacketWriter<PacketBuffer> pwR(response, target, QType::A, QClass::IN, 0);
+    pwR.getHeader()->qr = 1;
+    pwR.getHeader()->rd = 1;
+    pwR.getHeader()->ra = 1;
+    pwR.getHeader()->id = htons(42);
+    pwR.startRecord(target, QType::CNAME, 7200, QClass::IN, DNSResourceRecord::ANSWER);
+    pwR.xfrName(notTheTarget);
+    pwR.commit();
+    pwR.addOpt(4096, 0, 0);
+    pwR.commit();
+
+    auto packet = std::string_view(reinterpret_cast<const char*>(response.data()), response.size());
+    dnsdist::DNSPacketOverlay overlay(packet);
+    BOOST_CHECK_EQUAL(overlay.d_records[0].d_type, QType::CNAME);
+    BOOST_CHECK_EQUAL(*dnsdist::RecordParsers::parseCNAMERecord(packet, overlay.d_records[0]), notTheTarget);
+  }
 }
 
 BOOST_AUTO_TEST_SUITE_END();
index 1a14821aab3286e5b15526ee054e95fcb29050eb..77796bb671add5853351188ff8f7b6ae0adaf9d2 100644 (file)
@@ -144,3 +144,109 @@ class TestDNSParser(DNSDistTest):
             receivedQuery.id = query.id
             self.assertEqual(query, receivedQuery)
             self.assertEqual(receivedResponse, response)
+
+
+class TestDNSRecordParser(DNSDistTest):
+
+    _verboseMode = True
+    _config_template = """
+  function checkResponsePacket(dq)
+    local packet = dq:getContent()
+    local overlay = newDNSPacketOverlay(packet)
+    local count = overlay:getRecordsCountInSection(DNSSection.Answer)
+    for i = 0, count - 1 do
+      local record = overlay:getRecord(i)
+      local parsedAsA = parseARecord(packet, record)
+      local parsedAsAAAA = parseAAAARecord(packet, record)
+      local parsedAsAddress = parseAddressRecord(packet, record)
+      local parsedAsCNAME = parseCNAMERecord(packet, record)
+      if record.type == DNSQType.A then
+        if parsedAsA:toString() ~= "192.0.2.1" then
+          print(parsedAsA:toString()..".invalid.parsed.a.record.")
+          return DNSResponseAction.ServFail
+        end
+        if parsedAsAddress:toString() ~= "192.0.2.1" then
+          print(parsedAsAddress:toString()..".invalid.parsed.a.record. as address")
+          return DNSResponseAction.ServFail
+        end
+      else
+        if parsedAsA then
+          print("Unexpected A parse success")
+          return DNSResponseAction.ServFail
+        end
+      end
+
+      if record.type == DNSQType.AAAA then
+        if parsedAsAAAA:toString() ~= "ff:db8::ffff" then
+          print(parsedAsAAAA:toString()..".invalid.parsed.aaaa.record.")
+          return DNSResponseAction.ServFail
+        end
+        if parsedAsAddress:toString() ~= "ff:db8::ffff" then
+          print(parsedAsAddress:toString()..".invalid.parsed.aaaa.record. as address")
+          return DNSResponseAction.ServFail
+        end
+      else
+        if parsedAsAAAA then
+          print("Unexpected AAAA parse success")
+          return DNSResponseAction.ServFail
+        end
+      end
+
+      if record.type == DNSQType.CNAME then
+        if parsedAsCNAME:toString() ~= "not-powerdns.com." then
+          print(parsedAsCNAME:toString()..".invalid.parsed.cname.record.")
+          return DNSResponseAction.ServFail
+        end
+        if parsedAsAddress then
+          print("Unexpected address parse success")
+          return DNSResponseAction.ServFail
+        end
+      else
+        if parsedAsCNAME then
+          print("Unexpected CNAME parse success")
+          return DNSResponseAction.ServFail
+        end
+      end
+    end
+    return DNSAction.None
+  end
+
+  addResponseAction(AllRule(), LuaResponseAction(checkResponsePacket))
+  newServer{address="127.0.0.1:%s"}
+    """
+
+    def testQuestionAndResponseWithParsers(self):
+        """
+        DNS Parser: parsers checks
+        """
+        name = 'powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN', use_edns=True)
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    3600,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.A,
+                                    '192.0.2.1')
+        response.answer.append(rrset)
+        rrset = dns.rrset.from_text(name,
+                                    3600,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.AAAA,
+                                    'ff:db8::ffff')
+        response.answer.append(rrset)
+        rrset = dns.rrset.from_text(name,
+                                    3600,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.CNAME,
+                                    'not-powerdns.com.')
+        response.answer.append(rrset)
+
+        for method in ("sendUDPQuery", "sendTCPQuery"):
+            sender = getattr(self, method)
+            (receivedQuery, receivedResponse) = sender(query, response)
+            print(receivedResponse)
+            self.assertTrue(receivedQuery)
+            self.assertTrue(receivedResponse)
+            receivedQuery.id = query.id
+            self.assertEqual(query, receivedQuery)
+            self.assertEqual(receivedResponse, response)