]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Add actions, methods and FFI functions to unset a tag
authorRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 13 Feb 2026 10:24:24 +0000 (11:24 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 13 Feb 2026 10:36:45 +0000 (11:36 +0100)
Signed-off-by: Remi Gacogne <remi.gacogne@powerdns.com>
13 files changed:
pdns/dnsdistdist/dnsdist-actions-definitions.yml
pdns/dnsdistdist/dnsdist-actions-factory.cc
pdns/dnsdistdist/dnsdist-lua-bindings-dnsquestion.cc
pdns/dnsdistdist/dnsdist-lua-ffi-interface.h
pdns/dnsdistdist/dnsdist-lua-ffi.cc
pdns/dnsdistdist/dnsdist-response-actions-definitions.yml
pdns/dnsdistdist/dnsdist.hh
pdns/dnsdistdist/docs/reference/actions.rst
pdns/dnsdistdist/docs/reference/dq.rst
pdns/dnsdistdist/test-dnsdist-lua-ffi.cc
pdns/dnsdistdist/test-dnsdist_cc.cc
regression-tests.dnsdist/test_LuaFFI.py
regression-tests.dnsdist/test_Tags.py

index 78d6d895250a4bbbe59d5c3a8aa4307a86612d3a..a57fc258c21931b747b4156c49b93a7f1d4d3556 100644 (file)
@@ -542,3 +542,9 @@ are processed after this action"
       type: "bool"
       default: "false"
       description: "Whether to add a proxy protocol payload to the query"
+- name: "UnsetTag"
+  description: "Remove a tag named ``tag`` from this query. Subsequent rules are processed after this action"
+  parameters:
+    - name: "tag"
+      type: "String"
+      description: "The tag name"
index befe201d00cb3f73d14b2ba830d36991463b908b..93063dc61a2b3e4cef9ad7403ea9d76ba7d50913 100644 (file)
@@ -1862,6 +1862,30 @@ private:
   std::string d_value;
 };
 
+class UnsetTagAction : public DNSAction
+{
+public:
+  // this action does not stop the processing
+  UnsetTagAction(std::string tag) :
+    d_tag(std::move(tag))
+  {
+  }
+  DNSAction::Action operator()(DNSQuestion* dnsquestion, std::string* ruleresult) const override
+  {
+    (void)ruleresult;
+    dnsquestion->unsetTag(d_tag);
+
+    return Action::None;
+  }
+  [[nodiscard]] std::string toString() const override
+  {
+    return "unset tag '" + d_tag;
+  }
+
+private:
+  std::string d_tag;
+};
+
 #ifndef DISABLE_PROTOBUF
 class DnstapLogResponseAction : public DNSResponseAction, public boost::noncopyable
 {
@@ -2102,6 +2126,30 @@ private:
   std::string d_value;
 };
 
+class UnsetTagResponseAction : public DNSResponseAction
+{
+public:
+  // this action does not stop the processing
+  UnsetTagResponseAction(std::string tag) :
+    d_tag(std::move(tag))
+  {
+  }
+  DNSResponseAction::Action operator()(DNSResponse* dnsresponse, std::string* ruleresult) const override
+  {
+    (void)ruleresult;
+    dnsresponse->unsetTag(d_tag);
+
+    return Action::None;
+  }
+  [[nodiscard]] std::string toString() const override
+  {
+    return "unset tag '" + d_tag;
+  }
+
+private:
+  std::string d_tag;
+};
+
 class ClearRecordTypesResponseAction : public DNSResponseAction, public boost::noncopyable
 {
 public:
index 219366db91045b705c5d7908826b8a7ea536239f..1b2162940c41157babbf57a3e3ddd7885865cfca 100644 (file)
@@ -216,22 +216,20 @@ void setupLuaBindingsDNSQuestion([[maybe_unused]] LuaContext& luaCtx)
   luaCtx.registerFunction<void (DNSQuestion::*)(std::string, std::string)>("setTag", [](DNSQuestion& dnsQuestion, const std::string& strLabel, const std::string& strValue) {
     dnsQuestion.setTag(strLabel, strValue);
   });
+  luaCtx.registerFunction<void (DNSQuestion::*)(std::string)>("unsetTag", [](DNSQuestion& dnsQuestion, const std::string& strLabel) {
+    dnsQuestion.unsetTag(strLabel);
+  });
   luaCtx.registerFunction<void (DNSQuestion::*)(LuaAssociativeTable<std::string>)>("setTagArray", [](DNSQuestion& dnsQuestion, const LuaAssociativeTable<std::string>& tags) {
     for (const auto& tag : tags) {
       dnsQuestion.setTag(tag.first, tag.second);
     }
   });
   luaCtx.registerFunction<string (DNSQuestion::*)(std::string) const>("getTag", [](const DNSQuestion& dnsQuestion, const std::string& strLabel) {
-    if (!dnsQuestion.ids.qTag) {
-      return string();
-    }
-
-    std::string strValue;
-    const auto tagIt = dnsQuestion.ids.qTag->find(strLabel);
-    if (tagIt == dnsQuestion.ids.qTag->cend()) {
+    auto value = dnsQuestion.getTag(strLabel);
+    if (!value) {
       return string();
     }
-    return tagIt->second;
+    return *value;
   });
   luaCtx.registerFunction<QTag (DNSQuestion::*)(void) const>("getTagArray", [](const DNSQuestion& dnsQuestion) -> QTag {
     if (!dnsQuestion.ids.qTag) {
@@ -539,22 +537,21 @@ void setupLuaBindingsDNSQuestion([[maybe_unused]] LuaContext& luaCtx)
     dnsResponse.setTag(strLabel, strValue);
   });
 
+  luaCtx.registerFunction<void (DNSResponse::*)(std::string)>("unsetTag", [](DNSResponse& dnsResponse, const std::string& strLabel) {
+    dnsResponse.unsetTag(strLabel);
+  });
+
   luaCtx.registerFunction<void (DNSResponse::*)(LuaAssociativeTable<std::string>)>("setTagArray", [](DNSResponse& dnsResponse, const LuaAssociativeTable<string>& tags) {
     for (const auto& tag : tags) {
       dnsResponse.setTag(tag.first, tag.second);
     }
   });
   luaCtx.registerFunction<string (DNSResponse::*)(std::string) const>("getTag", [](const DNSResponse& dnsResponse, const std::string& strLabel) {
-    if (!dnsResponse.ids.qTag) {
-      return string();
-    }
-
-    std::string strValue;
-    const auto tagIt = dnsResponse.ids.qTag->find(strLabel);
-    if (tagIt == dnsResponse.ids.qTag->cend()) {
+    auto value = dnsResponse.getTag(strLabel);
+    if (!value) {
       return string();
     }
-    return tagIt->second;
+    return *value;
   });
   luaCtx.registerFunction<QTag (DNSResponse::*)(void) const>("getTagArray", [](const DNSResponse& dnsResponse) {
     if (!dnsResponse.ids.qTag) {
index 3ec3014a62498fcce53c3d9f0d7479e49c47e36a..65c8aecac631b98ea57d014c46cae2baa756d3f7 100644 (file)
@@ -111,7 +111,8 @@ void dnsdist_ffi_dnsquestion_set_ecs_override(dnsdist_ffi_dnsquestion_t* dq, boo
 void dnsdist_ffi_dnsquestion_set_ecs_prefix_length(dnsdist_ffi_dnsquestion_t* dq, uint16_t ecsPrefixLength) __attribute__ ((visibility ("default")));
 void dnsdist_ffi_dnsquestion_set_temp_failure_ttl(dnsdist_ffi_dnsquestion_t* dq, uint32_t tempFailureTTL) __attribute__ ((visibility ("default")));
 void dnsdist_ffi_dnsquestion_unset_temp_failure_ttl(dnsdist_ffi_dnsquestion_t* dq) __attribute__ ((visibility ("default")));
-void dnsdist_ffi_dnsquestion_set_tag(dnsdist_ffi_dnsquestion_t* dq, const char* label, const char* value) __attribute__ ((visibility ("default")));
+void dnsdist_ffi_dnsquestion_set_tag(dnsdist_ffi_dnsquestion_t* dq, const char* label, const char* value) __attribute__((visibility("default")));
+void dnsdist_ffi_dnsquestion_unset_tag(dnsdist_ffi_dnsquestion_t* dq, const char* label) __attribute__ ((visibility ("default")));
 void dnsdist_ffi_dnsquestion_set_tag_raw(dnsdist_ffi_dnsquestion_t* dq, const char* label, const char* value, size_t valueSize) __attribute__ ((visibility ("default")));
 
 void dnsdist_ffi_dnsquestion_set_requestor_id(dnsdist_ffi_dnsquestion_t* dq, const char* value, size_t valueSize) __attribute__ ((visibility ("default")));
index 9219e9bb89d7146d51442d6ff1fbc609d09a3800..c764b63677817a5d6c2e32c275a70b467c4a0167 100644 (file)
@@ -614,6 +614,11 @@ void dnsdist_ffi_dnsquestion_set_tag(dnsdist_ffi_dnsquestion_t* dq, const char*
   dq->dq->setTag(label, value);
 }
 
+void dnsdist_ffi_dnsquestion_unset_tag(dnsdist_ffi_dnsquestion_t* dq, const char* label)
+{
+  dq->dq->unsetTag(label);
+}
+
 void dnsdist_ffi_dnsquestion_set_tag_raw(dnsdist_ffi_dnsquestion_t* dq, const char* label, const char* value, size_t valueSize)
 {
   dq->dq->setTag(label, std::string(value, valueSize));
index 866921a65a1017cc39f2bbcd8c54211dcc424fc3..62f474578b3d8b6af3947fd9ab28f0b785461bf5 100644 (file)
@@ -262,3 +262,9 @@ The function will be invoked in a per-thread Lua state, without access to the gl
       description: "The SNMP trap reason"
 - name: "TC"
   description: "Truncate an existing answer, to force the client to TCP. Only applied to answers that will be sent to the client over TCP. In addition to the TC bit being set, all records are removed from the answer, authority and additional sections"
+- name: "UnsetTag"
+  description: "Remove a tag named ``tag`` from this response. Subsequent rules are processed after this action"
+  parameters:
+    - name: "tag"
+      type: "String"
+      description: "The tag name"
index e4483af0e22fc9519ca36a6bbf5f6c2202ef941a..0279ad7d6e88afd6f6a8f441584c6177e625fdc3 100644 (file)
@@ -149,6 +149,24 @@ struct DNSQuestion
     ids.qTag->insert_or_assign(key, std::move(value));
   }
 
+  void unsetTag(const std::string& key)
+  {
+    if (ids.qTag) {
+      ids.qTag->erase(key);
+    }
+  }
+
+  std::optional<std::string> getTag(const std::string& key) const
+  {
+    if (ids.qTag) {
+      const auto tagIt = ids.qTag->find(key);
+      if (tagIt != ids.qTag->cend()) {
+        return tagIt->second;
+      }
+    }
+    return std::nullopt;
+  }
+
   const struct timespec& getQueryRealTime() const
   {
     return ids.queryRealTime.d_start;
index 556d6edff70537badb63707131dc9e354f0aef4d..779b3a55133ae0eb1c6a3ba8e81146422bd8937f 100644 (file)
@@ -1012,3 +1012,21 @@ The following actions exist.
   Subsequent rules are processed after this action.
 
   :param int ttl: Cache TTL for temporary failure replies
+
+.. function:: UnsetTagAction(name)
+
+  .. versionadded:: 2.1.0
+
+  Remove a tag named ``name``.
+  Subsequent rules are processed after this action.
+
+  :param string name: The name of the tag to set
+
+.. function:: UnsetTagResponseAction(name)
+
+  .. versionadded:: 2.1.0
+
+  Remove a tag named ``name``.
+  Subsequent rules are processed after this action.
+
+  :param string name: The name of the tag to set
index dfdbfa89b9eda1dc4c684ef57424a2b802c18025..0698b37ceb544f491106be8a886d80ce714eb405 100644 (file)
@@ -441,6 +441,15 @@ This state can be modified from the various hooks.
     :param int queryID: A numeric identifier used to identify the suspended query for later retrieval. This ID does not have to match the query ID present in the initial DNS header. A given (asyncID, queryID) tuple should be unique at a given time. Valid values range from 0 to 65535, both included.
     :param int timeoutMS: The maximum duration this query will be kept in the asynchronous holder before being automatically resumed,  in milliseconds.
 
+  .. method:: DNSQuestion:unsetTag(key)
+
+    .. versionadded:: 2.1.0
+
+    Remove a tag from the DNSQuestion object.
+
+    :param string key: The tag's key
+
+
 .. _DNSResponse:
 
 DNSResponse object
index a1918c88c432066a38d007aac3cab6a82a8f9b14..515d12c84047b37672cd7fdeb6f730a6b4f8b1ea 100644 (file)
@@ -328,6 +328,11 @@ BOOST_AUTO_TEST_CASE(test_Query)
     BOOST_CHECK_EQUAL(std::string(tags[0].name), tagName.c_str());
     BOOST_CHECK_EQUAL(std::string(tags[0].value), tagValue.c_str());
 
+    dnsdist_ffi_dnsquestion_unset_tag(&lightDQ, tagName.c_str());
+
+    got = dnsdist_ffi_dnsquestion_get_tag(&lightDQ, tagName.c_str());
+    BOOST_CHECK(got == nullptr);
+
     dnsdist_ffi_dnsquestion_set_tag_raw(&lightDQ, tagName.c_str(), tagRawValue.c_str(), tagRawValue.size());
 
     // too small
index 51fcba39898486ee508d058802449dd4c26413e1..ca7060eb3b9ae25fc2445838468deca7bf1c6e75 100644 (file)
@@ -2476,4 +2476,42 @@ BOOST_AUTO_TEST_CASE(test_setEDNSOption)
   BOOST_CHECK_EQUAL(cookiesOptionStr, std::string(ecsOption->second.values.at(0).content, ecsOption->second.values.at(0).size));
 }
 
+BOOST_AUTO_TEST_CASE(test_DNSQuestion_Tags)
+{
+  InternalQueryState ids;
+  ids.origRemote = ComboAddress("192.0.2.1:42");
+  ids.origDest = ComboAddress("127.0.0.1:53");
+  ids.protocol = dnsdist::Protocol::DoUDP;
+  ids.qname = DNSName("powerdns.com.");
+  ids.qtype = QType::A;
+  ids.qclass = QClass::IN;
+  ids.queryRealTime.start();
+
+  PacketBuffer packet;
+  GenericDNSPacketWriter<PacketBuffer> packetWriter(packet, ids.qname, ids.qtype, ids.qclass, 0);
+  packetWriter.addOpt(4096, 0, EDNS_HEADER_FLAG_DO);
+  packetWriter.commit();
+
+  DNSQuestion dnsQuestion(ids, packet);
+
+  BOOST_CHECK(dnsQuestion.getTag("not-existing") == std::nullopt);
+
+  const std::string tagName{"my-tag-name"};
+  const std::string tagValue{"my-tag-value"};
+  const std::string tagValue2{"my-tag-value-2"};
+  dnsQuestion.setTag(tagName, tagValue);
+  auto got = dnsQuestion.getTag(tagName);
+  BOOST_REQUIRE(got);
+  BOOST_CHECK_EQUAL(*got, tagValue);
+
+  dnsQuestion.setTag(tagName, tagValue2);
+  got = dnsQuestion.getTag(tagName);
+  BOOST_REQUIRE(got);
+  BOOST_CHECK_EQUAL(*got, tagValue2);
+
+  dnsQuestion.unsetTag(tagName);
+  got = dnsQuestion.getTag(tagName);
+  BOOST_CHECK(!got);
+}
+
 BOOST_AUTO_TEST_SUITE_END();
index ff14d6df5f0635de35057bb20dc4410b40415c87..c794bcb68c729bb977d9eecb20bebdbd01485431 100644 (file)
@@ -95,6 +95,13 @@ class TestAdvancedLuaFFI(DNSDistTest):
         return false
       end
 
+      tag = ffi.C.dnsdist_ffi_dnsquestion_get_tag(dq, 'b-tag')
+      if tag ~= nil then
+        print('invalid B tag value')
+        print(ffi.string(tag))
+        return false
+      end
+
       local raw_tag_buf_size = 255
       local raw_tag_buf = ffi.new("char [?]", raw_tag_buf_size)
       local raw_tag_size = ffi.C.dnsdist_ffi_dnsquestion_get_tag_raw(dq, 'raw-tag', raw_tag_buf, raw_tag_buf_size)
@@ -123,6 +130,12 @@ class TestAdvancedLuaFFI(DNSDistTest):
 
     function luaffiactionsettag(dq)
       ffi.C.dnsdist_ffi_dnsquestion_set_tag(dq, 'a-tag', 'a-value')
+      ffi.C.dnsdist_ffi_dnsquestion_set_tag(dq, 'b-tag', 'b-value')
+      return DNSAction.None
+    end
+
+    function luaffiactionunsettag(dq)
+      ffi.C.dnsdist_ffi_dnsquestion_unset_tag(dq, 'b-tag')
       return DNSAction.None
     end
 
@@ -134,6 +147,7 @@ class TestAdvancedLuaFFI(DNSDistTest):
 
     addAction(AllRule(), LuaFFIAction(luaffiactionsettag))
     addAction(AllRule(), LuaFFIAction(luaffiactionsettagraw))
+    addAction(AllRule(), LuaFFIAction(luaffiactionunsettag))
     addAction(LuaFFIRule(luaffirulefunction), LuaFFIAction(luaffiactionfunction))
     -- newServer{address="127.0.0.1:%d"}
     """
index f3a3ab416fc60c947595c35d734dd8f8d39e2bf1..62d6e83a58bf51c1df7a5c5bdabb696ea9bc37ae 100644 (file)
@@ -335,3 +335,87 @@ class TestSetTag(DNSDistTest):
             (_, receivedResponse) = sender(query, response=None, useQueue=False)
             self.assertTrue(receivedResponse)
             self.assertEqual(expectedResponse, receivedResponse)
+
+class TestUnsetTag(DNSDistTest):
+
+    _config_template = """
+    newServer{address="127.0.0.1:%d"}
+
+    function dqset(dq)
+      dq:setTag("dns", "value1")
+      return DNSAction.None, ""
+    end
+
+    addAction(AllRule(), LuaAction(dqset))
+
+    addAction(AllRule(), UnsetTagAction("dns"))
+    addAction(TagRule("dns", "value1"), SpoofAction("1.2.3.4"))
+    """
+
+    def testUnsetTag(self):
+
+        """
+        Tag: Test UnsetTagAction
+        """
+        name = 'unset.tags.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN')
+        # dnsdist set RA = RD for spoofed responses
+        query.flags &= ~dns.flags.RD
+        expectedResponse = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    60,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.A,
+                                    '1.2.3.50')
+        expectedResponse.answer.append(rrset)
+
+        for method in ("sendUDPQuery", "sendTCPQuery"):
+            sender = getattr(self, method)
+            (receivedQuery, receivedResponse) = sender(query, response=expectedResponse)
+            self.assertTrue(receivedQuery)
+            self.assertTrue(receivedResponse)
+            receivedQuery.id = query.id
+            self.assertEqual(query, receivedQuery)
+            self.assertEqual(expectedResponse, receivedResponse)
+
+class TestUnsetTagViaLua(DNSDistTest):
+
+    _config_template = """
+    newServer{address="127.0.0.1:%d"}
+
+    function dqunset(dq)
+      dq:unsetTag("dns")
+      return DNSAction.None, ""
+    end
+
+    addAction(AllRule(), SetTagAction("dns", "value1"))
+    addAction(AllRule(), LuaAction(dqunset))
+
+    addAction(TagRule("dns", "value1"), SpoofAction("1.2.3.4"))
+    """
+
+    def testUnsetTag(self):
+
+        """
+        Tag: Test UnsetTag via Lua
+        """
+        name = 'unset-lua.tags.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN')
+        # dnsdist set RA = RD for spoofed responses
+        query.flags &= ~dns.flags.RD
+        expectedResponse = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    60,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.A,
+                                    '1.2.3.50')
+        expectedResponse.answer.append(rrset)
+
+        for method in ("sendUDPQuery", "sendTCPQuery"):
+            sender = getattr(self, method)
+            (receivedQuery, receivedResponse) = sender(query, response=expectedResponse)
+            self.assertTrue(receivedQuery)
+            self.assertTrue(receivedResponse)
+            receivedQuery.id = query.id
+            self.assertEqual(query, receivedQuery)
+            self.assertEqual(expectedResponse, receivedResponse)