From af9f750cb259fc1c908174b00ceb09f46b782ae0 Mon Sep 17 00:00:00 2001 From: Remi Gacogne Date: Tue, 6 Aug 2019 17:44:56 +0200 Subject: [PATCH] dnsdist: Add SetNegativeAndSOAAction() and its Lua binding --- pdns/dnsdist-console.cc | 1 + pdns/dnsdist-ecs.cc | 100 ++++++++++++++++++++-- pdns/dnsdist-ecs.hh | 1 + pdns/dnsdist-lua-actions.cc | 38 ++++++++ pdns/dnsdist-lua-bindings-dnsquestion.cc | 4 + pdns/dnsdistdist/docs/reference/dq.rst | 17 ++++ pdns/dnsdistdist/docs/rules-actions.rst | 17 ++++ pdns/dnsname.hh | 2 +- pdns/test-dnsdist_cc.cc | 70 +++++++++++++++ regression-tests.dnsdist/test_Advanced.py | 48 +++++++++++ 10 files changed, 292 insertions(+), 6 deletions(-) diff --git a/pdns/dnsdist-console.cc b/pdns/dnsdist-console.cc index 8baa664653..d3a8b06e24 100644 --- a/pdns/dnsdist-console.cc +++ b/pdns/dnsdist-console.cc @@ -515,6 +515,7 @@ const std::vector g_consoleKeywords{ { "setMaxTCPQueriesPerConnection", true, "n", "set the maximum number of queries in an incoming TCP connection. 0 means unlimited" }, { "setMaxTCPQueuedConnections", true, "n", "set the maximum number of TCP connections queued (waiting to be picked up by a client thread)" }, { "setMaxUDPOutstanding", true, "n", "set the maximum number of outstanding UDP queries to a given backend server. This can only be set at configuration time and defaults to 65535" }, + { "SetNegativeAndSOAAction", "true", "nxd, zone, ttl, mname, rname, serial, refresh, retry, expire, minimum", "Turn a query into a NXDomain or NoData answer and sets a SOA record in the additional section" }, { "setPayloadSizeOnSelfGeneratedAnswers", true, "payloadSize", "set the UDP payload size advertised via EDNS on self-generated responses" }, { "setPoolServerPolicy", true, "policy, pool", "set the server selection policy for this pool to that policy" }, { "setPoolServerPolicyLua", true, "name, func, pool", "set the server selection policy for this pool to one named 'name' and provided by 'function'" }, diff --git a/pdns/dnsdist-ecs.cc b/pdns/dnsdist-ecs.cc index b1b43c111a..7cda963d4a 100644 --- a/pdns/dnsdist-ecs.cc +++ b/pdns/dnsdist-ecs.cc @@ -839,10 +839,6 @@ int rewriteResponseWithoutEDNSOption(const std::string& initialPacket, const uin bool addEDNS(dnsheader* dh, uint16_t& len, const size_t size, bool dnssecOK, uint16_t payloadSize, uint8_t ednsrcode) { - if (dh->arcount != 0) { - return false; - } - std::string optRecord; generateOptRR(std::string(), optRecord, payloadSize, ednsrcode, dnssecOK); @@ -853,7 +849,101 @@ bool addEDNS(dnsheader* dh, uint16_t& len, const size_t size, bool dnssecOK, uin char * optPtr = reinterpret_cast(dh) + len; memcpy(optPtr, optRecord.data(), optRecord.size()); len += optRecord.size(); - dh->arcount = htons(1); + dh->arcount = htons(ntohs(dh->arcount) + 1); + + return true; +} + +/* + This function keeps the existing header and DNSSECOK bit (if any) but wipes anything else, + generating a NXD or NODATA answer with a SOA record in the additional section. +*/ +bool setNegativeAndAdditionalSOA(DNSQuestion& dq, bool nxd, const DNSName& zone, uint32_t ttl, const DNSName& mname, const DNSName& rname, uint32_t serial, uint32_t refresh, uint32_t retry, uint32_t expire, uint32_t minimum) +{ + if (ntohs(dq.dh->qdcount) != 1) { + return false; + } + + assert(dq.consumed == dq.qname->wirelength()); + size_t queryPartSize = sizeof(dnsheader) + dq.consumed + DNS_TYPE_SIZE + DNS_CLASS_SIZE; + if (dq.len < queryPartSize) { + /* something is already wrong, don't build on flawed foundations */ + return false; + } + + size_t available = dq.size - queryPartSize; + uint16_t qtype = htons(QType::SOA); + uint16_t qclass = htons(QClass::IN); + uint16_t rdLength = mname.wirelength() + rname.wirelength() + sizeof(serial) + sizeof(refresh) + sizeof(retry) + sizeof(expire) + sizeof(minimum); + size_t soaSize = zone.wirelength() + sizeof(qtype) + sizeof(qclass) + sizeof(ttl) + sizeof(rdLength) + rdLength; + + if (soaSize > available) { + /* not enough space left to add the SOA, sorry! */ + return false; + } + + bool hadEDNS = false; + bool dnssecOK = false; + + if (g_addEDNSToSelfGeneratedResponses) { + uint16_t payloadSize = 0; + uint16_t z = 0; + hadEDNS = getEDNSUDPPayloadSizeAndZ(reinterpret_cast(dq.dh), dq.len, &payloadSize, &z); + if (hadEDNS) { + dnssecOK = z & EDNS_HEADER_FLAG_DO; + } + } + + /* chop off everything after the question */ + dq.len = queryPartSize; + if (nxd) { + dq.dh->rcode = RCode::NXDomain; + } + else { + dq.dh->rcode = RCode::NoError; + } + dq.dh->qr = true; + dq.dh->ancount = 0; + dq.dh->nscount = 0; + dq.dh->arcount = 0; + + rdLength = htons(rdLength); + ttl = htonl(ttl); + serial = htonl(serial); + refresh = htonl(refresh); + retry = htonl(retry); + expire = htonl(expire); + minimum = htonl(minimum); + + std::string soa; + soa.reserve(soaSize); + soa.append(zone.toDNSString()); + soa.append(reinterpret_cast(&qtype), sizeof(qtype)); + soa.append(reinterpret_cast(&qclass), sizeof(qclass)); + soa.append(reinterpret_cast(&ttl), sizeof(ttl)); + soa.append(reinterpret_cast(&rdLength), sizeof(rdLength)); + soa.append(mname.toDNSString()); + soa.append(rname.toDNSString()); + soa.append(reinterpret_cast(&serial), sizeof(serial)); + soa.append(reinterpret_cast(&refresh), sizeof(refresh)); + soa.append(reinterpret_cast(&retry), sizeof(retry)); + soa.append(reinterpret_cast(&expire), sizeof(expire)); + soa.append(reinterpret_cast(&minimum), sizeof(minimum)); + + if (soa.size() != soaSize) { + throw std::runtime_error("Unexpected SOA response size: " + std::to_string(soa.size()) + " vs " + std::to_string(soaSize)); + } + + memcpy(reinterpret_cast(dq.dh) + queryPartSize, soa.c_str(), soa.size()); + + dq.len += soa.size(); + + dq.dh->arcount = htons(1); + + if (g_addEDNSToSelfGeneratedResponses) { + /* now we need to add a new OPT record */ + return addEDNS(dq.dh, dq.len, dq.size, dnssecOK, g_PayloadSizeSelfGenAnswers, dq.ednsRCode); + } return true; } diff --git a/pdns/dnsdist-ecs.hh b/pdns/dnsdist-ecs.hh index b339269064..293bc0cd92 100644 --- a/pdns/dnsdist-ecs.hh +++ b/pdns/dnsdist-ecs.hh @@ -37,6 +37,7 @@ int getEDNSOptionsStart(const char* packet, const size_t offset, const size_t le bool isEDNSOptionInOpt(const std::string& packet, const size_t optStart, const size_t optLen, const uint16_t optionCodeToFind, size_t* optContentStart = nullptr, uint16_t* optContentLen = nullptr); bool addEDNS(dnsheader* dh, uint16_t& len, const size_t size, bool dnssecOK, uint16_t payloadSize, uint8_t ednsrcode); bool addEDNSToQueryTurnedResponse(DNSQuestion& dq); +bool setNegativeAndAdditionalSOA(DNSQuestion& dq, bool nxd, const DNSName& zone, uint32_t ttl, const DNSName& mname, const DNSName& rname, uint32_t serial, uint32_t refresh, uint32_t retry, uint32_t expire, uint32_t minimum); bool handleEDNSClientSubnet(DNSQuestion& dq, bool& ednsAdded, bool& ecsAdded, bool preserveTrailingData); bool handleEDNSClientSubnet(char* packet, size_t packetSize, unsigned int consumed, uint16_t* len, bool& ednsAdded, bool& ecsAdded, bool overrideExisting, const string& newECSOption, bool preserveTrailingData); diff --git a/pdns/dnsdist-lua-actions.cc b/pdns/dnsdist-lua-actions.cc index 689d9ace4f..425827bbb1 100644 --- a/pdns/dnsdist-lua-actions.cc +++ b/pdns/dnsdist-lua-actions.cc @@ -1258,6 +1258,40 @@ private: std::string d_tag; }; +class SetNegativeAndSOAAction: public DNSAction +{ +public: + SetNegativeAndSOAAction(bool nxd, const DNSName& zone, uint32_t ttl, const DNSName& mname, const DNSName& rname, uint32_t serial, uint32_t refresh, uint32_t retry, uint32_t expire, uint32_t minimum): d_zone(zone), d_mname(mname), d_rname(rname), d_ttl(ttl), d_serial(serial), d_refresh(refresh), d_retry(retry), d_expire(expire), d_minimum(minimum), d_nxd(nxd) + { + } + + DNSAction::Action operator()(DNSQuestion* dq, std::string* ruleresult) const override + { + if (!setNegativeAndAdditionalSOA(*dq, d_nxd, d_zone, d_ttl, d_mname, d_rname, d_serial, d_refresh, d_retry, d_expire, d_minimum)) { + return Action::None; + } + + return Action::Allow; + } + + std::string toString() const override + { + return std::string(d_nxd ? "NXD " : "NODATA") + " with SOA"; + } + +private: + DNSName d_zone; + DNSName d_mname; + DNSName d_rname; + uint32_t d_ttl; + uint32_t d_serial; + uint32_t d_refresh; + uint32_t d_retry; + uint32_t d_expire; + uint32_t d_minimum; + bool d_nxd; +}; + template static void addAction(GlobalStateHolder > *someRulActions, const luadnsrule_t& var, const std::shared_ptr& action, boost::optional& params) { setLuaSideEffect(); @@ -1642,4 +1676,8 @@ void setupLuaActions() g_lua.writeFunction("KeyValueStoreLookupAction", [](std::shared_ptr& kvs, std::shared_ptr& lookupKey, const std::string& destinationTag) { return std::shared_ptr(new KeyValueStoreLookupAction(kvs, lookupKey, destinationTag)); }); + + g_lua.writeFunction("SetNegativeAndSOAAction", [](bool nxd, const std::string& zone, uint32_t ttl, const std::string& mname, const std::string& rname, uint32_t serial, uint32_t refresh, uint32_t retry, uint32_t expire, uint32_t minimum) { + return std::shared_ptr(new SetNegativeAndSOAAction(nxd, DNSName(zone), ttl, DNSName(mname), DNSName(rname), serial, refresh, retry, expire, minimum)); + }); } diff --git a/pdns/dnsdist-lua-bindings-dnsquestion.cc b/pdns/dnsdist-lua-bindings-dnsquestion.cc index 1b4709cbfc..eff46f3ab5 100644 --- a/pdns/dnsdist-lua-bindings-dnsquestion.cc +++ b/pdns/dnsdist-lua-bindings-dnsquestion.cc @@ -259,4 +259,8 @@ void setupLuaBindingsDNSQuestion() dq.du->setHTTPResponse(statusCode, body, contentType ? *contentType : ""); }); #endif /* HAVE_DNS_OVER_HTTPS */ + + g_lua.registerFunction("setNegativeAndAdditionalSOA", [](DNSQuestion& dq, bool nxd, const std::string& zone, uint32_t ttl, const std::string& mname, const std::string& rname, uint32_t serial, uint32_t refresh, uint32_t retry, uint32_t expire, uint32_t minimum) { + return setNegativeAndAdditionalSOA(dq, nxd, DNSName(zone), ttl, DNSName(mname), DNSName(rname), serial, refresh, retry, expire, minimum); + }); } diff --git a/pdns/dnsdistdist/docs/reference/dq.rst b/pdns/dnsdistdist/docs/reference/dq.rst index 53e7eb053c..da2672f753 100644 --- a/pdns/dnsdistdist/docs/reference/dq.rst +++ b/pdns/dnsdistdist/docs/reference/dq.rst @@ -187,6 +187,23 @@ This state can be modified from the various hooks. :param string body: The body of the HTTP response, or a URL if the status code is a redirect (3xx) :param string contentType: The HTTP Content-Type header to return for a 200 response, ignored otherwise. Default is ''application/dns-message''. + .. method:: DNSQuestion:setNegativeAndAdditionalSOA(nxd, zone, ttl, mname, rname, serial, refresh, retry, expire, minimum) + + .. versionadded:: 1.5.0 + + Turn a question into a response, either a NXDOMAIN or a NODATA one based on ''nxd'', setting the QR bit to 1 and adding a SOA record in the additional section. + + :param bool nxd: Whether the answer is a NXDOMAIN (true) or a NODATA (false) + :param string zone: The owner name for the SOA record + :param int ttl: The TTL of the SOA record + :param string mname: The mname of the SOA record + :param string rname: The rname of the SOA record + :param int serial: The value of the serial field in the SOA record + :param int refresh: The value of the refresh field in the SOA record + :param int retry: The value of the retry field in the SOA record + :param int expire: The value of the expire field in the SOA record + :param int minimum: The value of the minimum field in the SOA record + .. method:: DNSQuestion:setTag(key, value) .. versionadded:: 1.2.0 diff --git a/pdns/dnsdistdist/docs/rules-actions.rst b/pdns/dnsdistdist/docs/rules-actions.rst index d1e18c5703..4197e89efe 100644 --- a/pdns/dnsdistdist/docs/rules-actions.rst +++ b/pdns/dnsdistdist/docs/rules-actions.rst @@ -1187,6 +1187,23 @@ The following actions exist. :param string v4: The IPv4 netmask, for example "192.0.2.1/32" :param string v6: The IPv6 netmask, if any +.. function:: SetNegativeAndSOAAction(nxd, zone, ttl, mname, rname, serial, refresh, retry, expire, minimum) + + .. versionadded:: 1.5.0 + + Turn a question into a response, either a NXDOMAIN or a NODATA one based on ''nxd'', setting the QR bit to 1 and adding a SOA record in the additional section. + + :param bool nxd: Whether the answer is a NXDOMAIN (true) or a NODATA (false) + :param string zone: The owner name for the SOA record + :param int ttl: The TTL of the SOA record + :param string mname: The mname of the SOA record + :param string rname: The rname of the SOA record + :param int serial: The value of the serial field in the SOA record + :param int refresh: The value of the refresh field in the SOA record + :param int retry: The value of the retry field in the SOA record + :param int expire: The value of the expire field in the SOA record + :param int minimum: The value of the minimum field in the SOA record + .. function:: SkipCacheAction() Don't lookup the cache for this query, don't store the answer. diff --git a/pdns/dnsname.hh b/pdns/dnsname.hh index da6c4de464..5028273070 100644 --- a/pdns/dnsname.hh +++ b/pdns/dnsname.hh @@ -64,7 +64,7 @@ public: DNSName() {} //!< Constructs an *empty* DNSName, NOT the root! explicit DNSName(const char* p); //!< Constructs from a human formatted, escaped presentation explicit DNSName(const std::string& str) : DNSName(str.c_str()) {}; //!< Constructs from a human formatted, escaped presentation - DNSName(const char* p, int len, int offset, bool uncompress, uint16_t* qtype=0, uint16_t* qclass=0, unsigned int* consumed=0, uint16_t minOffset=0); //!< Construct from a DNS Packet, taking the first question if offset=12 + DNSName(const char* p, int len, int offset, bool uncompress, uint16_t* qtype=nullptr, uint16_t* qclass=nullptr, unsigned int* consumed=nullptr, uint16_t minOffset=0); //!< Construct from a DNS Packet, taking the first question if offset=12. If supplied, consumed is set to the number of bytes consumed from the packet, which will not be equal to the wire length of the resulting name in case of compression. bool isPartOf(const DNSName& rhs) const; //!< Are we part of the rhs name? inline bool operator==(const DNSName& rhs) const; //!< DNS-native comparison (case insensitive) - empty compares to empty diff --git a/pdns/test-dnsdist_cc.cc b/pdns/test-dnsdist_cc.cc index 3a5bd23c80..bc06bebf82 100644 --- a/pdns/test-dnsdist_cc.cc +++ b/pdns/test-dnsdist_cc.cc @@ -1915,4 +1915,74 @@ BOOST_AUTO_TEST_CASE(test_isEDNSOptionInOpt) { } } +BOOST_AUTO_TEST_CASE(test_setNegativeAndAdditionalSOA) { + struct timespec queryTime; + gettime(&queryTime); // does not have to be accurate ("realTime") in tests + ComboAddress remote; + DNSName name("www.powerdns.com."); + + vector query; + DNSPacketWriter pw(query, name, QType::A, QClass::IN, 0); + pw.getHeader()->rd = 1; + const uint16_t len = query.size(); + + /* test NXD */ + { + char packet[1500]; + memcpy(packet, query.data(), query.size()); + + unsigned int consumed = 0; + uint16_t qtype; + DNSName qname(packet, len, sizeof(dnsheader), false, &qtype, nullptr, &consumed); + auto dh = reinterpret_cast(packet); + DNSQuestion dq(&qname, qtype, QClass::IN, qname.wirelength(), &remote, &remote, dh, sizeof(packet), query.size(), false, &queryTime); + + BOOST_CHECK(setNegativeAndAdditionalSOA(dq, true, DNSName("zone."), 42, DNSName("mname."), DNSName("rname."), 1, 2, 3, 4 , 5)); + BOOST_CHECK(static_cast(dq.len) > query.size()); + MOADNSParser mdp(true, packet, dq.len); + + BOOST_CHECK_EQUAL(mdp.d_qname.toString(), "www.powerdns.com."); + BOOST_CHECK_EQUAL(mdp.d_header.rcode, RCode::NXDomain); + BOOST_CHECK_EQUAL(mdp.d_header.qdcount, 1); + BOOST_CHECK_EQUAL(mdp.d_header.ancount, 0); + BOOST_CHECK_EQUAL(mdp.d_header.nscount, 0); + BOOST_CHECK_EQUAL(mdp.d_header.arcount, 2); + BOOST_REQUIRE_EQUAL(mdp.d_answers.size(), 2); + BOOST_CHECK_EQUAL(mdp.d_answers.at(0).first.d_type, static_cast(QType::SOA)); + BOOST_CHECK_EQUAL(mdp.d_answers.at(0).first.d_class, QClass::IN); + BOOST_CHECK_EQUAL(mdp.d_answers.at(0).first.d_name, DNSName("zone.")); + BOOST_CHECK_EQUAL(mdp.d_answers.at(1).first.d_type, static_cast(QType::OPT)); + BOOST_CHECK_EQUAL(mdp.d_answers.at(1).first.d_name, g_rootdnsname); + } + + /* test No Data */ + { + char packet[1500]; + memcpy(packet, query.data(), query.size()); + + unsigned int consumed = 0; + uint16_t qtype; + DNSName qname(packet, len, sizeof(dnsheader), false, &qtype, nullptr, &consumed); + auto dh = reinterpret_cast(packet); + DNSQuestion dq(&qname, qtype, QClass::IN, qname.wirelength(), &remote, &remote, dh, sizeof(packet), query.size(), false, &queryTime); + + BOOST_CHECK(setNegativeAndAdditionalSOA(dq, false, DNSName("zone."), 42, DNSName("mname."), DNSName("rname."), 1, 2, 3, 4 , 5)); + BOOST_CHECK(static_cast(dq.len) > query.size()); + MOADNSParser mdp(true, packet, dq.len); + + BOOST_CHECK_EQUAL(mdp.d_qname.toString(), "www.powerdns.com."); + BOOST_CHECK_EQUAL(mdp.d_header.rcode, RCode::NoError); + BOOST_CHECK_EQUAL(mdp.d_header.qdcount, 1); + BOOST_CHECK_EQUAL(mdp.d_header.ancount, 0); + BOOST_CHECK_EQUAL(mdp.d_header.nscount, 0); + BOOST_CHECK_EQUAL(mdp.d_header.arcount, 2); + BOOST_REQUIRE_EQUAL(mdp.d_answers.size(), 2); + BOOST_CHECK_EQUAL(mdp.d_answers.at(0).first.d_type, static_cast(QType::SOA)); + BOOST_CHECK_EQUAL(mdp.d_answers.at(0).first.d_class, QClass::IN); + BOOST_CHECK_EQUAL(mdp.d_answers.at(0).first.d_name, DNSName("zone.")); + BOOST_CHECK_EQUAL(mdp.d_answers.at(1).first.d_type, static_cast(QType::OPT)); + BOOST_CHECK_EQUAL(mdp.d_answers.at(1).first.d_name, g_rootdnsname); + } +} + BOOST_AUTO_TEST_SUITE_END(); diff --git a/regression-tests.dnsdist/test_Advanced.py b/regression-tests.dnsdist/test_Advanced.py index 6be7d4f3c0..e4d4c636dd 100644 --- a/regression-tests.dnsdist/test_Advanced.py +++ b/regression-tests.dnsdist/test_Advanced.py @@ -1842,3 +1842,51 @@ class TestAdvancedContinueAction(DNSDistTest): print(receivedResponse) print(expectedResponse) self.assertEquals(receivedResponse, expectedResponse) + +class TestAdvancedSetNegativeAndSOA(DNSDistTest): + + _config_template = """ + addAction("nxd.setnegativeandsoa.advanced.tests.powerdns.com.", SetNegativeAndSOAAction(true, "auth.", 42, "mname", "rname", 5, 4, 3, 2, 1)) + addAction("nodata.setnegativeandsoa.advanced.tests.powerdns.com.", SetNegativeAndSOAAction(false, "another-auth.", 42, "mname", "rname", 1, 2, 3, 4, 5)) + newServer{address="127.0.0.1:%s"} + """ + + def testAdvancedNegativeAndSOANXD(self): + """ + Advanced: SetNegativeAndSOAAction NXD + """ + name = 'nxd.setnegativeandsoa.advanced.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + expectedResponse = dns.message.make_response(query) + expectedResponse.set_rcode(dns.rcode.NXDOMAIN) + soa = dns.rrset.from_text("auth", + 42, + dns.rdataclass.IN, + dns.rdatatype.SOA, + 'mname. rname. 5 4 3 2 1') + expectedResponse.additional.append(soa) + + for method in ("sendUDPQuery", "sendTCPQuery"): + sender = getattr(self, method) + (_, receivedResponse) = sender(query, response=None, useQueue=False) + self.assertEquals(receivedResponse, expectedResponse) + + def testAdvancedNegativeAndSOANoData(self): + """ + Advanced: SetNegativeAndSOAAction NoData + """ + name = 'nodata.setnegativeandsoa.advanced.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + expectedResponse = dns.message.make_response(query) + expectedResponse.set_rcode(dns.rcode.NOERROR) + soa = dns.rrset.from_text("another-auth", + 42, + dns.rdataclass.IN, + dns.rdatatype.SOA, + 'mname. rname. 1 2 3 4 5') + expectedResponse.additional.append(soa) + + for method in ("sendUDPQuery", "sendTCPQuery"): + sender = getattr(self, method) + (_, receivedResponse) = sender(query, response=None, useQueue=False) + self.assertEquals(receivedResponse, expectedResponse) -- 2.39.2