From b6f9a21db93ee25ec665dc5f65e87eb7adebd102 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Ensar=20Saraj=C4=8Di=C4=87?= Date: Wed, 30 Jul 2025 11:24:35 +0200 Subject: [PATCH] dnsdist: add Lua parsers for A, AAAA and CNAME records 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 | 49 ++++++++ pdns/dnsdistdist/dnsdist-dnsparser.hh | 9 ++ .../dnsdist-lua-bindings-dnsparser.cc | 12 ++ pdns/dnsdistdist/dnsdist-lua-ffi-interface.h | 4 + pdns/dnsdistdist/dnsdist-lua-ffi.cc | 81 +++++++++++++ pdns/dnsdistdist/test-dnsdist-dnsparser.cc | 51 +++++++++ regression-tests.dnsdist/test_DNSParser.py | 106 ++++++++++++++++++ 7 files changed, 312 insertions(+) diff --git a/pdns/dnsdistdist/dnsdist-dnsparser.cc b/pdns/dnsdistdist/dnsdist-dnsparser.cc index 2e3ce15318..cfcd5ce86d 100644 --- a/pdns/dnsdistdist/dnsdist-dnsparser.cc +++ b/pdns/dnsdistdist/dnsdist-dnsparser.cc @@ -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 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 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 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 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) { diff --git a/pdns/dnsdistdist/dnsdist-dnsparser.hh b/pdns/dnsdistdist/dnsdist-dnsparser.hh index cbb0ff79ea..e9bb9691cd 100644 --- a/pdns/dnsdistdist/dnsdist-dnsparser.hh +++ b/pdns/dnsdistdist/dnsdist-dnsparser.hh @@ -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::max(), const std::unordered_set& types = {}); } +namespace RecordParsers +{ + std::optional parseARecord(const std::string_view& packet, const DNSPacketOverlay::Record& record); + std::optional parseAAAARecord(const std::string_view& packet, const DNSPacketOverlay::Record& record); + std::optional parseAddressRecord(const std::string_view& packet, const DNSPacketOverlay::Record& record); + std::optional parseCNAMERecord(const std::string_view& packet, const DNSPacketOverlay::Record& record); +} + struct ResponseConfig { boost::optional setAA{boost::none}; diff --git a/pdns/dnsdistdist/dnsdist-lua-bindings-dnsparser.cc b/pdns/dnsdistdist/dnsdist-lua-bindings-dnsparser.cc index 9605314490..72d2835a0e 100644 --- a/pdns/dnsdistdist/dnsdist-lua-bindings-dnsparser.cc +++ b/pdns/dnsdistdist/dnsdist-lua-bindings-dnsparser.cc @@ -62,5 +62,17 @@ void setupLuaBindingsDNSParser(LuaContext& luaCtx) luaCtx.registerMember(std::string("contentLength"), [](const dnsdist::DNSPacketOverlay::Record& record) { return record.d_contentLength; }); luaCtx.registerMember(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 */ } diff --git a/pdns/dnsdistdist/dnsdist-lua-ffi-interface.h b/pdns/dnsdistdist/dnsdist-lua-ffi-interface.h index 7d5d8bee76..dfbce92621 100644 --- a/pdns/dnsdistdist/dnsdist-lua-ffi-interface.h +++ b/pdns/dnsdistdist/dnsdist-lua-ffi-interface.h @@ -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"))); diff --git a/pdns/dnsdistdist/dnsdist-lua-ffi.cc b/pdns/dnsdistdist/dnsdist-lua-ffi.cc index 0ffa484e84..f6df4a0e65 100644 --- a/pdns/dnsdistdist/dnsdist-lua-ffi.cc +++ b/pdns/dnsdistdist/dnsdist-lua-ffi.cc @@ -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) { diff --git a/pdns/dnsdistdist/test-dnsdist-dnsparser.cc b/pdns/dnsdistdist/test-dnsdist-dnsparser.cc index caf5d42c5b..6b5397ce78 100644 --- a/pdns/dnsdistdist/test-dnsdist-dnsparser.cc +++ b/pdns/dnsdistdist/test-dnsdist-dnsparser.cc @@ -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 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(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 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(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(); diff --git a/regression-tests.dnsdist/test_DNSParser.py b/regression-tests.dnsdist/test_DNSParser.py index 1a14821aab..77796bb671 100644 --- a/regression-tests.dnsdist/test_DNSParser.py +++ b/regression-tests.dnsdist/test_DNSParser.py @@ -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) -- 2.47.3