]> granicus.if.org Git - pdns/commitdiff
dnsdist: Add a negative TTL option to the packet cache
authorRemi Gacogne <remi.gacogne@powerdns.com>
Wed, 13 Jun 2018 08:35:55 +0000 (10:35 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Thu, 14 Jun 2018 15:42:06 +0000 (17:42 +0200)
Also add unit tests for the standalone functions in dnsparser.cc

13 files changed:
pdns/Makefile.am
pdns/dnsdist-cache.cc
pdns/dnsdist-cache.hh
pdns/dnsdist-lua-bindings.cc
pdns/dnsdistdist/Makefile.am
pdns/dnsdistdist/docs/reference/config.rst
pdns/dnsdistdist/test-dnsparser_cc.cc [new symlink]
pdns/dnsparser.cc
pdns/dnsparser.hh
pdns/dnswriter.cc
pdns/test-dnsdistpacketcache_cc.cc
pdns/test-dnsparser_cc.cc [new file with mode: 0644]
regression-tests.dnsdist/test_Caching.py

index ec22cc9227470b69d6ce5d622c68c56eef60aa75..dca59982a8e27ffa1b61df4f57081377efd7ad06 100644 (file)
@@ -1296,6 +1296,7 @@ testrunner_SOURCES = \
        test-distributor_hh.cc \
        test-dns_random_hh.cc \
        test-dnsname_cc.cc \
+       test-dnsparser_cc.cc \
        test-dnsparser_hh.cc \
        test-dnsrecords_cc.cc \
        test-iputils_hh.cc \
index 50703e28c8f3984063cc571196345db55ca1bdde..656e32f07408f60ba435870ddef02f63220bce73 100644 (file)
@@ -24,7 +24,7 @@
 #include "dnsparser.hh"
 #include "dnsdist-cache.hh"
 
-DNSDistPacketCache::DNSDistPacketCache(size_t maxEntries, uint32_t maxTTL, uint32_t minTTL, uint32_t tempFailureTTL, uint32_t staleTTL, bool dontAge, uint32_t shards, bool deferrableInsertLock): d_maxEntries(maxEntries), d_shardCount(shards), d_maxTTL(maxTTL), d_tempFailureTTL(tempFailureTTL), d_minTTL(minTTL), d_staleTTL(staleTTL), d_dontAge(dontAge), d_deferrableInsertLock(deferrableInsertLock)
+DNSDistPacketCache::DNSDistPacketCache(size_t maxEntries, uint32_t maxTTL, uint32_t minTTL, uint32_t tempFailureTTL, uint32_t maxNegativeTTL, uint32_t staleTTL, bool dontAge, uint32_t shards, bool deferrableInsertLock): d_maxEntries(maxEntries), d_shardCount(shards), d_maxTTL(maxTTL), d_tempFailureTTL(tempFailureTTL), d_maxNegativeTTL(maxNegativeTTL), d_minTTL(minTTL), d_staleTTL(staleTTL), d_dontAge(dontAge), d_deferrableInsertLock(deferrableInsertLock)
 {
   d_shards.resize(d_shardCount);
 
@@ -103,14 +103,18 @@ void DNSDistPacketCache::insert(uint32_t key, const DNSName& qname, uint16_t qty
     }
   }
   else {
-    minTTL = getMinTTL(response, responseLen);
+    bool seenAuthSOA = false;
+    minTTL = getMinTTL(response, responseLen, &seenAuthSOA);
 
     /* no TTL found, we don't want to cache this */
     if (minTTL == std::numeric_limits<uint32_t>::max()) {
       return;
     }
 
-    if (minTTL > d_maxTTL) {
+    if (rcode == RCode::NXDomain || (rcode == RCode::NoError && seenAuthSOA)) {
+      minTTL = std::min(minTTL, d_maxNegativeTTL);
+    }
+    else if (minTTL > d_maxTTL) {
       minTTL = d_maxTTL;
     }
 
@@ -345,9 +349,9 @@ uint64_t DNSDistPacketCache::getSize()
   return count;
 }
 
-uint32_t DNSDistPacketCache::getMinTTL(const char* packet, uint16_t length)
+uint32_t DNSDistPacketCache::getMinTTL(const char* packet, uint16_t length, bool* seenNoDataSOA)
 {
-  return getDNSPacketMinTTL(packet, length);
+  return getDNSPacketMinTTL(packet, length, seenNoDataSOA);
 }
 
 uint32_t DNSDistPacketCache::getKey(const std::string& qname, uint16_t consumed, const unsigned char* packet, uint16_t packetLen, bool tcp)
index daeabc6f61fd0e2547050b907603b565979d647d..6aae4fc0db7acba993624792222de6ffb7769b19 100644 (file)
@@ -30,7 +30,7 @@ struct DNSQuestion;
 class DNSDistPacketCache : boost::noncopyable
 {
 public:
-  DNSDistPacketCache(size_t maxEntries, uint32_t maxTTL=86400, uint32_t minTTL=0, uint32_t tempFailureTTL=60, uint32_t staleTTL=60, bool dontAge=false, uint32_t shards=1, bool deferrableInsertLock=true);
+  DNSDistPacketCache(size_t maxEntries, uint32_t maxTTL=86400, uint32_t minTTL=0, uint32_t tempFailureTTL=60, uint32_t maxNegativeTTL=3600, uint32_t staleTTL=60, bool dontAge=false, uint32_t shards=1, bool deferrableInsertLock=true);
   ~DNSDistPacketCache();
 
   void insert(uint32_t key, const DNSName& qname, uint16_t qtype, uint16_t qclass, const char* response, uint16_t responseLen, bool tcp, uint8_t rcode, boost::optional<uint32_t> tempFailureTTL);
@@ -51,7 +51,7 @@ public:
   uint64_t getTTLTooShorts() const { return d_ttlTooShorts; }
   uint64_t getEntriesCount();
 
-  static uint32_t getMinTTL(const char* packet, uint16_t length);
+  static uint32_t getMinTTL(const char* packet, uint16_t length, bool* seenNoDataSOA);
 
 private:
 
@@ -110,6 +110,7 @@ private:
   uint32_t d_shardCount;
   uint32_t d_maxTTL;
   uint32_t d_tempFailureTTL;
+  uint32_t d_maxNegativeTTL;
   uint32_t d_minTTL;
   uint32_t d_staleTTL;
   bool d_dontAge;
index f5229184b227dfd76e81b8a07789d2d7dcf891ca..372d685e255847035f34364abaff49fbaf9ca43c 100644 (file)
@@ -212,8 +212,8 @@ void setupLuaBindings(bool client)
 #endif /* HAVE_EBPF */
 
   /* PacketCache */
-  g_lua.writeFunction("newPacketCache", [](size_t maxEntries, boost::optional<uint32_t> maxTTL, boost::optional<uint32_t> minTTL, boost::optional<uint32_t> tempFailTTL, boost::optional<uint32_t> staleTTL, boost::optional<bool> dontAge, boost::optional<size_t> numberOfShards, boost::optional<bool> deferrableInsertLock) {
-      return std::make_shared<DNSDistPacketCache>(maxEntries, maxTTL ? *maxTTL : 86400, minTTL ? *minTTL : 0, tempFailTTL ? *tempFailTTL : 60, staleTTL ? *staleTTL : 60, dontAge ? *dontAge : false, numberOfShards ? *numberOfShards : 1, deferrableInsertLock ? *deferrableInsertLock : true);
+  g_lua.writeFunction("newPacketCache", [](size_t maxEntries, boost::optional<uint32_t> maxTTL, boost::optional<uint32_t> minTTL, boost::optional<uint32_t> tempFailTTL, boost::optional<uint32_t> staleTTL, boost::optional<bool> dontAge, boost::optional<size_t> numberOfShards, boost::optional<bool> deferrableInsertLock, boost::optional<uint32_t> maxNegativeTTL) {
+      return std::make_shared<DNSDistPacketCache>(maxEntries, maxTTL ? *maxTTL : 86400, minTTL ? *minTTL : 0, tempFailTTL ? *tempFailTTL : 60, maxNegativeTTL ? *maxNegativeTTL : 3600, staleTTL ? *staleTTL : 60, dontAge ? *dontAge : false, numberOfShards ? *numberOfShards : 1, deferrableInsertLock ? *deferrableInsertLock : true);
     });
   g_lua.registerFunction("toString", &DNSDistPacketCache::toString);
   g_lua.registerFunction("isFull", &DNSDistPacketCache::isFull);
index 78f7b7aba02090632faf043dbe90b4880010e86c..3081f33cf6ffe1a2b24de81a302702be295dbe9c 100644 (file)
@@ -214,10 +214,11 @@ testrunner_SOURCES = \
        base64.hh \
        dns.hh \
        test-base64_cc.cc \
+       test-dnscrypt_cc.cc \
        test-dnsdist_cc.cc \
        test-dnsdistpacketcache_cc.cc \
        test-dnsdistrings_cc.cc \
-       test-dnscrypt_cc.cc \
+       test-dnsparser_cc.cc \
        test-iputils_hh.cc \
        dnsdist.hh \
        dnsdist-cache.cc dnsdist-cache.hh \
index 5d12abd5b882589773901850aae5873750b405da..ab836f8c618a4b7daf3f4fc410367d252d63b607 100644 (file)
@@ -506,11 +506,14 @@ PacketCache
 A Pool can have a packet cache to answer queries directly in stead of going to the backend.
 See :doc:`../guides/cache` for a how to.
 
-.. function:: newPacketCache(maxEntries[, maxTTL=86400[, minTTL=0[, temporaryFailureTTL=60[, staleTTL=60[, dontAge=false[, numberOfShards=1[, deferrableInsertLock=true]]]]]]]) -> PacketCache
+.. function:: newPacketCache(maxEntries[, maxTTL=86400[, minTTL=0[, temporaryFailureTTL=60[, staleTTL=60[, dontAge=false[, numberOfShards=1[, deferrableInsertLock=true[, maxNegativeTTL=3600]]]]]]]) -> PacketCache
 
   .. versionchanged:: 1.3.0
     ``numberOfShards`` and ``deferrableInsertLock`` parameters added.
 
+  .. versionchanged:: 1.3.1
+    ``maxNegativeTTL`` parameter added.
+
   Creates a new :class:`PacketCache` with the settings specified.
 
   :param int maxEntries: The maximum number of entries in this cache
@@ -521,6 +524,7 @@ See :doc:`../guides/cache` for a how to.
   :param bool dontAge: Don't reduce TTLs when serving from the cache. Use this when :program:`dnsdist` fronts a cluster of authoritative servers
   :param int numberOfShards: Number of shards to divide the cache into, to reduce lock contention
   :param bool deferrableInsertLock: Whether the cache should give up insertion if the lock is held by another thread, or simply wait to get the lock
+  :param bool maxNegativeTTL: Cache a NXDomain or NoData answer from the backend for at most this amount of seconds, even if the TTL of the SOA record is higher
 
 .. class:: PacketCache
 
diff --git a/pdns/dnsdistdist/test-dnsparser_cc.cc b/pdns/dnsdistdist/test-dnsparser_cc.cc
new file mode 120000 (symlink)
index 0000000..1faf2cc
--- /dev/null
@@ -0,0 +1 @@
+../test-dnsparser_cc.cc
\ No newline at end of file
index 77ac1b96d1c56767d4dddfbfcd85b57cb77fe2b1..41806237b953b9f0fb3a47fb2ab0048979cbdccc 100644 (file)
@@ -780,7 +780,7 @@ void ageDNSPacket(std::string& packet, uint32_t seconds)
   ageDNSPacket((char*)packet.c_str(), packet.length(), seconds);
 }
 
-uint32_t getDNSPacketMinTTL(const char* packet, size_t length)
+uint32_t getDNSPacketMinTTL(const char* packet, size_t length, bool* seenAuthSOA)
 {
   uint32_t result = std::numeric_limits<uint32_t>::max();
   if(length < sizeof(dnsheader)) {
@@ -802,20 +802,21 @@ uint32_t getDNSPacketMinTTL(const char* packet, size_t length)
       dpm.skipLabel();
       const uint16_t dnstype = dpm.get16BitInt();
       /* class */
-      dpm.skipBytes(2);
+      const uint16_t dnsclass = dpm.get16BitInt();
 
       if(dnstype == QType::OPT) {
         break;
       }
 
       /* report it if we see a SOA record in the AUTHORITY section */
-      if(dnstype == QType::SOA && seenAuthSOA != nullptr && n >= (ntohs(dh->ancount) && n < (ntohs(dh->ancount) + ntohs(dh->nscount)))) {
+      if(dnstype == QType::SOA && dnsclass == QClass::IN && seenAuthSOA != nullptr && n >= (ntohs(dh->ancount) && n < (ntohs(dh->ancount) + ntohs(dh->nscount)))) {
         *seenAuthSOA = true;
       }
 
       const uint32_t ttl = dpm.get32BitInt();
-      if (result > ttl)
+      if (result > ttl) {
         result = ttl;
+      }
 
       dpm.skipRData();
     }
index 28ce70386bb2f9f7855e7d2eda47cb1940adab1f..353efccdd07256806e6b9d04c64114078132204c 100644 (file)
@@ -399,7 +399,7 @@ string simpleCompress(const string& label, const string& root="");
 void ageDNSPacket(char* packet, size_t length, uint32_t seconds);
 void ageDNSPacket(std::string& packet, uint32_t seconds);
 void editDNSPacketTTL(char* packet, size_t length, std::function<uint32_t(uint8_t, uint16_t, uint16_t, uint32_t)> visitor);
-uint32_t getDNSPacketMinTTL(const char* packet, size_t length);
+uint32_t getDNSPacketMinTTL(const char* packet, size_t length, bool* seenAuthSOA=nullptr);
 uint32_t getDNSPacketLength(const char* packet, size_t length);
 uint16_t getRecordsOfTypeCount(const char* packet, size_t length, uint8_t section, uint16_t type);
 
index 95e0687bb9d84ca11032206aad7a3a3337fd5eaa..02abb9f8a34d2e6b7ff22ab2036b1386a2f69930 100644 (file)
@@ -142,7 +142,7 @@ void DNSPacketWriter::xfr48BitInt(uint64_t val)
 
 void DNSPacketWriter::xfr32BitInt(uint32_t val)
 {
-  int rval=htonl(val);
+  uint32_t rval=htonl(val);
   uint8_t* ptr=reinterpret_cast<uint8_t*>(&rval);
   d_content.insert(d_content.end(), ptr, ptr+4);
 }
index a08ac278abac84e68df60517b84f83e4d156acad..0d46082bb36e1af622e3cf34345e28f737e3df06 100644 (file)
@@ -160,6 +160,108 @@ BOOST_AUTO_TEST_CASE(test_PacketCacheServFailTTL) {
   }
 }
 
+BOOST_AUTO_TEST_CASE(test_PacketCacheNoDataTTL) {
+  const size_t maxEntries = 150000;
+  DNSDistPacketCache PC(maxEntries, /* maxTTL */ 86400, /* minTTL */ 1, /* tempFailureTTL */ 60, /* maxNegativeTTL */ 1);
+
+  struct timespec queryTime;
+  gettime(&queryTime);  // does not have to be accurate ("realTime") in tests
+
+  ComboAddress remote;
+  try {
+    DNSName name("nodata");
+    vector<uint8_t> query;
+    DNSPacketWriter pwQ(query, name, QType::A, QClass::IN, 0);
+    pwQ.getHeader()->rd = 1;
+
+    vector<uint8_t> response;
+    DNSPacketWriter pwR(response, name, QType::A, QClass::IN, 0);
+    pwR.getHeader()->rd = 1;
+    pwR.getHeader()->ra = 0;
+    pwR.getHeader()->qr = 1;
+    pwR.getHeader()->rcode = RCode::NoError;
+    pwR.getHeader()->id = pwQ.getHeader()->id;
+    pwR.commit();
+    pwR.startRecord(name, QType::SOA, 86400, QClass::IN, DNSResourceRecord::AUTHORITY);
+    pwR.commit();
+    pwR.addOpt(4096, 0, 0);
+    pwR.commit();
+
+    uint16_t responseLen = response.size();
+
+    char responseBuf[4096];
+    uint16_t responseBufSize = sizeof(responseBuf);
+    uint32_t key = 0;
+    DNSQuestion dq(&name, QType::A, QClass::IN, &remote, &remote, (struct dnsheader*) query.data(), query.size(), query.size(), false, &queryTime);
+    bool found = PC.get(dq, name.wirelength(), 0, responseBuf, &responseBufSize, &key);
+    BOOST_CHECK_EQUAL(found, false);
+
+    PC.insert(key, name, QType::A, QClass::IN, reinterpret_cast<const char*>(response.data()), responseLen, false, RCode::NoError, boost::none);
+    found = PC.get(dq, name.wirelength(), pwR.getHeader()->id, responseBuf, &responseBufSize, &key, 0, true);
+    BOOST_CHECK_EQUAL(found, true);
+    sleep(2);
+    /* it should have expired by now */
+    found = PC.get(dq, name.wirelength(), pwR.getHeader()->id, responseBuf, &responseBufSize, &key, 0, true);
+    BOOST_CHECK_EQUAL(found, false);
+
+  }
+  catch(const PDNSException& e) {
+    cerr<<"Had error: "<<e.reason<<endl;
+    throw;
+  }
+}
+
+BOOST_AUTO_TEST_CASE(test_PacketCacheNXDomainTTL) {
+  const size_t maxEntries = 150000;
+  DNSDistPacketCache PC(maxEntries, /* maxTTL */ 86400, /* minTTL */ 1, /* tempFailureTTL */ 60, /* maxNegativeTTL */ 1);
+
+  struct timespec queryTime;
+  gettime(&queryTime);  // does not have to be accurate ("realTime") in tests
+
+  ComboAddress remote;
+  try {
+    DNSName name("nxdomain");
+    vector<uint8_t> query;
+    DNSPacketWriter pwQ(query, name, QType::A, QClass::IN, 0);
+    pwQ.getHeader()->rd = 1;
+
+    vector<uint8_t> response;
+    DNSPacketWriter pwR(response, name, QType::A, QClass::IN, 0);
+    pwR.getHeader()->rd = 1;
+    pwR.getHeader()->ra = 0;
+    pwR.getHeader()->qr = 1;
+    pwR.getHeader()->rcode = RCode::NXDomain;
+    pwR.getHeader()->id = pwQ.getHeader()->id;
+    pwR.commit();
+    pwR.startRecord(name, QType::SOA, 86400, QClass::IN, DNSResourceRecord::AUTHORITY);
+    pwR.commit();
+    pwR.addOpt(4096, 0, 0);
+    pwR.commit();
+
+    uint16_t responseLen = response.size();
+
+    char responseBuf[4096];
+    uint16_t responseBufSize = sizeof(responseBuf);
+    uint32_t key = 0;
+    DNSQuestion dq(&name, QType::A, QClass::IN, &remote, &remote, (struct dnsheader*) query.data(), query.size(), query.size(), false, &queryTime);
+    bool found = PC.get(dq, name.wirelength(), 0, responseBuf, &responseBufSize, &key);
+    BOOST_CHECK_EQUAL(found, false);
+
+    PC.insert(key, name, QType::A, QClass::IN, reinterpret_cast<const char*>(response.data()), responseLen, false, RCode::NXDomain, boost::none);
+    found = PC.get(dq, name.wirelength(), pwR.getHeader()->id, responseBuf, &responseBufSize, &key, 0, true);
+    BOOST_CHECK_EQUAL(found, true);
+    sleep(2);
+    /* it should have expired by now */
+    found = PC.get(dq, name.wirelength(), pwR.getHeader()->id, responseBuf, &responseBufSize, &key, 0, true);
+    BOOST_CHECK_EQUAL(found, false);
+
+  }
+  catch(const PDNSException& e) {
+    cerr<<"Had error: "<<e.reason<<endl;
+    throw;
+  }
+}
+
 static DNSDistPacketCache PC(500000);
 
 static void *threadMangler(void* off)
diff --git a/pdns/test-dnsparser_cc.cc b/pdns/test-dnsparser_cc.cc
new file mode 100644 (file)
index 0000000..4a1084d
--- /dev/null
@@ -0,0 +1,461 @@
+#define BOOST_TEST_DYN_LINK
+#define BOOST_TEST_NO_MAIN
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <boost/test/unit_test.hpp>
+
+#include "dnsparser.hh"
+
+BOOST_AUTO_TEST_SUITE(test_dnsparser_cc)
+
+BOOST_AUTO_TEST_CASE(test_editDNSPacketTTL) {
+
+  auto generatePacket = [](uint32_t ttl) {
+    DNSName name("powerdns.com.");
+    ComboAddress v4("1.2.3.4");
+    ComboAddress v6("2001:db8::1");
+
+    vector<uint8_t> packet;
+    DNSPacketWriter pwR(packet, name, QType::A, QClass::IN, 0);
+    pwR.getHeader()->qr = 1;
+
+    /* record we want to see altered */
+    pwR.startRecord(name, QType::A, ttl, QClass::IN, DNSResourceRecord::ANSWER);
+    pwR.xfrIP(v4.sin4.sin_addr.s_addr);
+    pwR.commit();
+
+    /* same record but different TTL (yeah, don't do that but it's just a test) */
+    pwR.startRecord(name, QType::A, 100, QClass::IN, DNSResourceRecord::ANSWER);
+    pwR.xfrIP(v4.sin4.sin_addr.s_addr);
+    pwR.commit();
+
+    /* different type */
+    pwR.startRecord(name, QType::AAAA, 42, QClass::IN, DNSResourceRecord::ANSWER);
+    pwR.xfrIP6(std::string(reinterpret_cast<const char*>(v6.sin6.sin6_addr.s6_addr), 16));
+    pwR.commit();
+
+    /* different class */
+    pwR.startRecord(name, QType::A, 42, QClass::CHAOS, DNSResourceRecord::ANSWER);
+    pwR.commit();
+
+    /* different section */
+    pwR.startRecord(name, QType::A, 42, QClass::IN, DNSResourceRecord::AUTHORITY);
+    pwR.xfrIP(v4.sin4.sin_addr.s_addr);
+    pwR.commit();
+
+    return packet;
+  };
+
+  auto firstPacket = generatePacket(42);
+  auto expectedAlteredPacket = generatePacket(84);
+
+  size_t called = 0;
+  editDNSPacketTTL(reinterpret_cast<char*>(firstPacket.data()), firstPacket.size(), [&called](uint8_t section, uint16_t class_, uint16_t type, uint32_t ttl) {
+
+      called++;
+
+      /* only updates the TTL of IN/A, in answer, with an existing ttl of 42 */
+      if (section == 1 && class_ == QClass::IN && type == QType::A && ttl == 42) {
+        return 84;
+      }
+      return 0;
+    });
+
+  /* check that we have been for all records */
+  BOOST_CHECK_EQUAL(called, 5);
+
+  BOOST_REQUIRE_EQUAL(firstPacket.size(), expectedAlteredPacket.size());
+  for (size_t idx = 0; idx < firstPacket.size(); idx++) {
+    BOOST_CHECK_EQUAL(firstPacket.at(idx), expectedAlteredPacket.at(idx));
+  }
+  BOOST_CHECK(firstPacket == expectedAlteredPacket);
+
+  /* now call it with a truncated packet, missing the last TTL and rdata,
+     we should only be called 4 times but everything else should be fine. */
+  called = 0;
+  editDNSPacketTTL(reinterpret_cast<char*>(firstPacket.data()), firstPacket.size() - sizeof(uint32_t) - /* rdata length */ sizeof (uint16_t) - /* IPv4 payload in rdata */ 4, [&called](uint8_t section, uint16_t class_, uint16_t type, uint32_t ttl) {
+
+      called++;
+
+      /* only updates the TTL of IN/A, in answer, with an existing ttl of 42 */
+      if (section == 1 && class_ == QClass::IN && type == QType::A && ttl == 42) {
+        return 84;
+      }
+      return 0;
+    });
+
+  /* check that we have been for all records */
+  BOOST_CHECK_EQUAL(called, 4);
+  BOOST_CHECK(firstPacket == expectedAlteredPacket);
+}
+
+BOOST_AUTO_TEST_CASE(test_ageDNSPacket) {
+
+  auto generatePacket = [](uint32_t ttl) {
+    DNSName name("powerdns.com.");
+    ComboAddress v4("1.2.3.4");
+    ComboAddress v6("2001:db8::1");
+
+    vector<uint8_t> packet;
+    DNSPacketWriter pwR(packet, name, QType::A, QClass::IN, 0);
+    pwR.getHeader()->qr = 1;
+
+    /* record we want to see altered */
+    pwR.startRecord(name, QType::A, ttl, QClass::IN, DNSResourceRecord::ANSWER);
+    pwR.xfrIP(v4.sin4.sin_addr.s_addr);
+    pwR.commit();
+
+    pwR.addOpt(4096, 0, 0);
+    pwR.commit();
+
+    return packet;
+  };
+
+  auto firstPacket = generatePacket(3600);
+  auto expectedAlteredPacket = generatePacket(1800);
+
+  ageDNSPacket(reinterpret_cast<char*>(firstPacket.data()), firstPacket.size(), 1800);
+
+  BOOST_REQUIRE_EQUAL(firstPacket.size(), expectedAlteredPacket.size());
+  for (size_t idx = 0; idx < firstPacket.size(); idx++) {
+    BOOST_CHECK_EQUAL(firstPacket.at(idx), expectedAlteredPacket.at(idx));
+  }
+  BOOST_CHECK(firstPacket == expectedAlteredPacket);
+
+  /* now call it with a truncated packet, missing the last TTL and rdata,
+     the packet should not be altered. */
+  ageDNSPacket(reinterpret_cast<char*>(firstPacket.data()), firstPacket.size() - sizeof(uint32_t) - /* rdata length */ sizeof (uint16_t) - /* IPv4 payload in rdata */ 4 - /* size of OPT record */ 11, 900);
+
+  BOOST_CHECK(firstPacket == expectedAlteredPacket);
+
+  /* now remove more than the remaining TTL, not that while TTL are,
+     per rfc1035 errata, "a 32 bit unsigned integer" so we should be
+     able to expect unsigned overflow to apply, but rfc2181 specifies
+     a maximum of "2^31 - 1". */
+  ageDNSPacket(reinterpret_cast<char*>(firstPacket.data()), firstPacket.size(), 1801);
+
+  uint32_t ttl = std::numeric_limits<uint32_t>::max();
+
+  expectedAlteredPacket = generatePacket(ttl);
+  BOOST_REQUIRE_EQUAL(firstPacket.size(), expectedAlteredPacket.size());
+  for (size_t idx = 0; idx < firstPacket.size(); idx++) {
+    BOOST_CHECK_EQUAL(firstPacket.at(idx), expectedAlteredPacket.at(idx));
+  }
+  BOOST_CHECK(firstPacket == expectedAlteredPacket);
+}
+
+BOOST_AUTO_TEST_CASE(test_getDNSPacketMinTTL) {
+
+  const DNSName name("powerdns.com.");
+  const ComboAddress v4("1.2.3.4");
+  const ComboAddress v6("2001:db8::1");
+
+  {
+    /* no records */
+    vector<uint8_t> packet;
+    DNSPacketWriter pwR(packet, name, QType::A, QClass::IN, 0);
+    pwR.getHeader()->qr = 1;
+    pwR.commit();
+
+    auto result = getDNSPacketMinTTL(reinterpret_cast<char*>(packet.data()), packet.size(), nullptr);
+    BOOST_CHECK_EQUAL(result, std::numeric_limits<uint32_t>::max());
+  }
+
+  {
+    /* only one record, not an OPT one */
+    uint32_t ttl = 42;
+    vector<uint8_t> packet;
+    DNSPacketWriter pwR(packet, name, QType::A, QClass::IN, 0);
+    pwR.getHeader()->qr = 1;
+    pwR.commit();
+
+    pwR.startRecord(name, QType::A, ttl, QClass::IN, DNSResourceRecord::ANSWER);
+    pwR.xfrIP(v4.sin4.sin_addr.s_addr);
+    pwR.commit();
+
+    auto result = getDNSPacketMinTTL(reinterpret_cast<char*>(packet.data()), packet.size(), nullptr);
+    BOOST_CHECK_EQUAL(result, ttl);
+  }
+
+  {
+    /* only one record, an OPT one */
+    vector<uint8_t> packet;
+    DNSPacketWriter pwR(packet, name, QType::A, QClass::IN, 0);
+    pwR.getHeader()->qr = 1;
+    pwR.commit();
+
+    pwR.addOpt(4096, 0, 0);
+    pwR.commit();
+
+    auto result = getDNSPacketMinTTL(reinterpret_cast<char*>(packet.data()), packet.size(), nullptr);
+    BOOST_CHECK_EQUAL(result, std::numeric_limits<uint32_t>::max());
+  }
+
+  {
+    /* records with different TTLs, should return the lower */
+    vector<uint8_t> packet;
+    DNSPacketWriter pwR(packet, name, QType::A, QClass::IN, 0);
+    pwR.getHeader()->qr = 1;
+    pwR.commit();
+
+    pwR.startRecord(name, QType::A, 257, QClass::IN, DNSResourceRecord::ANSWER);
+    pwR.xfrIP(v4.sin4.sin_addr.s_addr);
+    pwR.commit();
+
+    pwR.startRecord(name, QType::A, 255, QClass::IN, DNSResourceRecord::AUTHORITY);
+    pwR.xfrIP(v4.sin4.sin_addr.s_addr);
+    pwR.commit();
+
+    pwR.startRecord(name, QType::A, 256, QClass::IN, DNSResourceRecord::ADDITIONAL);
+    pwR.xfrIP(v4.sin4.sin_addr.s_addr);
+    pwR.commit();
+
+    auto result = getDNSPacketMinTTL(reinterpret_cast<char*>(packet.data()), packet.size(), nullptr);
+    BOOST_CHECK_EQUAL(result, 255);
+  }
+
+  {
+    /* SOA record in answer, seenAuthSOA should not be set */
+    vector<uint8_t> packet;
+    DNSPacketWriter pwR(packet, name, QType::A, QClass::IN, 0);
+    pwR.getHeader()->qr = 1;
+    pwR.commit();
+
+    pwR.startRecord(name, QType::SOA, 257, QClass::IN, DNSResourceRecord::ANSWER);
+    pwR.commit();
+
+    pwR.startRecord(name, QType::A, 255, QClass::IN, DNSResourceRecord::AUTHORITY);
+    pwR.xfrIP(v4.sin4.sin_addr.s_addr);
+    pwR.commit();
+
+    pwR.startRecord(name, QType::A, 256, QClass::IN, DNSResourceRecord::ADDITIONAL);
+    pwR.xfrIP(v4.sin4.sin_addr.s_addr);
+    pwR.commit();
+
+    bool seenAuthSOA = false;
+    auto result = getDNSPacketMinTTL(reinterpret_cast<char*>(packet.data()), packet.size(), &seenAuthSOA);
+    BOOST_CHECK_EQUAL(result, 255);
+    BOOST_CHECK_EQUAL(seenAuthSOA, false);
+  }
+
+  {
+    /* one SOA record in auth, seenAuthSOA should be set */
+    vector<uint8_t> packet;
+    DNSPacketWriter pwR(packet, name, QType::A, QClass::IN, 0);
+    pwR.getHeader()->qr = 1;
+    pwR.commit();
+
+    pwR.startRecord(name, QType::A, 255, QClass::IN, DNSResourceRecord::ANSWER);
+    pwR.xfrIP(v4.sin4.sin_addr.s_addr);
+    pwR.commit();
+
+    pwR.startRecord(name, QType::SOA, 257, QClass::IN, DNSResourceRecord::AUTHORITY);
+    pwR.commit();
+
+    pwR.startRecord(name, QType::A, 256, QClass::IN, DNSResourceRecord::ADDITIONAL);
+    pwR.xfrIP(v4.sin4.sin_addr.s_addr);
+    pwR.commit();
+
+    bool seenAuthSOA = false;
+    auto result = getDNSPacketMinTTL(reinterpret_cast<char*>(packet.data()), packet.size(), &seenAuthSOA);
+    BOOST_CHECK_EQUAL(result, 255);
+    BOOST_CHECK_EQUAL(seenAuthSOA, true);
+  }
+
+  {
+    /* one SOA record of the wrong qclass in auth, seenAuthSOA should not be set */
+    vector<uint8_t> packet;
+    DNSPacketWriter pwR(packet, name, QType::A, QClass::IN, 0);
+    pwR.getHeader()->qr = 1;
+    pwR.commit();
+
+    pwR.startRecord(name, QType::A, 257, QClass::IN, DNSResourceRecord::ANSWER);
+    pwR.xfrIP(v4.sin4.sin_addr.s_addr);
+    pwR.commit();
+
+    pwR.startRecord(name, QType::SOA, 255, QClass::CHAOS, DNSResourceRecord::AUTHORITY);
+    pwR.commit();
+
+    pwR.startRecord(name, QType::A, 256, QClass::IN, DNSResourceRecord::ADDITIONAL);
+    pwR.xfrIP(v4.sin4.sin_addr.s_addr);
+    pwR.commit();
+
+    bool seenAuthSOA = false;
+    auto result = getDNSPacketMinTTL(reinterpret_cast<char*>(packet.data()), packet.size(), &seenAuthSOA);
+    BOOST_CHECK_EQUAL(result, 255);
+    BOOST_CHECK_EQUAL(seenAuthSOA, false);
+  }
+
+  {
+    /* one A record in auth, seenAuthSOA should not be set */
+    vector<uint8_t> packet;
+    DNSPacketWriter pwR(packet, name, QType::A, QClass::IN, 0);
+    pwR.getHeader()->qr = 1;
+    pwR.commit();
+
+    pwR.startRecord(name, QType::A, 257, QClass::IN, DNSResourceRecord::AUTHORITY);
+    pwR.xfrIP(v4.sin4.sin_addr.s_addr);
+    pwR.commit();
+
+    bool seenAuthSOA = false;
+    auto result = getDNSPacketMinTTL(reinterpret_cast<char*>(packet.data()), packet.size(), &seenAuthSOA);
+    BOOST_CHECK_EQUAL(result, 257);
+    BOOST_CHECK_EQUAL(seenAuthSOA, false);
+  }
+
+  {
+    /* one SOA record in additional, seenAuthSOA should not be set */
+    vector<uint8_t> packet;
+    DNSPacketWriter pwR(packet, name, QType::A, QClass::IN, 0);
+    pwR.getHeader()->qr = 1;
+    pwR.commit();
+
+    pwR.startRecord(name, QType::SOA, 255, QClass::CHAOS, DNSResourceRecord::ADDITIONAL);
+    pwR.commit();
+
+    bool seenAuthSOA = false;
+    auto result = getDNSPacketMinTTL(reinterpret_cast<char*>(packet.data()), packet.size(), &seenAuthSOA);
+    BOOST_CHECK_EQUAL(result, 255);
+    BOOST_CHECK_EQUAL(seenAuthSOA, false);
+  }
+
+  {
+    /* truncated packet, no exception should be raised */
+    /* one SOA record in auth, seenAuthSOA should be set */
+    vector<uint8_t> packet;
+    DNSPacketWriter pwR(packet, name, QType::A, QClass::IN, 0);
+    pwR.getHeader()->qr = 1;
+    pwR.commit();
+
+    pwR.startRecord(name, QType::A, 255, QClass::IN, DNSResourceRecord::ANSWER);
+    pwR.xfrIP(v4.sin4.sin_addr.s_addr);
+    pwR.commit();
+
+    pwR.startRecord(name, QType::SOA, 257, QClass::IN, DNSResourceRecord::AUTHORITY);
+    pwR.commit();
+
+    pwR.startRecord(name, QType::A, 254, QClass::IN, DNSResourceRecord::ADDITIONAL);
+    pwR.xfrIP(v4.sin4.sin_addr.s_addr);
+    pwR.commit();
+
+    bool seenAuthSOA = false;
+    auto result = getDNSPacketMinTTL(reinterpret_cast<char*>(packet.data()), packet.size() - sizeof(uint32_t) - /* rdata length */ sizeof (uint16_t) - /* IPv4 payload in rdata */ 4, &seenAuthSOA);
+    BOOST_CHECK_EQUAL(result, 255);
+    BOOST_CHECK_EQUAL(seenAuthSOA, true);
+  }
+}
+
+BOOST_AUTO_TEST_CASE(test_getDNSPacketLength) {
+
+  const DNSName name("powerdns.com.");
+  const ComboAddress v4("1.2.3.4");
+  const ComboAddress v6("2001:db8::1");
+
+  {
+    /* no records */
+    vector<uint8_t> packet;
+    DNSPacketWriter pwR(packet, name, QType::A, QClass::IN, 0);
+    pwR.getHeader()->qr = 1;
+    pwR.commit();
+
+    auto result = getDNSPacketLength(reinterpret_cast<char*>(packet.data()), packet.size());
+    BOOST_CHECK_EQUAL(result, packet.size());
+  }
+
+  {
+    /* several records */
+    vector<uint8_t> packet;
+    DNSPacketWriter pwR(packet, name, QType::A, QClass::IN, 0);
+    pwR.getHeader()->qr = 1;
+    pwR.commit();
+
+    pwR.startRecord(name, QType::A, 255, QClass::IN, DNSResourceRecord::ANSWER);
+    pwR.xfrIP(v4.sin4.sin_addr.s_addr);
+    pwR.commit();
+
+    pwR.startRecord(name, QType::SOA, 257, QClass::IN, DNSResourceRecord::AUTHORITY);
+    pwR.commit();
+
+    pwR.startRecord(name, QType::A, 256, QClass::IN, DNSResourceRecord::ADDITIONAL);
+    pwR.xfrIP(v4.sin4.sin_addr.s_addr);
+    pwR.commit();
+
+    pwR.addOpt(4096, 0, 0);
+    pwR.commit();
+
+    auto result = getDNSPacketLength(reinterpret_cast<char*>(packet.data()), packet.size());
+    BOOST_CHECK_EQUAL(result, packet.size());
+  }
+
+  {
+    /* trailing data */
+    vector<uint8_t> packet;
+    DNSPacketWriter pwR(packet, name, QType::A, QClass::IN, 0);
+    pwR.getHeader()->qr = 1;
+    pwR.commit();
+
+    pwR.startRecord(name, QType::A, 255, QClass::IN, DNSResourceRecord::ANSWER);
+    pwR.xfrIP(v4.sin4.sin_addr.s_addr);
+    pwR.commit();
+
+    pwR.startRecord(name, QType::SOA, 257, QClass::IN, DNSResourceRecord::AUTHORITY);
+    pwR.commit();
+
+    pwR.startRecord(name, QType::A, 256, QClass::IN, DNSResourceRecord::ADDITIONAL);
+    pwR.xfrIP(v4.sin4.sin_addr.s_addr);
+    pwR.commit();
+
+    pwR.addOpt(4096, 0, 0);
+    pwR.commit();
+
+    auto realSize = packet.size();
+    packet.resize(realSize + 512);
+    auto result = getDNSPacketLength(reinterpret_cast<char*>(packet.data()), packet.size());
+    BOOST_CHECK_EQUAL(result, realSize);
+  }
+
+}
+
+BOOST_AUTO_TEST_CASE(test_getRecordsOfTypeCount) {
+  const DNSName name("powerdns.com.");
+  const ComboAddress v4("1.2.3.4");
+  const ComboAddress v6("2001:db8::1");
+
+  {
+    vector<uint8_t> packet;
+    DNSPacketWriter pwR(packet, name, QType::A, QClass::IN, 0);
+    pwR.getHeader()->qr = 1;
+    pwR.commit();
+
+    pwR.startRecord(name, QType::A, 255, QClass::IN, DNSResourceRecord::ANSWER);
+    pwR.xfrIP(v4.sin4.sin_addr.s_addr);
+    pwR.commit();
+
+    pwR.startRecord(name, QType::SOA, 257, QClass::IN, DNSResourceRecord::AUTHORITY);
+    pwR.commit();
+
+    pwR.startRecord(name, QType::A, 256, QClass::IN, DNSResourceRecord::ADDITIONAL);
+    pwR.xfrIP(v4.sin4.sin_addr.s_addr);
+    pwR.commit();
+
+    pwR.addOpt(4096, 0, 0);
+    pwR.commit();
+
+     BOOST_CHECK_EQUAL(getRecordsOfTypeCount(reinterpret_cast<char*>(packet.data()), packet.size(), 0, QType::A), 1);
+     BOOST_CHECK_EQUAL(getRecordsOfTypeCount(reinterpret_cast<char*>(packet.data()), packet.size(), 0, QType::SOA), 0);
+     BOOST_CHECK_EQUAL(getRecordsOfTypeCount(reinterpret_cast<char*>(packet.data()), packet.size(), 1, QType::A), 1);
+     BOOST_CHECK_EQUAL(getRecordsOfTypeCount(reinterpret_cast<char*>(packet.data()), packet.size(), 1, QType::SOA), 0);
+     BOOST_CHECK_EQUAL(getRecordsOfTypeCount(reinterpret_cast<char*>(packet.data()), packet.size(), 2, QType::A), 0);
+     BOOST_CHECK_EQUAL(getRecordsOfTypeCount(reinterpret_cast<char*>(packet.data()), packet.size(), 2, QType::SOA), 1);
+     BOOST_CHECK_EQUAL(getRecordsOfTypeCount(reinterpret_cast<char*>(packet.data()), packet.size(), 3, QType::A), 1);
+     BOOST_CHECK_EQUAL(getRecordsOfTypeCount(reinterpret_cast<char*>(packet.data()), packet.size(), 3, QType::SOA), 0);
+
+     BOOST_CHECK_EQUAL(getRecordsOfTypeCount(reinterpret_cast<char*>(packet.data()), packet.size(), 4, QType::SOA), 0);
+}
+
+}
+
+BOOST_AUTO_TEST_SUITE_END()
index 2d6214fbc60f5eb0281b0efd6b1f075d3a0f4463..14765bd3ee7e30d1f09192425794b9549966664c 100644 (file)
@@ -1316,6 +1316,112 @@ class TestCachingFailureTTL(DNSDistTest):
 
         self.assertEquals(total, misses)
 
+class TestCachingNegativeTTL(DNSDistTest):
+
+    _negCacheTTL = 1
+    _config_params = ['_negCacheTTL', '_testServerPort']
+    _config_template = """
+    pc = newPacketCache(1000, 86400, 0, 60, 60, false, 1, true, %d)
+    getPool(""):setCache(pc)
+    newServer{address="127.0.0.1:%s"}
+    """
+
+    def testCacheNegativeTTLNXDomain(self):
+        """
+        Cache: Negative TTL on NXDOMAIN
+
+        """
+        misses = 0
+        name = 'nxdomain.negativettl.cache.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN')
+        response = dns.message.make_response(query)
+        response.set_rcode(dns.rcode.NXDOMAIN)
+        soa = dns.rrset.from_text(name,
+                                  60,
+                                  dns.rdataclass.IN,
+                                  dns.rdatatype.SOA,
+                                  'ns.' + name + ' hostmaster.' + name + ' 1 3600 3600 3600 60')
+        response.authority.append(soa)
+
+        # Miss
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+        self.assertTrue(receivedQuery)
+        self.assertTrue(receivedResponse)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(response, receivedResponse)
+        misses += 1
+
+        # next queries should hit the cache
+        (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False)
+        self.assertEquals(receivedResponse, response)
+
+        time.sleep(self._negCacheTTL + 1)
+
+        # we should not have cached for longer than the negativel TTL
+        # so it should be a miss
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+        self.assertTrue(receivedQuery)
+        self.assertTrue(receivedResponse)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(response, receivedResponse)
+        misses += 1
+
+        total = 0
+        for key in self._responsesCounter:
+            total += self._responsesCounter[key]
+
+        self.assertEquals(total, misses)
+
+    def testCacheNegativeTTLNoData(self):
+        """
+        Cache: Negative TTL on NoData
+
+        """
+        misses = 0
+        name = 'nodata.negativettl.cache.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN')
+        response = dns.message.make_response(query)
+        response.set_rcode(dns.rcode.NOERROR)
+        soa = dns.rrset.from_text(name,
+                                  60,
+                                  dns.rdataclass.IN,
+                                  dns.rdatatype.SOA,
+                                  'ns.' + name + ' hostmaster.' + name + ' 1 3600 3600 3600 60')
+        response.authority.append(soa)
+
+        # Miss
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+        self.assertTrue(receivedQuery)
+        self.assertTrue(receivedResponse)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(response, receivedResponse)
+        misses += 1
+
+        # next queries should hit the cache
+        (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False)
+        self.assertEquals(receivedResponse, response)
+
+        time.sleep(self._negCacheTTL + 1)
+
+        # we should not have cached for longer than the negativel TTL
+        # so it should be a miss
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+        self.assertTrue(receivedQuery)
+        self.assertTrue(receivedResponse)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(response, receivedResponse)
+        misses += 1
+
+        total = 0
+        for key in self._responsesCounter:
+            total += self._responsesCounter[key]
+
+        self.assertEquals(total, misses)
+
 class TestCachingDontAge(DNSDistTest):
 
     _config_template = """