From: Remi Gacogne Date: Fri, 13 Feb 2026 10:24:24 +0000 (+0100) Subject: dnsdist: Add actions, methods and FFI functions to unset a tag X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f249e21c187846aac7dd8288b835a48b55adaaea;p=thirdparty%2Fpdns.git dnsdist: Add actions, methods and FFI functions to unset a tag Signed-off-by: Remi Gacogne --- diff --git a/pdns/dnsdistdist/dnsdist-actions-definitions.yml b/pdns/dnsdistdist/dnsdist-actions-definitions.yml index 78d6d89525..a57fc258c2 100644 --- a/pdns/dnsdistdist/dnsdist-actions-definitions.yml +++ b/pdns/dnsdistdist/dnsdist-actions-definitions.yml @@ -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" diff --git a/pdns/dnsdistdist/dnsdist-actions-factory.cc b/pdns/dnsdistdist/dnsdist-actions-factory.cc index befe201d00..93063dc61a 100644 --- a/pdns/dnsdistdist/dnsdist-actions-factory.cc +++ b/pdns/dnsdistdist/dnsdist-actions-factory.cc @@ -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: diff --git a/pdns/dnsdistdist/dnsdist-lua-bindings-dnsquestion.cc b/pdns/dnsdistdist/dnsdist-lua-bindings-dnsquestion.cc index 219366db91..1b2162940c 100644 --- a/pdns/dnsdistdist/dnsdist-lua-bindings-dnsquestion.cc +++ b/pdns/dnsdistdist/dnsdist-lua-bindings-dnsquestion.cc @@ -216,22 +216,20 @@ void setupLuaBindingsDNSQuestion([[maybe_unused]] LuaContext& luaCtx) luaCtx.registerFunction("setTag", [](DNSQuestion& dnsQuestion, const std::string& strLabel, const std::string& strValue) { dnsQuestion.setTag(strLabel, strValue); }); + luaCtx.registerFunction("unsetTag", [](DNSQuestion& dnsQuestion, const std::string& strLabel) { + dnsQuestion.unsetTag(strLabel); + }); luaCtx.registerFunction)>("setTagArray", [](DNSQuestion& dnsQuestion, const LuaAssociativeTable& tags) { for (const auto& tag : tags) { dnsQuestion.setTag(tag.first, tag.second); } }); luaCtx.registerFunction("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("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("unsetTag", [](DNSResponse& dnsResponse, const std::string& strLabel) { + dnsResponse.unsetTag(strLabel); + }); + luaCtx.registerFunction)>("setTagArray", [](DNSResponse& dnsResponse, const LuaAssociativeTable& tags) { for (const auto& tag : tags) { dnsResponse.setTag(tag.first, tag.second); } }); luaCtx.registerFunction("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("getTagArray", [](const DNSResponse& dnsResponse) { if (!dnsResponse.ids.qTag) { diff --git a/pdns/dnsdistdist/dnsdist-lua-ffi-interface.h b/pdns/dnsdistdist/dnsdist-lua-ffi-interface.h index 3ec3014a62..65c8aecac6 100644 --- a/pdns/dnsdistdist/dnsdist-lua-ffi-interface.h +++ b/pdns/dnsdistdist/dnsdist-lua-ffi-interface.h @@ -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"))); diff --git a/pdns/dnsdistdist/dnsdist-lua-ffi.cc b/pdns/dnsdistdist/dnsdist-lua-ffi.cc index 9219e9bb89..c764b63677 100644 --- a/pdns/dnsdistdist/dnsdist-lua-ffi.cc +++ b/pdns/dnsdistdist/dnsdist-lua-ffi.cc @@ -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)); diff --git a/pdns/dnsdistdist/dnsdist-response-actions-definitions.yml b/pdns/dnsdistdist/dnsdist-response-actions-definitions.yml index 866921a65a..62f474578b 100644 --- a/pdns/dnsdistdist/dnsdist-response-actions-definitions.yml +++ b/pdns/dnsdistdist/dnsdist-response-actions-definitions.yml @@ -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" diff --git a/pdns/dnsdistdist/dnsdist.hh b/pdns/dnsdistdist/dnsdist.hh index e4483af0e2..0279ad7d6e 100644 --- a/pdns/dnsdistdist/dnsdist.hh +++ b/pdns/dnsdistdist/dnsdist.hh @@ -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 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; diff --git a/pdns/dnsdistdist/docs/reference/actions.rst b/pdns/dnsdistdist/docs/reference/actions.rst index 556d6edff7..779b3a5513 100644 --- a/pdns/dnsdistdist/docs/reference/actions.rst +++ b/pdns/dnsdistdist/docs/reference/actions.rst @@ -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 diff --git a/pdns/dnsdistdist/docs/reference/dq.rst b/pdns/dnsdistdist/docs/reference/dq.rst index dfdbfa89b9..0698b37ceb 100644 --- a/pdns/dnsdistdist/docs/reference/dq.rst +++ b/pdns/dnsdistdist/docs/reference/dq.rst @@ -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 diff --git a/pdns/dnsdistdist/test-dnsdist-lua-ffi.cc b/pdns/dnsdistdist/test-dnsdist-lua-ffi.cc index a1918c88c4..515d12c840 100644 --- a/pdns/dnsdistdist/test-dnsdist-lua-ffi.cc +++ b/pdns/dnsdistdist/test-dnsdist-lua-ffi.cc @@ -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 diff --git a/pdns/dnsdistdist/test-dnsdist_cc.cc b/pdns/dnsdistdist/test-dnsdist_cc.cc index 51fcba3989..ca7060eb3b 100644 --- a/pdns/dnsdistdist/test-dnsdist_cc.cc +++ b/pdns/dnsdistdist/test-dnsdist_cc.cc @@ -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 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(); diff --git a/regression-tests.dnsdist/test_LuaFFI.py b/regression-tests.dnsdist/test_LuaFFI.py index ff14d6df5f..c794bcb68c 100644 --- a/regression-tests.dnsdist/test_LuaFFI.py +++ b/regression-tests.dnsdist/test_LuaFFI.py @@ -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"} """ diff --git a/regression-tests.dnsdist/test_Tags.py b/regression-tests.dnsdist/test_Tags.py index f3a3ab416f..62d6e83a58 100644 --- a/regression-tests.dnsdist/test_Tags.py +++ b/regression-tests.dnsdist/test_Tags.py @@ -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)