From: Remi Gacogne Date: Fri, 8 Mar 2024 11:26:33 +0000 (+0100) Subject: dnsdist: Add a new response chain for XFR responses X-Git-Tag: rec-5.1.0-alpha1~73^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e17659bd778fb25197881986a186f2aa8e2a7d66;p=thirdparty%2Fpdns.git dnsdist: Add a new response chain for XFR responses --- diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index addd80d075..d300b19267 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -1528,6 +1528,7 @@ xdp Xek Xeon XForwarded +XFR Xiang xorbooter xpf diff --git a/pdns/dnsdistdist/Makefile.am b/pdns/dnsdistdist/Makefile.am index 4488d46d4c..e5255a67f7 100644 --- a/pdns/dnsdistdist/Makefile.am +++ b/pdns/dnsdistdist/Makefile.am @@ -162,7 +162,7 @@ dnsdist_SOURCES = \ dnsdist-ecs.cc dnsdist-ecs.hh \ dnsdist-edns.cc dnsdist-edns.hh \ dnsdist-healthchecks.cc dnsdist-healthchecks.hh \ - dnsdist-idstate.hh \ + dnsdist-idstate.cc dnsdist-idstate.hh \ dnsdist-internal-queries.cc dnsdist-internal-queries.hh \ dnsdist-kvs.hh dnsdist-kvs.cc \ dnsdist-lbpolicies.cc dnsdist-lbpolicies.hh \ @@ -284,7 +284,7 @@ testrunner_SOURCES = \ dnsdist-dynbpf.cc dnsdist-dynbpf.hh \ dnsdist-ecs.cc dnsdist-ecs.hh \ dnsdist-edns.cc dnsdist-edns.hh \ - dnsdist-idstate.hh \ + dnsdist-idstate.cc dnsdist-idstate.hh \ dnsdist-kvs.cc dnsdist-kvs.hh \ dnsdist-lbpolicies.cc dnsdist-lbpolicies.hh \ dnsdist-lua-bindings-dnsquestion.cc \ diff --git a/pdns/dnsdistdist/dnsdist-console.cc b/pdns/dnsdistdist/dnsdist-console.cc index a175cae261..d7059c454c 100644 --- a/pdns/dnsdistdist/dnsdist-console.cc +++ b/pdns/dnsdistdist/dnsdist-console.cc @@ -495,6 +495,7 @@ const std::vector g_consoleKeywords {"addMaintenanceCallback", true, "callback", "register a function to be called as part of the maintenance hook, every second"}, {"addResponseAction", true, R"(DNS rule, DNS response action [, {uuid="UUID", name="name"}}])", "add a response rule"}, {"addSelfAnsweredResponseAction", true, R"(DNS rule, DNS response action [, {uuid="UUID", name="name"}}])", "add a self-answered response rule"}, + {"addXFRResponseAction", true, R"(DNS rule, DNS response action [, {uuid="UUID", name="name"}}])", "add a XFR response rule"}, {"addTLSLocal", true, "addr, certFile(s), keyFile(s) [,params]", "listen to incoming DNS over TLS queries on the specified address using the specified certificate (or list of) and key (or list of). The last parameter is a table"}, {"AllowAction", true, "", "let these packets go through"}, {"AllowResponseAction", true, "", "let these packets go through"}, @@ -573,10 +574,12 @@ const std::vector g_consoleKeywords {"getTopResponseRules", true, "[top]", "return the `top` response rules"}, {"getTopRules", true, "[top]", "return the `top` rules"}, {"getTopSelfAnsweredResponseRules", true, "[top]", "return the `top` self-answered response rules"}, + {"getTopXFRResponseRules", true, "[top]", "return the `top` XFR response rules"}, {"getTLSContext", true, "n", "returns the TLS context with index n"}, {"getTLSFrontend", true, "n", "returns the TLS frontend with index n"}, {"getTLSFrontendCount", true, "", "returns the number of DoT listeners"}, {"getVerbose", true, "", "get whether log messages at the verbose level will be logged"}, + {"getXFRResponseRule", true, "selector", "Return the XFR response rule corresponding to the selector, if any"}, {"grepq", true, R"(Netmask|DNS Name|100ms|{"::1", "powerdns.com", "100ms"} [, n] [,options])", "shows the last n queries and responses matching the specified client address or range (Netmask), or the specified DNS Name, or slower than 100ms"}, {"hashPassword", true, "password [, workFactor]", "Returns a hashed and salted version of the supplied password, usable with 'setWebserverConfig()'"}, {"HTTPHeaderRule", true, "name, regex", "matches DoH queries with a HTTP header 'name' whose content matches the regular expression 'regex'"}, @@ -628,6 +631,8 @@ const std::vector g_consoleKeywords {"mvRuleToTop", true, "", "move the last rule to the first position"}, {"mvSelfAnsweredResponseRule", true, "from, to", "move self-answered response rule 'from' to a position where it is in front of 'to'. 'to' can be one larger than the largest rule"}, {"mvSelfAnsweredResponseRuleToTop", true, "", "move the last self-answered response rule to the first position"}, + {"mvXFRResponseRule", true, "from, to", "move XFR response rule 'from' to a position where it is in front of 'to'. 'to' can be one larger than the largest rule"}, + {"mvXFRResponseRuleToTop", true, "", "move the last XFR response rule to the first position"}, {"NetmaskGroupRule", true, "nmg[, src]", "Matches traffic from/to the network range specified in nmg. Set the src parameter to false to match nmg against destination address instead of source address. This can be used to differentiate between clients"}, {"newBPFFilter", true, "{ipv4MaxItems=int, ipv4PinnedPath=string, ipv6MaxItems=int, ipv6PinnedPath=string, cidr4MaxItems=int, cidr4PinnedPath=string, cidr6MaxItems=int, cidr6PinnedPath=string, qnamesMaxItems=int, qnamesPinnedPath=string, external=bool}", "Return a new eBPF socket filter with specified options."}, {"newCA", true, "address", "Returns a ComboAddress based on `address`"}, @@ -688,6 +693,7 @@ const std::vector g_consoleKeywords {"rmRule", true, "id", "remove rule in position 'id', or whose uuid matches if 'id' is an UUID string, or finally whose name matches if 'id' is a string but not a valid UUID"}, {"rmSelfAnsweredResponseRule", true, "id", "remove self-answered response rule in position 'id', or whose uuid matches if 'id' is an UUID string, or finally whose name matches if 'id' is a string but not a valid UUID"}, {"rmServer", true, "id", "remove server with index 'id' or whose uuid matches if 'id' is an UUID string"}, + {"rmXFRResponseRule", true, "id", "remove XFR response rule in position 'id', or whose uuid matches if 'id' is an UUID string, or finally whose name matches if 'id' is a string but not a valid UUID"}, {"roundrobin", false, "", "Simple round robin over available servers"}, {"sendCustomTrap", true, "str", "send a custom `SNMP` trap from Lua, containing the `str` string"}, {"setACL", true, "{netmask, netmask}", "replace the ACL set with these netmasks. Use `setACL({})` to reset the list, meaning no one can use us"}, diff --git a/pdns/dnsdistdist/dnsdist-idstate.cc b/pdns/dnsdistdist/dnsdist-idstate.cc new file mode 100644 index 0000000000..395ce3ccba --- /dev/null +++ b/pdns/dnsdistdist/dnsdist-idstate.cc @@ -0,0 +1,54 @@ +/* + * This file is part of PowerDNS or dnsdist. + * Copyright -- PowerDNS.COM B.V. and its contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of version 2 of the GNU General Public License as + * published by the Free Software Foundation. + * + * In addition, for the avoidance of any doubt, permission is granted to + * link this program with OpenSSL and to (re)distribute the binaries + * produced as the result of such linking. + * + * 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. + */ + +#include "dnsdist-idstate.hh" +#include "dnsdist-doh-common.hh" +#include "doh3.hh" +#include "doq.hh" + +InternalQueryState InternalQueryState::partialCloneForXFR() const +{ + /* for XFR responses we cannot move the state from the query + because we usually have more than one response packet per query, + so we need to do a partial clone. + */ + InternalQueryState ids; + ids.qtype = qtype; + ids.qclass = qclass; + ids.qname = qname; + ids.poolName = poolName; + ids.queryRealTime = queryRealTime; + ids.protocol = protocol; + ids.subnet = subnet; + ids.origRemote = origRemote; + ids.origDest = origDest; + ids.hopRemote = hopRemote; + ids.hopLocal = hopLocal; + if (qTag) { + ids.qTag = std::make_unique(*qTag); + } + if (d_protoBufData) { + ids.d_protoBufData = std::make_unique(*d_protoBufData); + } + ids.cs = cs; + return ids; +} diff --git a/pdns/dnsdistdist/dnsdist-idstate.hh b/pdns/dnsdistdist/dnsdist-idstate.hh index f83f9b71f5..0a1e758b33 100644 --- a/pdns/dnsdistdist/dnsdist-idstate.hh +++ b/pdns/dnsdistdist/dnsdist-idstate.hh @@ -129,6 +129,8 @@ struct InternalQueryState #endif /* HAVE_XSK */ } + InternalQueryState partialCloneForXFR() const; + boost::optional subnet{boost::none}; // 40 ComboAddress origRemote; // 28 ComboAddress origDest; // 28 diff --git a/pdns/dnsdistdist/dnsdist-rule-chains.cc b/pdns/dnsdistdist/dnsdist-rule-chains.cc index fd736aeedb..62cf8435c5 100644 --- a/pdns/dnsdistdist/dnsdist-rule-chains.cc +++ b/pdns/dnsdistdist/dnsdist-rule-chains.cc @@ -29,12 +29,14 @@ GlobalStateHolder> s_respruleactions; GlobalStateHolder> s_cachehitrespruleactions; GlobalStateHolder> s_selfansweredrespruleactions; GlobalStateHolder> s_cacheInsertedRespRuleActions; +GlobalStateHolder> s_XFRRespRuleActions; static const std::vector s_responseRuleChains{ {"", "response-rules", s_respruleactions}, {"CacheHit", "cache-hit-response-rules", s_cachehitrespruleactions}, {"CacheInserted", "cache-inserted-response-rules", s_selfansweredrespruleactions}, {"SelfAnswered", "self-answered-response-rules", s_cacheInsertedRespRuleActions}, + {"XFR", "xfr-response-rules", s_XFRRespRuleActions}, }; const std::vector& getResponseRuleChains() diff --git a/pdns/dnsdistdist/dnsdist-rule-chains.hh b/pdns/dnsdistdist/dnsdist-rule-chains.hh index 1d1a93dee6..0b0ede3d14 100644 --- a/pdns/dnsdistdist/dnsdist-rule-chains.hh +++ b/pdns/dnsdistdist/dnsdist-rule-chains.hh @@ -40,7 +40,7 @@ enum class ResponseRuleChain : uint8_t CacheHitResponseRules = 1, CacheInsertedResponseRules = 2, SelfAnsweredResponseRules = 3, - ResponseRuleChainsCount = 4 + XFRResponseRules = 4, }; struct RuleAction diff --git a/pdns/dnsdistdist/dnsdist-tcp-downstream.cc b/pdns/dnsdistdist/dnsdist-tcp-downstream.cc index 684c46fcee..9473300374 100644 --- a/pdns/dnsdistdist/dnsdist-tcp-downstream.cc +++ b/pdns/dnsdistdist/dnsdist-tcp-downstream.cc @@ -675,9 +675,9 @@ IOState TCPConnectionToBackend::handleResponse(std::shared_ptrd_ds; - /* we don't move the whole IDS because we will need for the responses to come */ - response.d_idstate.qtype = it->second.d_query.d_idstate.qtype; - response.d_idstate.qname = it->second.d_query.d_idstate.qname; + const auto& queryIDS = it->second.d_query.d_idstate; + /* we don't move the whole IDS because we will need it for the responses to come */ + response.d_idstate = queryIDS.partialCloneForXFR(); DEBUGLOG("passing XFRresponse to client connection for "<second.d_query.d_xfrStarted = true; diff --git a/pdns/dnsdistdist/dnsdist-tcp-upstream.hh b/pdns/dnsdistdist/dnsdist-tcp-upstream.hh index 052d9e43c9..40ca9335a3 100644 --- a/pdns/dnsdistdist/dnsdist-tcp-upstream.hh +++ b/pdns/dnsdistdist/dnsdist-tcp-upstream.hh @@ -10,13 +10,14 @@ class TCPClientThreadData { public: TCPClientThreadData(): - localRespRuleActions(dnsdist::rules::getResponseRuleChainHolder(dnsdist::rules::ResponseRuleChain::ResponseRules).getLocal()), localCacheInsertedRespRuleActions(dnsdist::rules::getResponseRuleChainHolder(dnsdist::rules::ResponseRuleChain::CacheInsertedResponseRules).getLocal()), mplexer(std::unique_ptr(FDMultiplexer::getMultiplexerSilent())) + localRespRuleActions(dnsdist::rules::getResponseRuleChainHolder(dnsdist::rules::ResponseRuleChain::ResponseRules).getLocal()), localCacheInsertedRespRuleActions(dnsdist::rules::getResponseRuleChainHolder(dnsdist::rules::ResponseRuleChain::CacheInsertedResponseRules).getLocal()), localXFRRespRuleActions(dnsdist::rules::getResponseRuleChainHolder(dnsdist::rules::ResponseRuleChain::XFRResponseRules).getLocal()), mplexer(std::unique_ptr(FDMultiplexer::getMultiplexerSilent())) { } LocalHolders holders; LocalStateHolder> localRespRuleActions; LocalStateHolder> localCacheInsertedRespRuleActions; + LocalStateHolder> localXFRRespRuleActions; std::unique_ptr mplexer{nullptr}; pdns::channel::Receiver queryReceiver; pdns::channel::Receiver crossProtocolQueryReceiver; diff --git a/pdns/dnsdistdist/dnsdist-tcp.cc b/pdns/dnsdistdist/dnsdist-tcp.cc index e3eb68e115..da7e4e2232 100644 --- a/pdns/dnsdistdist/dnsdist-tcp.cc +++ b/pdns/dnsdistdist/dnsdist-tcp.cc @@ -28,6 +28,7 @@ #include "dnsdist-concurrent-connections.hh" #include "dnsdist-dnsparser.hh" #include "dnsdist-ecs.hh" +#include "dnsdist-edns.hh" #include "dnsdist-nghttp2-in.hh" #include "dnsdist-proxy-protocol.hh" #include "dnsdist-rings.hh" @@ -1212,6 +1213,23 @@ void IncomingTCPConnectionState::notifyIOError(const struct timeval& now, TCPRes } } +static bool processXFRResponse(PacketBuffer& response, const std::vector& xfrRespRuleActions, DNSResponse& dnsResponse) +{ + if (!applyRulesToResponse(xfrRespRuleActions, dnsResponse)) { + return false; + } + + if (dnsResponse.isAsynchronous()) { + return true; + } + + if (dnsResponse.ids.d_extendedError) { + dnsdist::edns::addExtendedDNSError(dnsResponse.getMutableData(), dnsResponse.getMaximumSize(), dnsResponse.ids.d_extendedError->infoCode, dnsResponse.ids.d_extendedError->extraText); + } + + return true; +} + void IncomingTCPConnectionState::handleXFRResponse(const struct timeval& now, TCPResponse&& response) { if (std::this_thread::get_id() != d_creatorThreadID) { @@ -1220,6 +1238,17 @@ void IncomingTCPConnectionState::handleXFRResponse(const struct timeval& now, TC } std::shared_ptr state = shared_from_this(); + auto& ids = response.d_idstate; + std::shared_ptr backend = response.d_ds ? response.d_ds : (response.d_connection ? response.d_connection->getDS() : nullptr); + DNSResponse dnsResponse(ids, response.d_buffer, backend); + dnsResponse.d_incomingTCPState = state; + memcpy(&response.d_cleartextDH, dnsResponse.getHeader().get(), sizeof(response.d_cleartextDH)); + + if (!processXFRResponse(response.d_buffer, *state->d_threadData.localXFRRespRuleActions, dnsResponse)) { + state->terminateClientConnection(); + return; + } + queueResponse(state, now, std::move(response), true); } diff --git a/pdns/dnsdistdist/dnsdist.cc b/pdns/dnsdistdist/dnsdist.cc index 423091b584..b899760565 100644 --- a/pdns/dnsdistdist/dnsdist.cc +++ b/pdns/dnsdistdist/dnsdist.cc @@ -514,7 +514,7 @@ static bool encryptResponse(PacketBuffer& response, size_t maximumSize, bool tcp } #endif /* HAVE_DNSCRYPT */ -static bool applyRulesToResponse(const std::vector& respRuleActions, DNSResponse& dnsResponse) +bool applyRulesToResponse(const std::vector& respRuleActions, DNSResponse& dnsResponse) { DNSResponseAction::Action action = DNSResponseAction::Action::None; std::string ruleresult; diff --git a/pdns/dnsdistdist/dnsdist.hh b/pdns/dnsdistdist/dnsdist.hh index 2f1604c364..3aca751af5 100644 --- a/pdns/dnsdistdist/dnsdist.hh +++ b/pdns/dnsdistdist/dnsdist.hh @@ -1247,6 +1247,7 @@ bool processResponse(PacketBuffer& response, const std::vector& cacheInsertedRespRuleActions, DNSResponse& dnsResponse, bool muted); bool processResponderPacket(std::shared_ptr& dss, PacketBuffer& response, const std::vector& localRespRuleActions, const std::vector& cacheInsertedRespRuleActions, InternalQueryState&& ids); +bool applyRulesToResponse(const std::vector& respRuleActions, DNSResponse& dnsResponse); bool assignOutgoingUDPQueryToBackend(std::shared_ptr& downstream, uint16_t queryID, DNSQuestion& dnsQuestion, PacketBuffer& query, bool actuallySend = true); diff --git a/pdns/dnsdistdist/docs/reference/actions.rst b/pdns/dnsdistdist/docs/reference/actions.rst index c0489b6dd2..3defabcfd0 100644 --- a/pdns/dnsdistdist/docs/reference/actions.rst +++ b/pdns/dnsdistdist/docs/reference/actions.rst @@ -10,6 +10,7 @@ Some actions allow further processing of rules, this is noted in their descripti - :func:`DnstapLogResponseAction` - :func:`LimitTTLResponseAction` - :func:`LogAction` +- :func:`LogResponseAction` - :func:`NoneAction` - :func:`RemoteLogAction` - :func:`RemoteLogResponseAction` diff --git a/pdns/dnsdistdist/docs/reference/rules-management.rst b/pdns/dnsdistdist/docs/reference/rules-management.rst index 3c9986487b..ba9e0f4069 100644 --- a/pdns/dnsdistdist/docs/reference/rules-management.rst +++ b/pdns/dnsdistdist/docs/reference/rules-management.rst @@ -402,10 +402,80 @@ Functions for manipulating Self-Answered Response Rules: .. versionchanged:: 1.6.0 Replaced by :func:`mvSelfAnsweredResponseRuleToTop` - Before 1.6.0 this function used to move the last cache hit response rule to the first position, which is now handled by :func:`mvSelfAnsweredResponseRuleToTop`. + Before 1.6.0 this function used to move the last self-answered response rule to the first position, which is now handled by :func:`mvSelfAnsweredResponseRuleToTop`. Move the last self answered response rule to the first position. +XFR +--- + +Functions for manipulating zone transfer (AXFR, IXFR) Response Rules: + +.. note:: + Please remember that a zone transfer (XFR) can and will often contain + several response packets to a single query packet. + +.. warning:: + While almost all existing selectors and Response actions should be usable from + the XFR response rules, it is strongly advised to only inspect the content of + XFR response packets, and not modify them. + Logging the content of response packets can be done via: + + - :func:`DnstapLogResponseAction` + - :func:`LogResponseAction` + - :func:`RemoteLogResponseAction` + +.. function:: addXFRResponseAction(DNSRule, action [, options]) + + .. versionadded:: 1.10 + + Add a Rule and ResponseAction for zone transfers (XFR) to the existing rules. + If a string (or list of) is passed as the first parameter instead of a :class:`DNSRule`, it behaves as if the string or list of strings was passed to :func:`NetmaskGroupRule` or :func:`SuffixMatchNodeRule`. + + :param DNSrule rule: A :class:`DNSRule`, e.g. an :func:`AllRule`, or a compounded bunch of rules using e.g. :func:`AndRule`. + :param action: The action to take + :param table options: A table with key: value pairs with options. + + Options: + + * ``uuid``: string - UUID to assign to the new rule. By default a random UUID is generated for each rule. + * ``name``: string - Name to assign to the new rule. + +.. function:: mvXFRResponseRule(from, to) + + .. versionadded:: 1.10 + + Move XFR response rule ``from`` to a position where it is in front of ``to``. + ``to`` can be one larger than the largest rule, in which case the rule will be moved to the last position. + + :param int from: Rule number to move + :param int to: Location to more the Rule to + +.. function:: mvXFRResponseRuleToTop() + + .. versionadded:: 1.10 + + This function moves the last XFR response rule to the first position. + +.. function:: rmXFRResponseRule(id) + + .. versionadded:: 1.10 + + :param int id: The position of the rule to remove if ``id`` is numerical, its UUID or name otherwise + +.. function:: showXFRResponseRules([options]) + + .. versionadded:: 1.10 + + Show all defined XFR response rules, optionally displaying their UUIDs. + + :param table options: A table with key: value pairs with display options. + + Options: + + * ``showUUIDs=false``: bool - Whether to display the UUIDs, defaults to false. + * ``truncateRuleWidth=-1``: int - Truncate rules output to ``truncateRuleWidth`` size. Defaults to ``-1`` to display the full rule. + Convenience Functions --------------------- diff --git a/pdns/dnsdistdist/test-dnsdist_cc.cc b/pdns/dnsdistdist/test-dnsdist_cc.cc index 130e57f59f..f9704f9f45 100644 --- a/pdns/dnsdistdist/test-dnsdist_cc.cc +++ b/pdns/dnsdistdist/test-dnsdist_cc.cc @@ -53,6 +53,11 @@ bool processResponseAfterRules(PacketBuffer& response, const std::vector& respRuleActions, DNSResponse& dnsResponse) +{ + return true; +} + bool sendUDPResponse(int origFD, const PacketBuffer& response, const int delayMsec, const ComboAddress& origDest, const ComboAddress& origRemote) { return false; diff --git a/regression-tests.dnsdist/test_AXFR.py b/regression-tests.dnsdist/test_AXFR.py index 2788c79044..87ceb44c13 100644 --- a/regression-tests.dnsdist/test_AXFR.py +++ b/regression-tests.dnsdist/test_AXFR.py @@ -14,7 +14,6 @@ class TestAXFR(DNSDistTest): _config_template = """ newServer{address="127.0.0.1:%s"} """ - _verboseMode = True @classmethod def startResponders(cls): @@ -324,112 +323,3 @@ class TestAXFR(DNSDistTest): receivedQuery.id = query.id self.assertEqual(query, receivedQuery) self.assertEqual(len(receivedResponses), len(responses) - 1) - - # def testFourNoFirstSOAAXFR(self): - # """ - # AXFR: Four messages, no SOA in the first one - # """ - # name = 'fournosoainfirst.axfr.tests.powerdns.com.' - # query = dns.message.make_query(name, 'AXFR', 'IN') - # responses = [] - # soa = dns.rrset.from_text(name, - # 60, - # dns.rdataclass.IN, - # dns.rdatatype.SOA, - # 'ns.' + name + ' hostmaster.' + name + ' 1 3600 3600 3600 60') - # response = dns.message.make_response(query) - # response.answer.append(dns.rrset.from_text(name, - # 60, - # dns.rdataclass.IN, - # dns.rdatatype.A, - # '192.0.2.1')) - # responses.append(response) - - # response = dns.message.make_response(query) - # rrset = dns.rrset.from_text(name, - # 60, - # dns.rdataclass.IN, - # dns.rdatatype.AAAA, - # '2001:DB8::1') - # response.answer.append(soa) - # response.answer.append(rrset) - # responses.append(response) - - # response = dns.message.make_response(query) - # rrset = dns.rrset.from_text('dummy.' + name, - # 60, - # dns.rdataclass.IN, - # dns.rdatatype.AAAA, - # '2001:DB8::1') - # response.answer.append(rrset) - # responses.append(response) - - # response = dns.message.make_response(query) - # rrset = dns.rrset.from_text(name, - # 60, - # dns.rdataclass.IN, - # dns.rdatatype.TXT, - # 'dummy') - # response.answer.append(rrset) - # response.answer.append(soa) - # responses.append(response) - - # (receivedQuery, receivedResponses) = self.sendTCPQueryWithMultipleResponses(query, responses) - # receivedQuery.id = query.id - # self.assertEqual(query, receivedQuery) - # self.assertEqual(len(receivedResponses), 1) - - # def testFourLastSOAInSecondAXFR(self): - # """ - # AXFR: Four messages, SOA in the first one and the second one - # """ - # name = 'foursecondsoainsecond.axfr.tests.powerdns.com.' - # query = dns.message.make_query(name, 'AXFR', 'IN') - # responses = [] - # soa = dns.rrset.from_text(name, - # 60, - # dns.rdataclass.IN, - # dns.rdatatype.SOA, - # 'ns.' + name + ' hostmaster.' + name + ' 1 3600 3600 3600 60') - - # response = dns.message.make_response(query) - # response.answer.append(soa) - # response.answer.append(dns.rrset.from_text(name, - # 60, - # dns.rdataclass.IN, - # dns.rdatatype.A, - # '192.0.2.1')) - # responses.append(response) - - # response = dns.message.make_response(query) - # response.answer.append(soa) - # rrset = dns.rrset.from_text(name, - # 60, - # dns.rdataclass.IN, - # dns.rdatatype.AAAA, - # '2001:DB8::1') - # response.answer.append(rrset) - # responses.append(response) - - # response = dns.message.make_response(query) - # rrset = dns.rrset.from_text('dummy.' + name, - # 60, - # dns.rdataclass.IN, - # dns.rdatatype.AAAA, - # '2001:DB8::1') - # response.answer.append(rrset) - # responses.append(response) - - # response = dns.message.make_response(query) - # rrset = dns.rrset.from_text(name, - # 60, - # dns.rdataclass.IN, - # dns.rdatatype.TXT, - # 'dummy') - # response.answer.append(rrset) - # responses.append(response) - - # (receivedQuery, receivedResponses) = self.sendTCPQueryWithMultipleResponses(query, responses) - # receivedQuery.id = query.id - # self.assertEqual(query, receivedQuery) - # self.assertEqual(len(receivedResponses), 2) diff --git a/regression-tests.dnsdist/test_Protobuf.py b/regression-tests.dnsdist/test_Protobuf.py index 5f65fd31a9..a7d1195120 100644 --- a/regression-tests.dnsdist/test_Protobuf.py +++ b/regression-tests.dnsdist/test_Protobuf.py @@ -871,3 +871,133 @@ class TestProtobufQUIC(DNSDistProtobufTest): self.assertEqual(msg.httpVersion, dnsmessage_pb2.PBDNSMessage.HTTPVersion.HTTP3) self.checkProtobufQuery(msg, pbMessageType, query, dns.rdataclass.IN, dns.rdatatype.A, name) + +class TestProtobufAXFR(DNSDistProtobufTest): + # this test suite uses a different responder port + # because, contrary to the other ones, its + # TCP responder allows multiple responses and we don't want + # to mix things up. + _testServerPort = pickAvailablePort() + + @classmethod + def startResponders(cls): + print("Launching responders..") + + cls._UDPResponder = threading.Thread(name='UDP Protobuf AXFR Responder', target=cls.UDPResponder, args=[cls._testServerPort, cls._toResponderQueue, cls._fromResponderQueue]) + cls._UDPResponder.daemon = True + cls._UDPResponder.start() + cls._TCPResponder = threading.Thread(name='TCP Protobuf AXFR Responder', target=cls.TCPResponder, args=[cls._testServerPort, cls._toResponderQueue, cls._fromResponderQueue, False, True, None, None, True]) + cls._TCPResponder.daemon = True + cls._TCPResponder.start() + cls._protobufListener = threading.Thread(name='Protobuf Listener', target=cls.ProtobufListener, args=[cls._protobufServerPort]) + cls._protobufListener.daemon = True + cls._protobufListener.start() + + _config_template = """ + newServer{address="127.0.0.1:%d"} + rl = newRemoteLogger('127.0.0.1:%d') + + addXFRResponseAction(AllRule(), RemoteLogResponseAction(rl, nil, false, {serverID='dnsdist-server-1'})) + """ + _config_params = ['_testServerPort', '_protobufServerPort'] + + def testProtobufAXFR(self): + """ + Protobuf: Check the logging of multiple messages for AXFR responses + """ + # first query is NOT an AXFR, we should not log anything + name = 'axfr.protobuf.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + ttl = 60 + response = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + ttl, + dns.rdataclass.IN, + dns.rdatatype.A, + '127.0.0.1') + response.answer.append(rrset) + + for method in ("sendUDPQuery", "sendTCPQuery"): + sender = getattr(self, method) + (receivedQuery, receivedResponse) = sender(query, response) + + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = query.id + self.assertEqual(query, receivedQuery) + self.assertEqual(response, receivedResponse) + + self.assertTrue(self._protobufQueue.empty()) + + query = dns.message.make_query(name, 'AXFR', 'IN') + responses = [] + soa = dns.rrset.from_text(name, + ttl, + dns.rdataclass.IN, + dns.rdatatype.SOA, + 'ns.' + name + ' hostmaster.' + name + ' 1 3600 3600 3600 60') + response = dns.message.make_response(query) + response.answer.append(soa) + responses.append(response) + + response = dns.message.make_response(query) + response.answer.append(dns.rrset.from_text(name, + ttl, + dns.rdataclass.IN, + dns.rdatatype.A, + '192.0.2.1')) + responses.append(response) + + response = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + ttl, + dns.rdataclass.IN, + dns.rdatatype.AAAA, + '2001:db8::1') + response.answer.append(rrset) + responses.append(response) + + response = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + ttl, + dns.rdataclass.IN, + dns.rdatatype.TXT, + 'dummy') + response.answer.append(rrset) + responses.append(response) + + response = dns.message.make_response(query) + response.answer.append(soa) + responses.append(response) + + # UDP would not make sense since it does not support multiple messages + (receivedQuery, receivedResponses) = self.sendTCPQueryWithMultipleResponses(query, responses) + + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponses) + receivedQuery.id = query.id + self.assertEqual(query, receivedQuery) + self.assertEqual(len(receivedResponses), len(responses)) + + if self._protobufQueue.empty(): + # let the protobuf messages the time to get there + time.sleep(1) + + # check the protobuf messages corresponding to the responses + count = 0 + while not self._protobufQueue.empty(): + msg = self.getFirstProtobufMessage() + count = count + 1 + pbMessageType = dnsmessage_pb2.PBDNSMessage.TCP + self.checkProtobufResponse(msg, dnsmessage_pb2.PBDNSMessage.TCP, responses[count-1]) + + expected = responses[count-1].answer[0] + if expected.rdtype in [dns.rdatatype.A, dns.rdatatype.AAAA]: + rr = msg.response.rrs[0] + self.checkProtobufResponseRecord(rr, expected.rdclass, expected.rdtype, name, ttl) + if expected.rdtype == dns.rdatatype.A: + self.assertEqual(socket.inet_ntop(socket.AF_INET, rr.rdata), '192.0.2.1') + else: + self.assertEqual(socket.inet_ntop(socket.AF_INET6, rr.rdata), '2001:db8::1') + + self.assertEqual(count, len(responses))