]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Support mnemonics for the Opcode selector
authorRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 21 Jul 2025 09:56:47 +0000 (11:56 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 28 Jul 2025 09:34:03 +0000 (11:34 +0200)
Signed-off-by: Remi Gacogne <remi.gacogne@powerdns.com>
pdns/dns.cc
pdns/dns.hh
pdns/dnsdistdist/dnsdist-configuration-yaml.cc
pdns/dnsdistdist/dnsdist-rules-generator.py
pdns/dnsdistdist/dnsdist-rust-lib/dnsdist-settings-generator.py
pdns/dnsdistdist/dnsdist-selectors-definitions.yml
pdns/test-dns_cc.cc
regression-tests.dnsdist/test_Yaml.py

index 7d1b772a2fff89ec3dc83a07194f5ad6be72501e..df35dfa77f839390ab16f783f4bef1c775746f6d 100644 (file)
@@ -145,6 +145,16 @@ std::string Opcode::to_s(uint8_t opcode) {
   return s_opcodes.at(opcode);
 }
 
+std::optional<uint8_t> Opcode::from_lowercase_string(const std::string_view& opcode_string)
+{
+  static const std::array<std::string, 6> s_opcodes = { "query", "iquery", "status", "3", "notify", "update" };
+  const auto* position = std::find(s_opcodes.begin(), s_opcodes.end(), opcode_string);
+  if (position == s_opcodes.end()) {
+    return std::nullopt;
+  }
+  return std::distance(s_opcodes.begin(), position);
+}
+
 // goal is to hash based purely on the question name, and turn error into 'default'
 uint32_t hashQuestion(const uint8_t* packet, uint16_t packet_len, uint32_t init, bool& wasOK)
 {
index e1be00222f14b1bd0a2431c8c71baae8cb34ecf6..f1106828a356fd7321af7a67d54ea3581c4512f0 100644 (file)
@@ -55,6 +55,7 @@ class Opcode
 public:
   enum opcodes_ : uint8_t { Query=0, IQuery=1, Status=2, Notify=4, Update=5 };
   static std::string to_s(uint8_t opcode);
+  static std::optional<uint8_t> from_lowercase_string(const std::string_view& opcode_string);
 };
 
 // This needs to be a signed type, so that serialization of UnknownDomainID
index 998ee796d855c7a8d319f7b37087ef85f3c6a33d..b124b85c2e76555b044677a2150becf3829a9e01 100644 (file)
@@ -154,6 +154,17 @@ static uint16_t strToQType(const std::string& context, const std::string& parame
   return qtype;
 }
 
+static uint8_t strToOpcode(const std::string& context, const std::string& parameterName, const ::rust::String& opcode_rust_string)
+{
+  auto opcode_str = std::string(opcode_rust_string);
+  boost::to_lower(opcode_str);
+  auto opcode = Opcode::from_lowercase_string(opcode_str);
+  if (!opcode) {
+    return checkedConversionFromStr<uint8_t>(context, parameterName, opcode_rust_string);
+  }
+  return *opcode;
+}
+
 static std::optional<std::string> loadContentFromConfigurationFile(const std::string& fileName)
 {
   /* no check on the file size, don't do this with just any file! */
index f627a9959a2f454efc128dba41bbae0196bba39f..885ad23205960e03eb9061036379d68bc554843a 100644 (file)
@@ -68,6 +68,8 @@ def type_to_cpp(type_str, lua_interface, inside_container=False):
         return 'const std::string&'
     if type_str == 'RCode':
         return 'uint8_t'
+    if type_str == 'Opcode':
+        return 'uint8_t'
     return type_str
 
 def get_cpp_object_name(name, is_class=True):
index 74b9b6a9c1ad4bf0e26c000875bc10b9b52d917b..04529c4a70c273261ad7e5351351a1db79ca072f 100644 (file)
@@ -500,6 +500,8 @@ def get_cpp_parameters(struct_name, parameters, skip_name):
             field = f'convertSOAParams({field})'
         elif ptype == 'RCode':
             field = f'dnsdist::configuration::yaml::strToRCode("{struct_name}", "{name}", {field})'
+        elif ptype == 'Opcode':
+            field = f'dnsdist::configuration::yaml::strToOpcode("{struct_name}", "{name}", {field})'
         output += field
     return output
 
index b957f42984997505c81f113a69eac1e85a0ad817..5714ff6cf557b18e4158711f3bd7a6b8b8d69e2b 100644 (file)
@@ -222,7 +222,8 @@ Set the ``source`` parameter to ``false`` to match against destination address i
   description: "Matches queries with opcode equals to ``code``"
   parameters:
     - name: "code"
-      type: "u8"
+      type: "Opcode"
+      rust-type: "String"
       description: "The opcode to match"
 - name: "Or"
   description: "Matches the traffic if one or more of the selectors Rules does match"
index ca27b49d6b699e38103401f5a45ebbd2bc3e7c0b..9eedc8c0dd609b842cfe728acfccd67baf339752 100644 (file)
@@ -25,6 +25,7 @@
 
 #define BOOST_TEST_NO_MAIN
 
+#include <boost/algorithm/string.hpp>
 #include <boost/test/unit_test.hpp>
 
 #include "dns.hh"
@@ -68,9 +69,14 @@ BOOST_AUTO_TEST_CASE(test_opcode)
   for (uint8_t idx = Opcode::Query; idx <= Opcode::Update; idx++) {
     auto long_s = Opcode::to_s(idx);
     BOOST_CHECK(long_s.size() > 0);
+    boost::to_lower(long_s);
+    auto opcode = Opcode::from_lowercase_string(long_s);
+    BOOST_CHECK(opcode);
+    BOOST_CHECK_EQUAL(*opcode, idx);
   }
 
   BOOST_CHECK_EQUAL(Opcode::to_s(Opcode::Update + 1), std::to_string(Opcode::Update + 1));
+  BOOST_CHECK(!Opcode::from_lowercase_string("notanopcode"));
 }
 
 BOOST_AUTO_TEST_CASE(test_resource_record_place)
index bb12f6de0c03f7e52c3eabc55b4f2dd42ca9aeec..c383f885673327cd1d467099cdf386032b9df4ff 100644 (file)
@@ -391,3 +391,66 @@ query_rules:
             sender = getattr(self, method)
             (_, receivedResponse) = sender(query, response=None, useQueue=False)
             self.assertEqual(receivedResponse, expectedResponse)
+
+class TestYamlOpcode(DNSDistTest):
+
+    _yaml_config_template = """---
+binds:
+  - listen_address: "127.0.0.1:%d"
+    protocol: Do53
+
+backends:
+  - address: "127.0.0.1:%d"
+    protocol: Do53
+
+query_rules:
+  - name: "refuse queries from specific opcode"
+    selector:
+      type: "Opcode"
+      code: "NOTIFY"
+    action:
+      type: "RCode"
+      rcode: "Refused"
+"""
+    _yaml_config_params = ['_dnsDistPort', '_testServerPort']
+    _config_params = []
+
+    def testRefuseOpcodeNotify(self):
+        """
+        YAML: Refuse Opcode NOTIFY
+        """
+        name = 'opcodenotify.yaml.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN')
+        query.set_opcode(dns.opcode.NOTIFY)
+        query.flags &= ~dns.flags.RD
+        expectedResponse = dns.message.make_response(query)
+        expectedResponse.set_rcode(dns.rcode.REFUSED)
+
+        for method in ("sendUDPQuery", "sendTCPQuery"):
+            sender = getattr(self, method)
+            (_, receivedResponse) = sender(query, response=None, useQueue=False)
+            self.assertEqual(receivedResponse, expectedResponse)
+
+    def testAllowOpcodeUpdate(self):
+        """
+        YAML: Allow Opcode UPDATE
+        """
+        name = 'opcodeupdate.yaml.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'SOA', 'IN')
+        query.set_opcode(dns.opcode.UPDATE)
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    3600,
+                                    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)