]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Add a new response chain for XFR responses 13923/head
authorRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 8 Mar 2024 11:26:33 +0000 (12:26 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 15 Mar 2024 10:33:04 +0000 (11:33 +0100)
17 files changed:
.github/actions/spell-check/expect.txt
pdns/dnsdistdist/Makefile.am
pdns/dnsdistdist/dnsdist-console.cc
pdns/dnsdistdist/dnsdist-idstate.cc [new file with mode: 0644]
pdns/dnsdistdist/dnsdist-idstate.hh
pdns/dnsdistdist/dnsdist-rule-chains.cc
pdns/dnsdistdist/dnsdist-rule-chains.hh
pdns/dnsdistdist/dnsdist-tcp-downstream.cc
pdns/dnsdistdist/dnsdist-tcp-upstream.hh
pdns/dnsdistdist/dnsdist-tcp.cc
pdns/dnsdistdist/dnsdist.cc
pdns/dnsdistdist/dnsdist.hh
pdns/dnsdistdist/docs/reference/actions.rst
pdns/dnsdistdist/docs/reference/rules-management.rst
pdns/dnsdistdist/test-dnsdist_cc.cc
regression-tests.dnsdist/test_AXFR.py
regression-tests.dnsdist/test_Protobuf.py

index addd80d075d1b4e3f9075765ca3c4eefa8f1c479..d300b1926740b935c229914c55acc756360f24e3 100644 (file)
@@ -1528,6 +1528,7 @@ xdp
 Xek
 Xeon
 XForwarded
+XFR
 Xiang
 xorbooter
 xpf
index 4488d46d4cc935f9f7ee7322ad4ef2cb0728245a..e5255a67f790f038d51702f42aae9f9f2faeb08a 100644 (file)
@@ -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 \
index a175cae26156885b70bdba3c54fe1bc89d89c6fc..d7059c454cc9f022723e7f2cce9fe9a57acd6e8e 100644 (file)
@@ -495,6 +495,7 @@ const std::vector<ConsoleKeyword> 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<ConsoleKeyword> 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<ConsoleKeyword> 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<ConsoleKeyword> 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 (file)
index 0000000..395ce3c
--- /dev/null
@@ -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>(*qTag);
+  }
+  if (d_protoBufData) {
+    ids.d_protoBufData = std::make_unique<InternalQueryState::ProtoBufData>(*d_protoBufData);
+  }
+  ids.cs = cs;
+  return ids;
+}
index f83f9b71f5200e16a7c785c0c1c4b84c2d5a7d0f..0a1e758b33f4d0b1046af1f31ddb7cfa98550082 100644 (file)
@@ -129,6 +129,8 @@ struct InternalQueryState
 #endif /* HAVE_XSK */
   }
 
+  InternalQueryState partialCloneForXFR() const;
+
   boost::optional<Netmask> subnet{boost::none}; // 40
   ComboAddress origRemote; // 28
   ComboAddress origDest; // 28
index fd736aeedb43538ee2252839e4cadd1d27bcbeda..62cf8435c5a9f537b7cdf927d1ed12c0fe8f687b 100644 (file)
@@ -29,12 +29,14 @@ GlobalStateHolder<std::vector<ResponseRuleAction>> s_respruleactions;
 GlobalStateHolder<std::vector<ResponseRuleAction>> s_cachehitrespruleactions;
 GlobalStateHolder<std::vector<ResponseRuleAction>> s_selfansweredrespruleactions;
 GlobalStateHolder<std::vector<ResponseRuleAction>> s_cacheInsertedRespRuleActions;
+GlobalStateHolder<std::vector<ResponseRuleAction>> s_XFRRespRuleActions;
 
 static const std::vector<ResponseRuleChainDescription> 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<ResponseRuleChainDescription>& getResponseRuleChains()
index 1d1a93dee67f1f91be0d0558441f01540505639b..0b0ede3d14f44dc5373154170ab680633511a8c9 100644 (file)
@@ -40,7 +40,7 @@ enum class ResponseRuleChain : uint8_t
   CacheHitResponseRules = 1,
   CacheInsertedResponseRules = 2,
   SelfAnsweredResponseRules = 3,
-  ResponseRuleChainsCount = 4
+  XFRResponseRules = 4,
 };
 
 struct RuleAction
index 684c46fceee1913ff9aeaf004b32366b50805e41..9473300374e2e908d4b37c309b7eb37dc6df9f2a 100644 (file)
@@ -675,9 +675,9 @@ IOState TCPConnectionToBackend::handleResponse(std::shared_ptr<TCPConnectionToBa
     response.d_buffer = std::move(d_responseBuffer);
     response.d_connection = conn;
     response.d_ds = conn->d_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 "<<response.d_idstate.qname);
 
     it->second.d_query.d_xfrStarted = true;
index 052d9e43c96481e0084db916e249ecbba2a136d0..40ca9335a35479f4543776029d78224c05a21c8b 100644 (file)
@@ -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>(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>(FDMultiplexer::getMultiplexerSilent()))
   {
   }
 
   LocalHolders holders;
   LocalStateHolder<vector<dnsdist::rules::ResponseRuleAction>> localRespRuleActions;
   LocalStateHolder<vector<dnsdist::rules::ResponseRuleAction>> localCacheInsertedRespRuleActions;
+  LocalStateHolder<vector<dnsdist::rules::ResponseRuleAction>> localXFRRespRuleActions;
   std::unique_ptr<FDMultiplexer> mplexer{nullptr};
   pdns::channel::Receiver<ConnectionInfo> queryReceiver;
   pdns::channel::Receiver<CrossProtocolQuery> crossProtocolQueryReceiver;
index e3eb68e1152c4e50cb5f5db069f5b89ba590501a..da7e4e22325026dc4ec51c9a616e49fe1678215e 100644 (file)
@@ -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<dnsdist::rules::ResponseRuleAction>& 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<IncomingTCPConnectionState> state = shared_from_this();
+  auto& ids = response.d_idstate;
+  std::shared_ptr<DownstreamState> 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);
 }
 
index 423091b584197af31c6d63cdc4608183ac35ae39..b89976056542b1c852d982be00a8d61e3a9c40c4 100644 (file)
@@ -514,7 +514,7 @@ static bool encryptResponse(PacketBuffer& response, size_t maximumSize, bool tcp
 }
 #endif /* HAVE_DNSCRYPT */
 
-static bool applyRulesToResponse(const std::vector<dnsdist::rules::ResponseRuleAction>& respRuleActions, DNSResponse& dnsResponse)
+bool applyRulesToResponse(const std::vector<dnsdist::rules::ResponseRuleAction>& respRuleActions, DNSResponse& dnsResponse)
 {
   DNSResponseAction::Action action = DNSResponseAction::Action::None;
   std::string ruleresult;
index 2f1604c364877f7ca724d5a4878c02f4b690fe52..3aca751af53ecb5e4e3e6ec82faa3da4513a1f8b 100644 (file)
@@ -1247,6 +1247,7 @@ bool processResponse(PacketBuffer& response, const std::vector<dnsdist::rules::R
 bool processRulesResult(const DNSAction::Action& action, DNSQuestion& dnsQuestion, std::string& ruleresult, bool& drop);
 bool processResponseAfterRules(PacketBuffer& response, const std::vector<dnsdist::rules::ResponseRuleAction>& cacheInsertedRespRuleActions, DNSResponse& dnsResponse, bool muted);
 bool processResponderPacket(std::shared_ptr<DownstreamState>& dss, PacketBuffer& response, const std::vector<dnsdist::rules::ResponseRuleAction>& localRespRuleActions, const std::vector<dnsdist::rules::ResponseRuleAction>& cacheInsertedRespRuleActions, InternalQueryState&& ids);
+bool applyRulesToResponse(const std::vector<dnsdist::rules::ResponseRuleAction>& respRuleActions, DNSResponse& dnsResponse);
 
 bool assignOutgoingUDPQueryToBackend(std::shared_ptr<DownstreamState>& downstream, uint16_t queryID, DNSQuestion& dnsQuestion, PacketBuffer& query, bool actuallySend = true);
 
index c0489b6dd2f942d18ee4d758c6d07efcaf109ade..3defabcfd0b3cd2cf88168062144694a560ba4fd 100644 (file)
@@ -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`
index 3c9986487ba35703584099cc359506b1b2eb09f7..ba9e0f40692b14d7f61af6f3c5740485229f6bdf 100644 (file)
@@ -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
 ---------------------
 
index 130e57f59f5f31d6aa9ec256ba11902f81f95ab4..f9704f9f456029137ea1de084647c45496d0d4b2 100644 (file)
@@ -53,6 +53,11 @@ bool processResponseAfterRules(PacketBuffer& response, const std::vector<dnsdist
   return false;
 }
 
+bool applyRulesToResponse(const std::vector<dnsdist::rules::ResponseRuleAction>& respRuleActions, DNSResponse& dnsResponse)
+{
+  return true;
+}
+
 bool sendUDPResponse(int origFD, const PacketBuffer& response, const int delayMsec, const ComboAddress& origDest, const ComboAddress& origRemote)
 {
   return false;
index 2788c79044d362a576c58e0df540434cf9a3259c..87ceb44c13efdd7b2edbc3cc13490be931c15674 100644 (file)
@@ -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)
index 5f65fd31a9103cd83dcd6f6a34e24a33b78cbb15..a7d11951203622900d4092f45ba0bd9f7eca2b28 100644 (file)
@@ -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))