]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Add SetNegativeAndSOAAction() and its Lua binding
authorRemi Gacogne <remi.gacogne@powerdns.com>
Tue, 6 Aug 2019 15:44:56 +0000 (17:44 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 7 Feb 2020 10:11:51 +0000 (11:11 +0100)
pdns/dnsdist-console.cc
pdns/dnsdist-ecs.cc
pdns/dnsdist-ecs.hh
pdns/dnsdist-lua-actions.cc
pdns/dnsdist-lua-bindings-dnsquestion.cc
pdns/dnsdistdist/docs/reference/dq.rst
pdns/dnsdistdist/docs/rules-actions.rst
pdns/dnsname.hh
pdns/test-dnsdist_cc.cc
regression-tests.dnsdist/test_Advanced.py

index 8baa664653c68e0f37c1f72ad07bdb43b8f7f971..d3a8b06e240ea8e872a977688f0b3bc302476784 100644 (file)
@@ -515,6 +515,7 @@ const std::vector<ConsoleKeyword> 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" },
   { "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'" },
   { "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'" },
index b1b43c111aa0f489c51d69909c3dfb263ae0ce84..7cda963d4a1e27d56480f9512e0a1e70d8bd4eec 100644 (file)
@@ -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)
 {
 
 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);
 
   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<char*>(dh) + len;
   memcpy(optPtr, optRecord.data(), optRecord.size());
   len += optRecord.size();
   char * optPtr = reinterpret_cast<char*>(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<const char*>(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<const char*>(&qtype), sizeof(qtype));
+  soa.append(reinterpret_cast<const char*>(&qclass), sizeof(qclass));
+  soa.append(reinterpret_cast<const char*>(&ttl), sizeof(ttl));
+  soa.append(reinterpret_cast<const char*>(&rdLength), sizeof(rdLength));
+  soa.append(mname.toDNSString());
+  soa.append(rname.toDNSString());
+  soa.append(reinterpret_cast<const char*>(&serial), sizeof(serial));
+  soa.append(reinterpret_cast<const char*>(&refresh), sizeof(refresh));
+  soa.append(reinterpret_cast<const char*>(&retry), sizeof(retry));
+  soa.append(reinterpret_cast<const char*>(&expire), sizeof(expire));
+  soa.append(reinterpret_cast<const char*>(&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<char*>(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;
 }
 
   return true;
 }
index b339269064aeea4cd25bb072911b3e377cc34abb..293bc0cd926e01714b79e2f19b7564a7b68897b7 100644 (file)
@@ -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 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);
 
 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);
index 689d9ace4f0f575d5424e0ff187a9bf0cdbe4469..425827bbb1793c1a92a50efffc0f8f2608613d72 100644 (file)
@@ -1258,6 +1258,40 @@ private:
   std::string d_tag;
 };
 
   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<typename T, typename ActionT>
 static void addAction(GlobalStateHolder<vector<T> > *someRulActions, const luadnsrule_t& var, const std::shared_ptr<ActionT>& action, boost::optional<luaruleparams_t>& params) {
   setLuaSideEffect();
 template<typename T, typename ActionT>
 static void addAction(GlobalStateHolder<vector<T> > *someRulActions, const luadnsrule_t& var, const std::shared_ptr<ActionT>& action, boost::optional<luaruleparams_t>& params) {
   setLuaSideEffect();
@@ -1642,4 +1676,8 @@ void setupLuaActions()
   g_lua.writeFunction("KeyValueStoreLookupAction", [](std::shared_ptr<KeyValueStore>& kvs, std::shared_ptr<KeyValueLookupKey>& lookupKey, const std::string& destinationTag) {
       return std::shared_ptr<DNSAction>(new KeyValueStoreLookupAction(kvs, lookupKey, destinationTag));
     });
   g_lua.writeFunction("KeyValueStoreLookupAction", [](std::shared_ptr<KeyValueStore>& kvs, std::shared_ptr<KeyValueLookupKey>& lookupKey, const std::string& destinationTag) {
       return std::shared_ptr<DNSAction>(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<DNSAction>(new SetNegativeAndSOAAction(nxd, DNSName(zone), ttl, DNSName(mname), DNSName(rname), serial, refresh, retry, expire, minimum));
+    });
 }
 }
index 1b4709cbfccd4e3820be2cc99babd32419421219..eff46f3ab5aa7c835ab483a0492dbabea2305262 100644 (file)
@@ -259,4 +259,8 @@ void setupLuaBindingsDNSQuestion()
       dq.du->setHTTPResponse(statusCode, body, contentType ? *contentType : "");
     });
 #endif /* HAVE_DNS_OVER_HTTPS */
       dq.du->setHTTPResponse(statusCode, body, contentType ? *contentType : "");
     });
 #endif /* HAVE_DNS_OVER_HTTPS */
+
+  g_lua.registerFunction<bool(DNSQuestion::*)(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)>("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);
+    });
 }
 }
index 53e7eb053c5041659088961b029a4069df544f32..da2672f7534ca04c8a94ba5054a1b96f54e33830 100644 (file)
@@ -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''.
 
     :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
   .. method:: DNSQuestion:setTag(key, value)
 
     .. versionadded:: 1.2.0
index d1e18c5703ab64f609d1c029b890410d577f078a..4197e89efeedd787b735149c3fa2fcdfc67b3127 100644 (file)
@@ -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
 
   :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.
 .. function:: SkipCacheAction()
 
   Don't lookup the cache for this query, don't store the answer.
index da6c4de46471d27f02af5e0a2dbd6506a1d981b6..5028273070ba99ad9fa2dc3b1240beb6e57eef53 100644 (file)
@@ -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()  {}          //!< 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
   
   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
index 3a5bd23c80cb716c23206e6cf422069fbde51a65..bc06bebf8299ca6b72508529109b868f1c1ae94d 100644 (file)
@@ -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<uint8_t> 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<dnsheader*>(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<size_t>(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<uint16_t>(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<uint16_t>(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<dnsheader*>(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<size_t>(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<uint16_t>(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<uint16_t>(QType::OPT));
+    BOOST_CHECK_EQUAL(mdp.d_answers.at(1).first.d_name, g_rootdnsname);
+  }
+}
+
 BOOST_AUTO_TEST_SUITE_END();
 BOOST_AUTO_TEST_SUITE_END();
index 6be7d4f3c0c55488df0740ab62c51ff94a5abc6e..e4d4c636dd73f21c44a17c5b7eada7c028ea8973 100644 (file)
@@ -1842,3 +1842,51 @@ class TestAdvancedContinueAction(DNSDistTest):
             print(receivedResponse)
             print(expectedResponse)
             self.assertEquals(receivedResponse, expectedResponse)
             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)