From 9678b9fab6bfcedf1820a24ed9265f2987724a72 Mon Sep 17 00:00:00 2001 From: Remi Gacogne Date: Tue, 16 Dec 2025 17:48:54 +0100 Subject: [PATCH] dnsdist: Handle escaped values in YAML SpoofRaw parameters Signed-off-by: Remi Gacogne --- .../dnsdist-actions-definitions.yml | 14 +- .../dnsdistdist/dnsdist-configuration-yaml.cc | 30 +- pdns/dnsdistdist/dnsdist-rules.cc | 2 +- regression-tests.dnsdist/test_Spoofing.py | 260 ++++++++++++++++-- 4 files changed, 263 insertions(+), 43 deletions(-) diff --git a/pdns/dnsdistdist/dnsdist-actions-definitions.yml b/pdns/dnsdistdist/dnsdist-actions-definitions.yml index a9ea1c3ca6..eb78216e1a 100644 --- a/pdns/dnsdistdist/dnsdist-actions-definitions.yml +++ b/pdns/dnsdistdist/dnsdist-actions-definitions.yml @@ -459,19 +459,7 @@ are processed after this action" description: "The length of the DNS packet" - name: "SpoofRaw" description: | - Forge a response with the specified raw bytes as record data - .. code-block:: Lua - - -- select queries for the 'raw.powerdns.com.' name and TXT type, and answer with both a "aaa" "bbbb" and "ccc" TXT record: - addAction(AndRule({QNameRule('raw.powerdns.com.'), QTypeRule(DNSQType.TXT)}), SpoofRawAction({"\003aaa\004bbbb", "\003ccc"})) - -- select queries for the 'raw-srv.powerdns.com.' name and SRV type, and answer with a '0 0 65535 srv.powerdns.com.' SRV record, setting the AA bit to 1 and the TTL to 3600s - addAction(AndRule({QNameRule('raw-srv.powerdns.com.'), QTypeRule(DNSQType.SRV)}), SpoofRawAction("\000\000\000\000\255\255\003srv\008powerdns\003com\000", { aa=true, ttl=3600 })) - -- select reverse queries for '127.0.0.1' and answer with 'localhost' - addAction(AndRule({QNameRule('1.0.0.127.in-addr.arpa.'), QTypeRule(DNSQType.PTR)}), SpoofRawAction("\009localhost\000")) - -- rfc8482: Providing Minimal-Sized Responses to DNS Queries That Have QTYPE=ANY via HINFO of value "rfc8482" - addAction(QTypeRule(DNSQType.ANY), SpoofRawAction("\007rfc\056\052\056\050\000", { typeForAny=DNSQType.HINFO })) - - :func:`DNSName:toDNSString` is convenient for converting names to wire format for passing to ``SpoofRawAction``. + Forge a response with the specified raw bytes as record data. Non-character values should be encoded in the ``\DDD`` format where ``DDD`` is the decimal value. For example to wire content of an A record containing ``1.2.3.4`` should be encoded as ``\001\002\003\004``. ``sdig dumpluaraw`` and ``pdnsutil raw-lua-from-content`` from PowerDNS can generate raw answers for you: diff --git a/pdns/dnsdistdist/dnsdist-configuration-yaml.cc b/pdns/dnsdistdist/dnsdist-configuration-yaml.cc index 112de6227c..09af41a71f 100644 --- a/pdns/dnsdistdist/dnsdist-configuration-yaml.cc +++ b/pdns/dnsdistdist/dnsdist-configuration-yaml.cc @@ -106,7 +106,7 @@ template static T checkedConversionFromStr(const std::string& context, const std::string& parameterName, const std::string& str) { try { - return pdns::checked_stoi(std::string(str)); + return pdns::checked_stoi(str); } catch (const std::exception& exp) { throw std::runtime_error("Error converting value '" + str + "' for parameter '" + parameterName + "' in YAML directive '" + context + "': " + exp.what()); @@ -131,6 +131,30 @@ static bool getOptionalLuaFunction(T& destination, const ::rust::string& functio return true; } +static std::string rustStringWithEscapedRawContentToString(const ::rust::String& sourceRust) +{ + const std::string source(sourceRust); + std::string destination; + destination.reserve(source.size()); + + auto start = source.begin(); + auto position = std::find(start, source.end(), '\\'); + while (position < source.end() && std::distance(position, source.end()) >= 4) { + destination.insert(destination.end(), start, position); + start = position + 4; + auto escaped = std::string(position + 1, position + 4); + auto code = checkedConversionFromStr("SpoofRaw", "answers", escaped); + destination.insert(destination.end(), static_cast(code)); + position = std::find(start, source.end(), '\\'); + } + + if (start < source.end()) { + destination.insert(destination.end(), start, source.end()); + } + + return destination; +} + static uint8_t strToRCode(const std::string& context, const std::string& parameterName, const ::rust::String& rcode_rust_string) { auto rcode_str = std::string(rcode_rust_string); @@ -446,7 +470,7 @@ static std::shared_ptr createBackendFromConfiguration(const dns } backendConfig.checkType = std::string(hcConf.qtype); if (!hcConf.qclass.empty()) { - backendConfig.checkClass = QClass(std::string(hcConf.qclass)); + backendConfig.checkClass = QClass(boost::to_upper_copy(std::string(hcConf.qclass))); } backendConfig.checkTimeout = hcConf.timeout; backendConfig.d_tcpCheck = hcConf.use_tcp; @@ -1463,7 +1487,7 @@ std::shared_ptr getSpoofRawAction(const SpoofRawActionConfigur { std::vector raws; for (const auto& answer : config.answers) { - raws.emplace_back(answer); + raws.emplace_back(dnsdist::configuration::yaml::rustStringWithEscapedRawContentToString(answer)); } std::optional qtypeForAny; if (!config.qtype_for_any.empty()) { diff --git a/pdns/dnsdistdist/dnsdist-rules.cc b/pdns/dnsdistdist/dnsdist-rules.cc index 97b31bf9b9..61536e567e 100644 --- a/pdns/dnsdistdist/dnsdist-rules.cc +++ b/pdns/dnsdistdist/dnsdist-rules.cc @@ -143,7 +143,7 @@ std::shared_ptr getQClassSelector(const std::string& qclassStr, uint { QClass qclass(qclassCode); if (!qclassStr.empty()) { - qclass = QClass(std::string(qclassStr)); + qclass = QClass(boost::to_upper_copy(std::string(qclassStr))); } return std::make_shared(qclass); diff --git a/regression-tests.dnsdist/test_Spoofing.py b/regression-tests.dnsdist/test_Spoofing.py index a749272b1a..332615922a 100644 --- a/regression-tests.dnsdist/test_Spoofing.py +++ b/regression-tests.dnsdist/test_Spoofing.py @@ -2,27 +2,7 @@ import dns from dnsdisttests import DNSDistTest -class TestSpoofingSpoof(DNSDistTest): - - _config_template = """ - addAction(SuffixMatchNodeRule("spoofaction.spoofing.tests.powerdns.com."), SpoofAction({"192.0.2.1", "2001:DB8::1"})) - addAction(SuffixMatchNodeRule("spoofaction-aa.spoofing.tests.powerdns.com."), SpoofAction({"192.0.2.1", "2001:DB8::1"}, {aa=true})) - addAction(SuffixMatchNodeRule("spoofaction-ad.spoofing.tests.powerdns.com."), SpoofAction({"192.0.2.1", "2001:DB8::1"}, {ad=true})) - addAction(SuffixMatchNodeRule("spoofaction-ra.spoofing.tests.powerdns.com."), SpoofAction({"192.0.2.1", "2001:DB8::1"}, {ra=true})) - addAction(SuffixMatchNodeRule("spoofaction-nora.spoofing.tests.powerdns.com."), SpoofAction({"192.0.2.1", "2001:DB8::1"}, {ra=false})) - addAction(SuffixMatchNodeRule("spoofaction-ttl.spoofing.tests.powerdns.com."), SpoofAction({"192.0.2.1", "2001:DB8::1"}, {ttl=1500})) - addAction(SuffixMatchNodeRule("cnamespoofaction.spoofing.tests.powerdns.com."), SpoofCNAMEAction("cnameaction.spoofing.tests.powerdns.com.")) - addAction("multispoof.spoofing.tests.powerdns.com", SpoofAction({"192.0.2.1", "192.0.2.2", "2001:DB8::1", "2001:DB8::2"})) - addAction(AndRule{SuffixMatchNodeRule("raw.spoofing.tests.powerdns.com"), QTypeRule(DNSQType.A)}, SpoofRawAction("\\192\\000\\002\\001")) - addAction(AndRule{SuffixMatchNodeRule("raw.spoofing.tests.powerdns.com"), QTypeRule(DNSQType.TXT)}, SpoofRawAction("\\003aaa\\004bbbb\\011ccccccccccc")) - addAction(AndRule{SuffixMatchNodeRule("raw.spoofing.tests.powerdns.com"), QTypeRule(DNSQType.SRV)}, SpoofRawAction("\\000\\000\\000\\000\\255\\255\\003srv\\008powerdns\\003com\\000", { aa=true, ttl=3600 })) - addAction(AndRule{SuffixMatchNodeRule("rawchaos.spoofing.tests.powerdns.com"), QTypeRule(DNSQType.TXT), QClassRule(DNSClass.CHAOS)}, SpoofRawAction("\\005chaos")) - addAction(AndRule{SuffixMatchNodeRule("multiraw.spoofing.tests.powerdns.com"), QTypeRule(DNSQType.TXT)}, SpoofRawAction({"\\003aaa\\004bbbb", "\\011ccccccccccc"})) - addAction(AndRule{SuffixMatchNodeRule("multiraw.spoofing.tests.powerdns.com"), QTypeRule(DNSQType.A)}, SpoofRawAction({"\\192\\000\\002\\001", "\\192\\000\\002\\002"})) - -- rfc8482 - addAction(AndRule{SuffixMatchNodeRule("raw-any.spoofing.tests.powerdns.com"), QTypeRule(DNSQType.ANY)}, SpoofRawAction("\\007rfc\\056\\052\\056\\050\\000", { typeForAny=DNSQType.HINFO })) - newServer{address="127.0.0.1:%d"} - """ +class SpoofingTests(object): def testSpoofActionA(self): """ @@ -311,7 +291,7 @@ class TestSpoofingSpoof(DNSDistTest): expectedResponse = dns.message.make_response(query) expectedResponse.flags |= dns.flags.RA rrset = dns.rrset.from_text(name, - 60, + 1500, dns.rdataclass.IN, dns.rdatatype.AAAA, '2001:DB8::1') @@ -423,7 +403,7 @@ class TestSpoofingSpoof(DNSDistTest): 60, dns.rdataclass.CH, dns.rdatatype.TXT, - '"chaos"') + '"chaos\\\\test"') expectedResponse.answer.append(rrset) for method in ("sendUDPQuery", "sendTCPQuery"): @@ -501,6 +481,237 @@ class TestSpoofingSpoof(DNSDistTest): self.checkMessageNoEDNS(expectedResponse, receivedResponse) self.assertEqual(receivedResponse.answer[0].ttl, 60) +class TestSpoofingViaLuaConfig(DNSDistTest, SpoofingTests): + + _config_template = """ + addAction(SuffixMatchNodeRule("spoofaction.spoofing.tests.powerdns.com."), SpoofAction({"192.0.2.1", "2001:DB8::1"})) + addAction(SuffixMatchNodeRule("spoofaction-aa.spoofing.tests.powerdns.com."), SpoofAction({"192.0.2.1", "2001:DB8::1"}, {aa=true})) + addAction(SuffixMatchNodeRule("spoofaction-ad.spoofing.tests.powerdns.com."), SpoofAction({"192.0.2.1", "2001:DB8::1"}, {ad=true})) + addAction(SuffixMatchNodeRule("spoofaction-ra.spoofing.tests.powerdns.com."), SpoofAction({"192.0.2.1", "2001:DB8::1"}, {ra=true})) + addAction(SuffixMatchNodeRule("spoofaction-nora.spoofing.tests.powerdns.com."), SpoofAction({"192.0.2.1", "2001:DB8::1"}, {ra=false})) + addAction(SuffixMatchNodeRule("spoofaction-ttl.spoofing.tests.powerdns.com."), SpoofAction({"192.0.2.1", "2001:DB8::1"}, {ttl=1500})) + addAction(SuffixMatchNodeRule("cnamespoofaction.spoofing.tests.powerdns.com."), SpoofCNAMEAction("cnameaction.spoofing.tests.powerdns.com.")) + addAction("multispoof.spoofing.tests.powerdns.com", SpoofAction({"192.0.2.1", "192.0.2.2", "2001:DB8::1", "2001:DB8::2"})) + addAction(AndRule{SuffixMatchNodeRule("raw.spoofing.tests.powerdns.com"), QTypeRule(DNSQType.A)}, SpoofRawAction("\\192\\000\\002\\001")) + addAction(AndRule{SuffixMatchNodeRule("raw.spoofing.tests.powerdns.com"), QTypeRule(DNSQType.TXT)}, SpoofRawAction("\\003aaa\\004bbbb\\011ccccccccccc")) + addAction(AndRule{SuffixMatchNodeRule("raw.spoofing.tests.powerdns.com"), QTypeRule(DNSQType.SRV)}, SpoofRawAction("\\000\\000\\000\\000\\255\\255\\003srv\\008powerdns\\003com\\000", { aa=true, ttl=3600 })) + addAction(AndRule{SuffixMatchNodeRule("rawchaos.spoofing.tests.powerdns.com"), QTypeRule(DNSQType.TXT), QClassRule(DNSClass.CHAOS)}, SpoofRawAction("\\010chaos\\\\test")) + addAction(AndRule{SuffixMatchNodeRule("multiraw.spoofing.tests.powerdns.com"), QTypeRule(DNSQType.TXT)}, SpoofRawAction({"\\003aaa\\004bbbb", "\\011ccccccccccc"})) + addAction(AndRule{SuffixMatchNodeRule("multiraw.spoofing.tests.powerdns.com"), QTypeRule(DNSQType.A)}, SpoofRawAction({"\\192\\000\\002\\001", "\\192\\000\\002\\002"})) + -- rfc8482 + addAction(AndRule{SuffixMatchNodeRule("raw-any.spoofing.tests.powerdns.com"), QTypeRule(DNSQType.ANY)}, SpoofRawAction("\\007rfc\\056\\052\\056\\050\\000", { typeForAny=DNSQType.HINFO })) + newServer{address="127.0.0.1:%d"} + """ + +class TestSpoofingViaYamlConfig(DNSDistTest, SpoofingTests): + + _yaml_config_template = """ +backends: + - address: "127.0.0.1:%d" + protocol: Do53 + +query_rules: + - selector: + type: "QNameSuffix" + suffixes: + - "spoofaction.spoofing.tests.powerdns.com." + action: + type: "Spoof" + ips: + - 192.0.2.1 + - 2001:DB8::1 + vars: + ttl: 60 + - selector: + type: "QNameSuffix" + suffixes: + - "spoofaction-aa.spoofing.tests.powerdns.com." + action: + type: "Spoof" + ips: + - 192.0.2.1 + - 2001:DB8::1 + vars: + set_aa: true + ttl: 60 + - selector: + type: "QNameSuffix" + suffixes: + - "spoofaction-ad.spoofing.tests.powerdns.com." + action: + type: "Spoof" + ips: + - 192.0.2.1 + - 2001:DB8::1 + vars: + set_ad: true + ttl: 60 + - selector: + type: "QNameSuffix" + suffixes: + - "spoofaction-ra.spoofing.tests.powerdns.com." + action: + type: "Spoof" + ips: + - 192.0.2.1 + - 2001:DB8::1 + vars: + set_ra: true + ttl: 60 + - selector: + type: "QNameSuffix" + suffixes: + - "spoofaction-nora.spoofing.tests.powerdns.com." + action: + type: "Spoof" + ips: + - 192.0.2.1 + - 2001:DB8::1 + vars: + set_ra: false + ttl: 60 + - selector: + type: "QNameSuffix" + suffixes: + - "spoofaction-ttl.spoofing.tests.powerdns.com." + action: + type: "Spoof" + ips: + - 192.0.2.1 + - 2001:DB8::1 + vars: + set_ra: true + ttl: 1500 + - selector: + type: "QNameSuffix" + suffixes: + - "cnamespoofaction.spoofing.tests.powerdns.com." + action: + type: "SpoofCNAME" + cname: cnameaction.spoofing.tests.powerdns.com. + vars: + ttl: 60 + - selector: + type: "QNameSuffix" + suffixes: + - "multispoof.spoofing.tests.powerdns.com" + action: + type: "Spoof" + ips: + - 192.0.2.1 + - 192.0.2.2 + - 2001:DB8::1 + - 2001:DB8::2 + vars: + ttl: 60 + - selector: + type: "And" + selectors: + - type: "QNameSuffix" + suffixes: + - "raw.spoofing.tests.powerdns.com" + - type: "QType" + qtype: "A" + action: + type: "SpoofRaw" + answers: + - '\\192\\000\\002\\001' + vars: + ttl: 60 + - selector: + type: "And" + selectors: + - type: "QNameSuffix" + suffixes: + - "raw.spoofing.tests.powerdns.com" + - type: "QType" + qtype: "TXT" + action: + type: "SpoofRaw" + answers: + - '\\003aaa\\004bbbb\\011ccccccccccc' + vars: + ttl: 60 + - selector: + type: "And" + selectors: + - type: "QNameSuffix" + suffixes: + - "raw.spoofing.tests.powerdns.com" + - type: "QType" + qtype: "SRV" + action: + type: "SpoofRaw" + answers: + - '\\000\\000\\000\\000\\255\\255\\003srv\\008powerdns\\003com\\000' + vars: + set_aa: true + ttl: 3600 + - selector: + type: "And" + selectors: + - type: "QNameSuffix" + suffixes: + - "rawchaos.spoofing.tests.powerdns.com" + - type: "QType" + qtype: "TXT" + - type: "QClass" + qclass: "chaos" + action: + type: "SpoofRaw" + answers: + - '\\010chaos\\092test' + vars: + ttl: 60 + - selector: + type: "And" + selectors: + - type: "QNameSuffix" + suffixes: + - "multiraw.spoofing.tests.powerdns.com" + - type: "QType" + qtype: "TXT" + action: + type: "SpoofRaw" + answers: + - '\\003aaa\\004bbbb' + - '\\011ccccccccccc' + vars: + ttl: 60 + - selector: + type: "And" + selectors: + - type: "QNameSuffix" + suffixes: + - "multiraw.spoofing.tests.powerdns.com" + - type: "QType" + qtype: "A" + action: + type: "SpoofRaw" + answers: + - '\\192\\000\\002\\001' + - '\\192\\000\\002\\002' + vars: + ttl: 60 + - selector: + type: "And" + selectors: + - type: "QNameSuffix" + suffixes: + - "raw-any.spoofing.tests.powerdns.com" + - type: "QType" + qtype: "ANY" + action: + type: "SpoofRaw" + qtype_for_any: 'HINFO' + answers: + - '\\007rfc\\056\\052\\056\\050\\000' + vars: + ttl: 60 + """ + _yaml_config_params = ['_testServerPort'] + _config_params = [] + class TestSpoofingLuaSpoof(DNSDistTest): _config_template = """ @@ -520,9 +731,6 @@ class TestSpoofingLuaSpoof(DNSDistTest): return DNSAction.Spoof, "spoofedcname.spoofing.tests.powerdns.com." end - addAction(AndRule{SuffixMatchNodeRule("raw.spoofing.tests.powerdns.com"), QTypeRule(DNSQType.TXT)}, SpoofRawAction("\\003aaa\\004bbbb\\011ccccccccccc")) - addAction(AndRule{SuffixMatchNodeRule("raw.spoofing.tests.powerdns.com"), QTypeRule(DNSQType.SRV)}, SpoofRawAction("\\000\\000\\000\\000\\255\\255\\003srv\\008powerdns\\003com\\000", { aa=true, ttl=3600 })) - function spoofrawrule(dq) if dq.qtype == DNSQType.A then return DNSAction.SpoofRaw, "\\192\\000\\002\\001" -- 2.47.3