From: Remi Gacogne Date: Tue, 7 Nov 2023 10:53:00 +0000 (+0100) Subject: dnsdist: Add support for setting Extended DNS Error statuses X-Git-Tag: rec-5.0.0-rc1~48^2~4 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=f18d8af3e866211851806a5353e9bc4d8e8366bf;p=thirdparty%2Fpdns.git dnsdist: Add support for setting Extended DNS Error statuses This PR adds support for adding EDNS Extended DNS Error statuses from DNSDist, via the following mechanisms: - `SetExtendedDNSErrorAction` - `SetExtendedDNSErrorResponseAction` - `DNSQuestion:setExtendedDNSError(infoCode [, extraText])` - `DNSResponse:setExtendedDNSError(infoCode [, extraText])` - `dnsdist_ffi_dnsquestion_set_extended_dns_error(...)` --- diff --git a/pdns/dnsdist-console.cc b/pdns/dnsdist-console.cc index a49a509147..9dfe8159fe 100644 --- a/pdns/dnsdist-console.cc +++ b/pdns/dnsdist-console.cc @@ -773,6 +773,8 @@ const std::vector g_consoleKeywords{ { "SetECSPrefixLengthAction", true, "v4, v6", "Set the ECS prefix length. Subsequent rules are processed after this action" }, { "SetMacAddrAction", true, "option", "Add the source MAC address to the query as EDNS0 option option. This action is currently only supported on Linux. Subsequent rules are processed after this action" }, { "SetEDNSOptionAction", true, "option, data", "Add arbitrary EDNS option and data to the query. Subsequent rules are processed after this action" }, + { "SetExtendedDNSErrorAction", true, "infoCode [, extraText]", "Set an Extended DNS Error status that will be added to the response corresponding to the current query. Subsequent rules are processed after this action" }, + { "SetExtendedDNSErrorResponseAction", true, "infoCode [, extraText]", "Set an Extended DNS Error status that will be added to this response. Subsequent rules are processed after this action" }, { "SetNoRecurseAction", true, "", "strip RD bit from the question, let it go through" }, { "setOutgoingDoHWorkerThreads", true, "n", "Number of outgoing DoH worker threads" }, { "SetProxyProtocolValuesAction", true, "values", "Set the Proxy-Protocol values for this queries to 'values'" }, diff --git a/pdns/dnsdist-idstate.hh b/pdns/dnsdist-idstate.hh index 158bae03b2..e1fb8d38e1 100644 --- a/pdns/dnsdist-idstate.hh +++ b/pdns/dnsdist-idstate.hh @@ -27,6 +27,7 @@ #include "dnscrypt.hh" #include "dnsname.hh" #include "dnsdist-protocols.hh" +#include "ednsextendederror.hh" #include "gettime.hh" #include "iputils.hh" #include "noinitvector.hh" @@ -131,6 +132,7 @@ struct InternalQueryState std::unique_ptr qTag{nullptr}; // 8 std::unique_ptr d_packet{nullptr}; // Initial packet, so we can restart the query from the response path if needed // 8 std::unique_ptr d_protoBufData{nullptr}; + std::unique_ptr d_extendedError{nullptr}; boost::optional tempFailureTTL{boost::none}; // 8 ClientState* cs{nullptr}; // 8 std::unique_ptr du; // 8 diff --git a/pdns/dnsdist-lua-actions.cc b/pdns/dnsdist-lua-actions.cc index cd9daf9bc8..949e398801 100644 --- a/pdns/dnsdist-lua-actions.cc +++ b/pdns/dnsdist-lua-actions.cc @@ -2238,6 +2238,58 @@ private: double d_ratio{1.0}; }; +class SetExtendedDNSErrorAction : public DNSAction +{ +public: + // this action does not stop the processing + SetExtendedDNSErrorAction(uint16_t infoCode, const std::string& extraText) + { + d_ede.infoCode = infoCode; + d_ede.extraText = extraText; + } + + DNSAction::Action operator()(DNSQuestion* dq, std::string* ruleresult) const override + { + dq->ids.d_extendedError = std::make_unique(d_ede); + + return DNSAction::Action::None; + } + + std::string toString() const override + { + return "set EDNS Extended DNS Error to " + std::to_string(d_ede.infoCode) + (d_ede.extraText.empty() ? std::string() : std::string(": \"") + d_ede.extraText + std::string("\"")); + } + +private: + EDNSExtendedError d_ede; +}; + +class SetExtendedDNSErrorResponseAction : public DNSResponseAction +{ +public: + // this action does not stop the processing + SetExtendedDNSErrorResponseAction(uint16_t infoCode, const std::string& extraText) + { + d_ede.infoCode = infoCode; + d_ede.extraText = extraText; + } + + DNSResponseAction::Action operator()(DNSResponse* dr, std::string* ruleresult) const override + { + dr->ids.d_extendedError = std::make_unique(d_ede); + + return DNSResponseAction::Action::None; + } + + std::string toString() const override + { + return "set EDNS Extended DNS Error to " + std::to_string(d_ede.infoCode) + (d_ede.extraText.empty() ? std::string() : std::string(": \"") + d_ede.extraText + std::string("\"")); + } + +private: + EDNSExtendedError d_ede; +}; + template static void addAction(GlobalStateHolder > *someRuleActions, const luadnsrule_t& var, const std::shared_ptr& action, boost::optional& params) { setLuaSideEffect(); @@ -2773,4 +2825,12 @@ void setupLuaActions(LuaContext& luaCtx) luaCtx.writeFunction("SetAdditionalProxyProtocolValueAction", [](uint8_t type, const std::string& value) { return std::shared_ptr(new SetAdditionalProxyProtocolValueAction(type, value)); }); + + luaCtx.writeFunction("SetExtendedDNSErrorAction", [](uint16_t infoCode, boost::optional extraText) { + return std::shared_ptr(new SetExtendedDNSErrorAction(infoCode, extraText ? *extraText : "")); + }); + + luaCtx.writeFunction("SetExtendedDNSErrorResponseAction", [](uint16_t infoCode, boost::optional extraText) { + return std::shared_ptr(new SetExtendedDNSErrorResponseAction(infoCode, extraText ? *extraText : "")); + }); } diff --git a/pdns/dnsdist-lua-bindings-dnsquestion.cc b/pdns/dnsdist-lua-bindings-dnsquestion.cc index 314e6b2cf4..0bec91aac8 100644 --- a/pdns/dnsdist-lua-bindings-dnsquestion.cc +++ b/pdns/dnsdist-lua-bindings-dnsquestion.cc @@ -258,6 +258,15 @@ void setupLuaBindingsDNSQuestion(LuaContext& luaCtx) setEDNSOption(dq, code, data); }); + luaCtx.registerFunction& extraText)>("setExtendedDNSError", [](DNSQuestion& dq, uint16_t infoCode, const boost::optional& extraText) { + EDNSExtendedError ede; + ede.infoCode = infoCode; + if (extraText) { + ede.extraText = *extraText; + } + dq.ids.d_extendedError = std::make_unique(ede); + }); + luaCtx.registerFunction("suspend", [](DNSQuestion& dq, uint16_t asyncID, uint16_t queryID, uint32_t timeoutMs) { dq.asynchronous = true; return dnsdist::suspendQuery(dq, asyncID, queryID, timeoutMs); @@ -506,6 +515,15 @@ private: return setNegativeAndAdditionalSOA(dq, nxd, DNSName(zone), ttl, DNSName(mname), DNSName(rname), serial, refresh, retry, expire, minimum, false); }); + luaCtx.registerFunction& extraText)>("setExtendedDNSError", [](DNSResponse& dr, uint16_t infoCode, const boost::optional& extraText) { + EDNSExtendedError ede; + ede.infoCode = infoCode; + if (extraText) { + ede.extraText = *extraText; + } + dr.ids.d_extendedError = std::make_unique(ede); + }); + luaCtx.registerFunction("suspend", [](DNSResponse& dr, uint16_t asyncID, uint16_t queryID, uint32_t timeoutMs) { dr.asynchronous = true; return dnsdist::suspendResponse(dr, asyncID, queryID, timeoutMs); diff --git a/pdns/dnsdist.cc b/pdns/dnsdist.cc index dc490d2e22..00a2569426 100644 --- a/pdns/dnsdist.cc +++ b/pdns/dnsdist.cc @@ -56,6 +56,7 @@ #include "dnsdist-dnsparser.hh" #include "dnsdist-dynblocks.hh" #include "dnsdist-ecs.hh" +#include "dnsdist-edns.hh" #include "dnsdist-healthchecks.hh" #include "dnsdist-lua.hh" #include "dnsdist-nghttp2.hh" @@ -580,6 +581,10 @@ bool processResponseAfterRules(PacketBuffer& response, const std::vectorinfoCode, dr.ids.d_extendedError->extraText); + } + #ifdef HAVE_DNSCRYPT if (!muted) { if (!encryptResponse(response, dr.getMaximumSize(), dr.overTCP(), dr.ids.dnsCryptQuery)) { @@ -1326,6 +1331,10 @@ static bool prepareOutgoingResponse(LocalHolders& holders, const ClientState& cs ac(&dr, &result); } + if (dr.ids.d_extendedError) { + dnsdist::edns::addExtendedDNSError(dr.getMutableData(), dr.getMaximumSize(), dr.ids.d_extendedError->infoCode, dr.ids.d_extendedError->extraText); + } + if (cacheHit) { ++dnsdist::metrics::g_stats.cacheHits; } diff --git a/pdns/dnsdistdist/Makefile.am b/pdns/dnsdistdist/Makefile.am index 1f64134566..d463c5bfb8 100644 --- a/pdns/dnsdistdist/Makefile.am +++ b/pdns/dnsdistdist/Makefile.am @@ -214,6 +214,7 @@ dnsdist_SOURCES = \ dolog.hh \ doq.hh \ ednscookies.cc ednscookies.hh \ + ednsextendederror.cc ednsextendederror.hh \ ednsoptions.cc ednsoptions.hh \ ednssubnet.cc ednssubnet.hh \ ext/json11/json11.cpp \ diff --git a/pdns/dnsdistdist/dnsdist-edns.cc b/pdns/dnsdistdist/dnsdist-edns.cc index 1131bf29bc..0aa8539713 100644 --- a/pdns/dnsdistdist/dnsdist-edns.cc +++ b/pdns/dnsdistdist/dnsdist-edns.cc @@ -22,6 +22,7 @@ #include "dnsdist-ecs.hh" #include "dnsdist-edns.hh" #include "ednsoptions.hh" +#include "ednsextendederror.hh" namespace dnsdist::edns { @@ -55,4 +56,39 @@ std::pair, std::optional> getExtendedDNSErr } return {infoCode, std::move(extraText)}; } + +bool addExtendedDNSError(PacketBuffer& packet, size_t maximumPacketSize, uint16_t code, const std::string& extraStatus) +{ + uint16_t optStart = 0; + size_t optLen = 0; + bool last = false; + + int res = locateEDNSOptRR(packet, &optStart, &optLen, &last); + + if (res != 0) { + /* no EDNS OPT record in the response, something is not right */ + return false; + } + + EDNSExtendedError ede{.infoCode = code, .extraText = extraStatus}; + auto edeOptionPayload = makeEDNSExtendedErrorOptString(ede); + std::string edeOption; + generateEDNSOption(EDNSOptionCode::EXTENDEDERROR, edeOptionPayload, edeOption); + + /* we might have one record after the OPT one, we need to rewrite + the whole packet because of compression */ + PacketBuffer newContent; + bool ednsAdded = false; + bool edeAdded = false; + if (!slowRewriteEDNSOptionInQueryWithRecords(packet, newContent, ednsAdded, EDNSOptionCode::EXTENDEDERROR, edeAdded, true, edeOption)) { + return false; + } + + if (newContent.size() > maximumPacketSize) { + return false; + } + + packet = std::move(newContent); + return true; +} } diff --git a/pdns/dnsdistdist/dnsdist-edns.hh b/pdns/dnsdistdist/dnsdist-edns.hh index 75c92c143a..8e60e5b049 100644 --- a/pdns/dnsdistdist/dnsdist-edns.hh +++ b/pdns/dnsdistdist/dnsdist-edns.hh @@ -30,4 +30,5 @@ namespace dnsdist::edns { std::pair, std::optional> getExtendedDNSError(const PacketBuffer& packet); +bool addExtendedDNSError(PacketBuffer& packet, size_t maximumPacketSize, uint16_t code, const std::string& extraStatus); } diff --git a/pdns/dnsdistdist/dnsdist-lua-ffi-interface.h b/pdns/dnsdistdist/dnsdist-lua-ffi-interface.h index e7cc8fe873..38689663c1 100644 --- a/pdns/dnsdistdist/dnsdist-lua-ffi-interface.h +++ b/pdns/dnsdistdist/dnsdist-lua-ffi-interface.h @@ -117,6 +117,8 @@ void dnsdist_ffi_dnsquestion_set_device_name(dnsdist_ffi_dnsquestion_t* dq, cons void dnsdist_ffi_dnsquestion_set_http_response(dnsdist_ffi_dnsquestion_t* dq, uint16_t statusCode, const char* body, size_t bodyLen, const char* contentType) __attribute__ ((visibility ("default"))); +void dnsdist_ffi_dnsquestion_set_extended_dns_error(dnsdist_ffi_dnsquestion_t* dq, uint16_t infoCode, const char* extraText, size_t extraTextSize) __attribute__ ((visibility ("default"))); + size_t dnsdist_ffi_dnsquestion_get_trailing_data(dnsdist_ffi_dnsquestion_t* dq, const char** out) __attribute__ ((visibility ("default"))); bool dnsdist_ffi_dnsquestion_set_trailing_data(dnsdist_ffi_dnsquestion_t* dq, const char* data, size_t dataLen) __attribute__ ((visibility ("default"))); diff --git a/pdns/dnsdistdist/dnsdist-lua-ffi.cc b/pdns/dnsdistdist/dnsdist-lua-ffi.cc index 70f0ff2ab0..d2d960a947 100644 --- a/pdns/dnsdistdist/dnsdist-lua-ffi.cc +++ b/pdns/dnsdistdist/dnsdist-lua-ffi.cc @@ -465,6 +465,16 @@ void dnsdist_ffi_dnsquestion_set_http_response(dnsdist_ffi_dnsquestion_t* dq, ui #endif } +void dnsdist_ffi_dnsquestion_set_extended_dns_error(dnsdist_ffi_dnsquestion_t* dq, uint16_t infoCode, const char* extraText, size_t extraTextSize) +{ + EDNSExtendedError ede; + ede.infoCode = infoCode; + if (extraText != nullptr && extraTextSize > 0) { + ede.extraText = std::string(extraText, extraTextSize); + } + dq->dq->ids.d_extendedError = std::make_unique(ede); +} + void dnsdist_ffi_dnsquestion_set_rcode(dnsdist_ffi_dnsquestion_t* dq, int rcode) { dnsdist::PacketMangling::editDNSHeaderFromPacket(dq->dq->getMutableData(), [rcode](dnsheader& header) { diff --git a/pdns/dnsdistdist/docs/reference/dq.rst b/pdns/dnsdistdist/docs/reference/dq.rst index 33e209e553..4aa675fb1e 100644 --- a/pdns/dnsdistdist/docs/reference/dq.rst +++ b/pdns/dnsdistdist/docs/reference/dq.rst @@ -279,6 +279,15 @@ This state can be modified from the various hooks. :param int code: The EDNS option code :param string data: The EDNS option raw data + .. method:: DNSQuestion:setExtendedDNSError(infoCode [, extraText]) + + .. versionadded:: 1.9.0 + + Set an Extended DNS Error status that will be added to the response corresponding to the current query. + + :param int infoCode: The EDNS Extended DNS Error code + :param string extraText: The optional EDNS Extended DNS Error extra text + .. method:: DNSQuestion:setHTTPResponse(status, body, contentType="") .. versionadded:: 1.4.0 diff --git a/pdns/dnsdistdist/docs/rules-actions.rst b/pdns/dnsdistdist/docs/rules-actions.rst index 2fb0081eb6..090b5a7701 100644 --- a/pdns/dnsdistdist/docs/rules-actions.rst +++ b/pdns/dnsdistdist/docs/rules-actions.rst @@ -938,10 +938,6 @@ Some actions allow further processing of rules, this is noted in their descripti - :func:`NoneAction` - :func:`RemoteLogAction` - :func:`RemoteLogResponseAction` -- :func:`SetMaxReturnedTTLResponseAction` -- :func:`SetMaxReturnedTTLAction` -- :func:`SetMinTTLResponseAction` -- :func:`SetMaxTTLResponseAction` - :func:`SNMPTrapAction` - :func:`SNMPTrapResponseAction` - :func:`TeeAction` @@ -1515,6 +1511,26 @@ The following actions exist. :param int option: The EDNS option number :param string data: The EDNS0 option raw content +.. function:: SetExtendedDNSErrorAction(infoCode [, extraText]) + + .. versionadded:: 1.9.0 + + Set an Extended DNS Error status that will be added to the response corresponding to the current query. + Subsequent rules are processed after this action. + + :param int infoCode: The EDNS Extended DNS Error code + :param string extraText: The optional EDNS Extended DNS Error extra text + +.. function:: SetExtendedDNSErrorResponseAction(infoCode [, extraText]) + + .. versionadded:: 1.9.0 + + Set an Extended DNS Error status that will be added to this response. + Subsequent rules are processed after this action. + + :param int infoCode: The EDNS Extended DNS Error code + :param string extraText: The optional EDNS Extended DNS Error extra text + .. function:: SetMacAddrAction(option) .. versionadded:: 1.6.0 diff --git a/regression-tests.dnsdist/test_EDE.py b/regression-tests.dnsdist/test_EDE.py new file mode 100644 index 0000000000..a681424f7e --- /dev/null +++ b/regression-tests.dnsdist/test_EDE.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python +import extendederrors +import dns +from dnsdisttests import DNSDistTest, pickAvailablePort + +class TestBasics(DNSDistTest): + + _config_template = """ + newServer{address="127.0.0.1:%s"} + pc = newPacketCache(100, {maxTTL=86400, minTTL=1}) + getPool(""):setCache(pc) + + local ffi = require("ffi") + function ffiAction(dq) + local extraText = 'Synthesized from Lua' + ffi.C.dnsdist_ffi_dnsquestion_set_extended_dns_error(dq, 29, extraText, #extraText) + local str = "192.0.2.2" + local buf = ffi.new("char[?]", #str + 1) + ffi.copy(buf, str) + ffi.C.dnsdist_ffi_dnsquestion_set_result(dq, buf, #str) + return DNSAction.Spoof + end + + addAction("self-answered.ede.tests.powerdns.com.", SpoofAction("192.0.2.1")) + addAction("self-answered-ffi.ede.tests.powerdns.com.", LuaFFIAction(ffiAction)) + addSelfAnsweredResponseAction("self-answered.ede.tests.powerdns.com.", SetExtendedDNSErrorResponseAction(42, "my self-answered extended error status")) + addAction(AllRule(), SetExtendedDNSErrorAction(16, "my extended error status")) + + """ + + def testExtendedErrorNoEDNS(self): + """ + EDE: No EDNS + """ + name = 'no-edns.ede.tests.powerdns.com.' + # no EDNS + query = dns.message.make_query(name, 'A', 'IN', use_edns=False) + response = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + 60, + 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) + receivedQuery.id = query.id + self.assertEqual(query, receivedQuery) + self.checkResponseNoEDNS(response, receivedResponse) + + def testExtendedErrorBackendResponse(self): + """ + EDE: Backend response + """ + name = 'backend-response.ede.tests.powerdns.com.' + ede = extendederrors.ExtendedErrorOption(16, b'my extended error status') + query = dns.message.make_query(name, 'A', 'IN', use_edns=True) + + backendResponse = dns.message.make_response(query) + backendResponse.use_edns(edns=True, payload=4096, options=[]) + rrset = dns.rrset.from_text(name, + 60, + dns.rdataclass.IN, + dns.rdatatype.A, + '127.0.0.1') + + backendResponse.answer.append(rrset) + expectedResponse = dns.message.make_response(query) + expectedResponse.use_edns(edns=True, payload=4096, options=[ede]) + rrset = dns.rrset.from_text(name, + 60, + dns.rdataclass.IN, + dns.rdatatype.A, + '127.0.0.1') + expectedResponse.answer.append(rrset) + + for method in ("sendUDPQuery", "sendTCPQuery"): + sender = getattr(self, method) + (receivedQuery, receivedResponse) = sender(query, backendResponse) + receivedQuery.id = query.id + self.assertEqual(query, receivedQuery) + self.checkMessageEDNS(expectedResponse, receivedResponse) + + # testing the cache + for method in ("sendUDPQuery", "sendTCPQuery"): + sender = getattr(self, method) + (_, receivedResponse) = sender(query, response=None, useQueue=False) + self.checkMessageEDNS(expectedResponse, receivedResponse) + + def testExtendedErrorSelfAnswered(self): + """ + EDE: Self-answered + """ + name = 'self-answered.ede.tests.powerdns.com.' + ede = extendederrors.ExtendedErrorOption(42, b'my self-answered extended error status') + query = dns.message.make_query(name, 'A', 'IN', use_edns=True) + # dnsdist sets RA = RD for self-generated responses + query.flags &= ~dns.flags.RD + + expectedResponse = dns.message.make_response(query) + expectedResponse.use_edns(edns=True, payload=1232, options=[ede]) + rrset = dns.rrset.from_text(name, + 60, + dns.rdataclass.IN, + dns.rdatatype.A, + '192.0.2.1') + expectedResponse.answer.append(rrset) + + for method in ("sendUDPQuery", "sendTCPQuery"): + sender = getattr(self, method) + (_, receivedResponse) = sender(query, response=None, useQueue=False) + self.checkMessageEDNS(expectedResponse, receivedResponse) + + def testExtendedErrorLuaFFI(self): + """ + EDE: Self-answered via Lua FFI + """ + name = 'self-answered-ffi.ede.tests.powerdns.com.' + ede = extendederrors.ExtendedErrorOption(29, b'Synthesized from Lua') + query = dns.message.make_query(name, 'A', 'IN', use_edns=True) + # dnsdist sets RA = RD for self-generated responses + query.flags &= ~dns.flags.RD + + expectedResponse = dns.message.make_response(query) + expectedResponse.use_edns(edns=True, payload=1232, options=[ede]) + rrset = dns.rrset.from_text(name, + 60, + dns.rdataclass.IN, + dns.rdatatype.A, + '192.0.2.2') + expectedResponse.answer.append(rrset) + + for method in ("sendUDPQuery", "sendTCPQuery"): + sender = getattr(self, method) + (_, receivedResponse) = sender(query, response=None, useQueue=False) + self.checkMessageEDNS(expectedResponse, receivedResponse)