]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Implement DNSResponseAction.Truncate and TCResponseAction()
authorRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 7 Nov 2022 16:47:16 +0000 (17:47 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 8 Dec 2023 07:46:34 +0000 (08:46 +0100)
To be able to truncate already existing answers.

pdns/dnsdist-console.cc
pdns/dnsdist-lua-actions.cc
pdns/dnsdist-lua-vars.cc
pdns/dnsdist.cc
pdns/dnsdist.hh
pdns/dnsdistdist/dnsdist-lua-ffi.hh
pdns/dnsdistdist/docs/reference/constants.rst
pdns/dnsdistdist/docs/rules-actions.rst
regression-tests.dnsdist/test_Size.py

index b3c24434562edb517216508d976b79f1c99ea794..ee85726482b5e0bf9441bca2cb91fda45d6b5a43 100644 (file)
@@ -798,6 +798,7 @@ const std::vector<ConsoleKeyword> g_consoleKeywords{
   { "TagRule", true, "name [, value]", "matches if the tag named 'name' is present, with the given 'value' matching if any" },
   { "TCAction", true, "", "create answer to query with TC and RD bits set, to move to TCP" },
   { "TCPRule", true, "[tcp]", "Matches question received over TCP if tcp is true, over UDP otherwise" },
+  { "TCResponseAction", true, "", "truncate a response" },
   { "TeeAction", true, "remote [, addECS [, local]]", "send copy of query to remote, optionally adding ECS info, optionally set local address" },
   { "testCrypto", true, "", "test of the crypto all works" },
   { "TimedIPSetRule", true, "", "Create a rule which matches a set of IP addresses which expire"},
index dabe0b8dac355009c81010aefcea99e8e6f2d7c5..eb34ca125cea5f2b5ec737c7d411fbec5cda5fee 100644 (file)
@@ -513,6 +513,19 @@ public:
   }
 };
 
+class TCResponseAction : public DNSResponseAction
+{
+public:
+  DNSResponseAction::Action operator()(DNSResponse* dnsResponse, std::string* ruleresult) const override
+  {
+    return Action::Truncate;
+  }
+  std::string toString() const override
+  {
+    return "tc=1 answer";
+  }
+};
+
 class LuaAction : public DNSAction
 {
 public:
@@ -2538,6 +2551,10 @@ void setupLuaActions(LuaContext& luaCtx)
       return std::shared_ptr<DNSAction>(new TCAction);
     });
 
+  luaCtx.writeFunction("TCResponseAction", []() {
+      return std::shared_ptr<DNSResponseAction>(new TCResponseAction);
+    });
+
   luaCtx.writeFunction("SetDisableValidationAction", []() {
       return std::shared_ptr<DNSAction>(new SetDisableValidationAction);
     });
index 265568f9ffda2c4e2c86a77e56731ac5c1e74c1e..89927b72456a5a7bb1c5fe499c89695375ff7d69 100644 (file)
@@ -51,6 +51,7 @@ void setupLuaVars(LuaContext& luaCtx)
       {"Drop",         (int)DNSResponseAction::Action::Drop         },
       {"HeaderModify", (int)DNSResponseAction::Action::HeaderModify },
       {"ServFail",     (int)DNSResponseAction::Action::ServFail     },
+      {"Truncate",     (int)DNSResponseAction::Action::Truncate     },
       {"None",         (int)DNSResponseAction::Action::None         }
     });
 
index 77c7f8d3a9d01a2aff20393353a710ccc0408a0e..69dea53f083034abdafde9e1f6433cf41885508d 100644 (file)
@@ -532,6 +532,18 @@ static bool applyRulesToResponse(const std::vector<DNSDistResponseRuleAction>& r
         });
         return true;
         break;
+      case DNSResponseAction::Action::Truncate:
+        if (!dr.overTCP()) {
+          dnsdist::PacketMangling::editDNSHeaderFromPacket(dr.getMutableData(), [](dnsheader& header) {
+            header.tc = true;
+            header.qr = true;
+            return true;
+          });
+          truncateTC(dr.getMutableData(), dr.getMaximumSize(), dr.ids.qname.wirelength());
+          ++dnsdist::metrics::g_stats.ruleTruncated;
+          return true;
+        }
+        break;
         /* non-terminal actions follow */
       case DNSResponseAction::Action::Delay:
         pdns::checked_stoi_into(dr.ids.delayMsec, ruleresult); // sorry
index aa64c3f0c3b4d90335eed231b9b655287c6f2644..79e81159ac6afc3770d7c6b100a86a482e0bb12b 100644 (file)
@@ -266,7 +266,7 @@ public:
 class DNSResponseAction
 {
 public:
-  enum class Action : uint8_t { Allow, Delay, Drop, HeaderModify, ServFail, None };
+  enum class Action : uint8_t { Allow, Delay, Drop, HeaderModify, ServFail, Truncate, None };
   virtual Action operator()(DNSResponse*, string* ruleresult) const =0;
   virtual ~DNSResponseAction()
   {
index d0ed83341dde71f16abce0e4645a86c1c0b40732..a91003b971ad0ae557d05665325ae4c000dc6a39 100644 (file)
@@ -48,11 +48,11 @@ struct dnsdist_ffi_dnsquestion_t
   DNSQuestion* dq{nullptr};
   ComboAddress maskedRemote;
   std::string trailingData;
-  boost::optional<std::string> result{boost::none};
-  boost::optional<std::string> httpPath{boost::none};
-  boost::optional<std::string> httpQueryString{boost::none};
-  boost::optional<std::string> httpHost{boost::none};
-  boost::optional<std::string> httpScheme{boost::none};
+  std::optional<std::string> result{std::nullopt};
+  std::optional<std::string> httpPath{std::nullopt};
+  std::optional<std::string> httpQueryString{std::nullopt};
+  std::optional<std::string> httpHost{std::nullopt};
+  std::optional<std::string> httpScheme{std::nullopt};
   std::unique_ptr<std::vector<dnsdist_ffi_ednsoption_t>> ednsOptionsVect;
   std::unique_ptr<std::vector<dnsdist_ffi_http_header_t>> httpHeadersVect;
   std::unique_ptr<std::vector<dnsdist_ffi_tag_t>> tagsVect;
@@ -78,7 +78,7 @@ struct dnsdist_ffi_dnsresponse_t
   }
 
   DNSResponse* dr{nullptr};
-  boost::optional<std::string> result{boost::none};
+  std::optional<std::string> result{std::nullopt};
 };
 
 // dnsdist_ffi_server_t is a lightuserdata
index c391741b2668d0b9ed26b31661cd437ee774c285..e5300da5ef8504954cac5c7df03eca5f590e27d0 100755 (executable)
@@ -154,6 +154,9 @@ All named `QTypes <https://www.iana.org/assignments/dns-parameters/dns-parameter
 DNSResponseAction
 -----------------
 
+.. versionchanged:: 1.8.0
+  The ``DNSResponseAction.Truncate`` value was added.
+
 These constants represent an Action that can be returned from :func:`LuaResponseAction` functions.
 
  * ``DNSResponseAction.Allow``: let the response pass, skipping other rules
@@ -162,3 +165,4 @@ These constants represent an Action that can be returned from :func:`LuaResponse
  * ``DNSResponseAction.HeaderModify``: indicate that the query has been turned into a response
  * ``DNSResponseAction.None``: continue to the next rule
  * ``DNSResponseAction.ServFail``: return a response with a ServFail rcode
+ * ``DNSResponseAction.Truncate``: truncate the response
index 7bb7a31c07b3734f1efe222a12131bdc8fff3e4a..469f3c1e2f2eaba51bb01479d614d5ccd021c7ab 100644 (file)
@@ -1896,6 +1896,12 @@ The following actions exist.
   Before 1.7.0 this action was performed even when the query had been received over TCP, which required the use of :func:`TCPRule` to
   prevent the TC bit from being set over TCP transports.
 
+.. function:: TCResponseAction()
+
+  .. versionadded:: 1.9.0
+
+  Truncate an existing answer, to force the client to TCP.
+
 .. function:: TeeAction(remote[, addECS[, local [, addProxyProtocol]]])
 
   .. versionchanged:: 1.8.0
index 5503587ed8eaa6359cb691570eb0684b5530e0fa..cff8d81e713ab16d329f0b8d86f75d6bf214b4cb 100644 (file)
@@ -27,9 +27,53 @@ class TestSize(DNSDistTest):
                                     dns.rdatatype.A,
                                     '192.0.2.3')
         expectedResponse.answer.append(rrset)
+        self.assertEqual(len(query.to_wire()), self._payloadSize)
 
         for method in ("sendUDPQuery", "sendTCPQuery"):
             sender = getattr(self, method)
             (_, receivedResponse) = sender(query, response=None, useQueue=False)
             self.assertTrue(receivedResponse)
             self.assertEqual(receivedResponse, expectedResponse)
+
+class TestTruncateOversizedResponse(DNSDistTest):
+
+    _payloadSize = 512
+    _config_template = """
+    addResponseAction(PayloadSizeRule("greater", %d), TCResponseAction())
+    newServer{address="127.0.0.1:%d"}
+    """
+    _config_params = ['_payloadSize', '_testServerPort']
+
+    def testTruncateOversizedResponse(self):
+        """
+        Size: Truncate oversized response
+        """
+        name = 'truncate-oversized.size.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'TXT', 'IN')
+        query.flags &= ~dns.flags.RD
+        expectedResponse = dns.message.make_response(query)
+        expectedResponse.flags |= dns.flags.TC
+
+        backendResponse = dns.message.make_response(query)
+        content = ''
+        for i in range(2):
+            if len(content) > 0:
+                content = content + ' '
+            content = content + 'A' * 255
+        rrset = dns.rrset.from_text(name,
+                                    3600,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.TXT,
+                                    content)
+        backendResponse.answer.append(rrset)
+        self.assertGreater(len(backendResponse.to_wire()), self._payloadSize)
+
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response=backendResponse)
+        self.assertTrue(receivedQuery)
+        self.assertTrue(receivedResponse)
+        self.assertEqual(receivedResponse, expectedResponse)
+
+        (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response=backendResponse)
+        self.assertTrue(receivedQuery)
+        self.assertTrue(receivedResponse)
+        self.assertEqual(receivedResponse, backendResponse)