]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Skip EDNS Cookies in the packet cache
authorRemi Gacogne <remi.gacogne@powerdns.com>
Thu, 2 Apr 2020 12:33:01 +0000 (14:33 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Tue, 1 Sep 2020 08:50:05 +0000 (10:50 +0200)
26 files changed:
pdns/Makefile.am
pdns/auth-packetcache.cc
pdns/dnsdist-cache.cc
pdns/dnsdist-cache.hh
pdns/dnsdistdist/Makefile.am
pdns/dnsdistdist/dnsdist-lua-bindings-packetcache.cc
pdns/dnsdistdist/docs/reference/config.rst
pdns/dnsdistdist/docs/upgrade_guide.rst
pdns/dnsdistdist/packetcache.hh [new symlink]
pdns/ednsoptions.cc
pdns/ednsoptions.hh
pdns/fuzz_dnsdistcache.cc
pdns/fuzz_packetcache.cc
pdns/packetcache.hh
pdns/pdns_recursor.cc
pdns/recpacketcache.cc
pdns/recpacketcache.hh
pdns/recursordist/Makefile.am
pdns/recursordist/views.hh [new symlink]
pdns/test-dnsdistpacketcache_cc.cc
pdns/test-packetcache_cc.cc
pdns/test-packetcache_hh.cc
pdns/test-recpacketcache_cc.cc
pdns/views.hh [new file with mode: 0644]
regression-tests.dnsdist/test_Caching.py
regression-tests.recursor-dnssec/test_PacketCache.py [new file with mode: 0644]

index 71a3fe86c98b3c6a6bbf16cc9190ad4342d65747..c3b33f5aa60bbe86c88fee887956a6bfaad48908 100644 (file)
@@ -230,6 +230,7 @@ pdns_server_SOURCES = \
        unix_utility.cc \
        utility.hh \
        version.cc version.hh \
+       views.hh \
        webserver.cc webserver.hh \
        ws-api.cc ws-api.hh \
        ws-auth.cc ws-auth.hh \
index 1cb554f44ed4e4513594ecd1527608ba0e9ecc6b..4ed8112fbda5ed178a0b68276224776e697a16e3 100644 (file)
@@ -72,7 +72,7 @@ bool AuthPacketCache::get(DNSPacket& p, DNSPacket& cached)
 
   cleanupIfNeeded();
 
-  uint32_t hash = canHashPacket(p.getString());
+  uint32_t hash = canHashPacket(p.getString(), /* don't skip ECS */ false);
   p.setHash(hash);
 
   string value;
@@ -108,7 +108,8 @@ bool AuthPacketCache::get(DNSPacket& p, DNSPacket& cached)
 
 bool AuthPacketCache::entryMatches(cmap_t::index<HashTag>::type::iterator& iter, const std::string& query, const DNSName& qname, uint16_t qtype, bool tcp)
 {
-  return iter->tcp == tcp && iter->qtype == qtype && iter->qname == qname && queryMatches(iter->query, query, qname);
+  static const std::unordered_set<uint16_t> skippedEDNSTypes{ EDNSOptionCode::COOKIE };
+  return iter->tcp == tcp && iter->qtype == qtype && iter->qname == qname && queryMatches(iter->query, query, qname, skippedEDNSTypes);
 }
 
 void AuthPacketCache::insert(DNSPacket& q, DNSPacket& r, unsigned int maxTTL)
index 9990f5abab20178b608cab6f93c3368fc25da5e3..fa38fbb9dd5efbe4bb7e170e97aaa1920d3d7223 100644 (file)
@@ -28,6 +28,7 @@
 #include "dnsdist-ecs.hh"
 #include "ednsoptions.hh"
 #include "ednssubnet.hh"
+#include "packetcache.hh"
 
 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, bool parseECS): 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_parseECS(parseECS)
 {
@@ -206,8 +207,9 @@ bool DNSDistPacketCache::get(const DNSQuestion& dq, uint16_t consumed, uint16_t
   const auto& dnsQName = dq.qname->getStorage();
   uint32_t key = getKey(dnsQName, consumed, reinterpret_cast<const unsigned char*>(dq.dh), dq.len, dq.tcp);
 
-  if (keyOut)
+  if (keyOut) {
     *keyOut = key;
+  }
 
   if (d_parseECS) {
     getClientSubnet(reinterpret_cast<const char*>(dq.dh), consumed, dq.len, subnet);
@@ -415,15 +417,23 @@ uint32_t DNSDistPacketCache::getKey(const DNSName::string_t& qname, uint16_t con
 {
   uint32_t result = 0;
   /* skip the query ID */
-  if (packetLen < sizeof(dnsheader))
-    throw std::range_error("Computing packet cache key for an invalid packet size");
+  if (packetLen < sizeof(dnsheader)) {
+    throw std::range_error("Computing packet cache key for an invalid packet size (" + std::to_string(packetLen) +")");
+  }
+
   result = burtle(packet + 2, sizeof(dnsheader) - 2, result);
   result = burtleCI((const unsigned char*) qname.c_str(), qname.length(), result);
   if (packetLen < sizeof(dnsheader) + consumed) {
-    throw std::range_error("Computing packet cache key for an invalid packet");
+    throw std::range_error("Computing packet cache key for an invalid packet (" + std::to_string(packetLen) + " < " + std::to_string(sizeof(dnsheader) + consumed) + ")");
   }
   if (packetLen > ((sizeof(dnsheader) + consumed))) {
-    result = burtle(packet + sizeof(dnsheader) + consumed, packetLen - (sizeof(dnsheader) + consumed), result);
+    if (!d_cookieHashing) {
+      /* skip EDNS Cookie options if any */
+      result = PacketCache::hashAfterQname(string_view(reinterpret_cast<const char*>(packet), packetLen), result, sizeof(dnsheader) + consumed, false);
+    }
+    else {
+      result = burtle(packet + sizeof(dnsheader) + consumed, packetLen - (sizeof(dnsheader) + consumed), result);
+    }
   }
   result = burtle((const unsigned char*) &tcp, sizeof(tcp), result);
   return result;
index 79200c4b3948bc59ee9497301fbe9a77329c42e7..89ccbfec91de6629a8830100bc84a9046af6b2f2 100644 (file)
@@ -55,6 +55,7 @@ public:
   uint64_t dump(int fd);
 
   bool isECSParsingEnabled() const { return d_parseECS; }
+  bool isCookieHashingEnabled() const { return d_cookieHashing; }
 
   bool keepStaleData() const
   {
@@ -65,8 +66,19 @@ public:
     d_keepStaleData = keep;
   }
 
+  void setCookieHashing(bool hashing)
+  {
+    d_cookieHashing = hashing;
+  }
+
+  void setECSParsingEnabled(bool enabled)
+  {
+    d_parseECS = enabled;
+  }
+
+  uint32_t getKey(const DNSName::string_t& qname, uint16_t consumed, const unsigned char* packet, uint16_t packetLen, bool tcp);
+
   static uint32_t getMinTTL(const char* packet, uint16_t length, bool* seenNoDataSOA);
-  static uint32_t getKey(const DNSName::string_t& qname, uint16_t consumed, const unsigned char* packet, uint16_t packetLen, bool tcp);
   static bool getClientSubnet(const char* packet, unsigned int consumed, uint16_t len, boost::optional<Netmask>& subnet);
 
 private:
@@ -133,4 +145,5 @@ private:
   bool d_deferrableInsertLock;
   bool d_parseECS;
   bool d_keepStaleData{false};
+  bool d_cookieHashing{false};
 };
index b0ca67ad4245007a82776c7e56936db21831415e..c21e3628ef3a0a992215e4a61e5fd543afd61cdd 100644 (file)
@@ -179,6 +179,7 @@ dnsdist_SOURCES = \
        misc.cc misc.hh \
        mplexer.hh \
        namespaces.hh \
+       packetcache.hh \
        pdnsexception.hh \
        protobuf.cc protobuf.hh \
        proxy-protocol.cc proxy-protocol.hh \
index b6d687a759611ef6e5eb82c4f0a59e3d5e44407d..4dc93e25e4a5b34d9a0054f0dceafba5a591ef68 100644 (file)
@@ -42,6 +42,7 @@ void setupLuaBindingsPacketCache()
       bool dontAge = false;
       bool deferrableInsertLock = true;
       bool ecsParsing = false;
+      bool cookieHashing = false;
 
       if (vars) {
 
@@ -84,11 +85,16 @@ void setupLuaBindingsPacketCache()
         if (vars->count("temporaryFailureTTL")) {
           tempFailTTL = boost::get<size_t>((*vars)["temporaryFailureTTL"]);
         }
+
+        if (vars->count("cookieHashing")) {
+          cookieHashing = boost::get<bool>((*vars)["cookieHashing"]);
+        }
       }
 
       auto res = std::make_shared<DNSDistPacketCache>(maxEntries, maxTTL, minTTL, tempFailTTL, maxNegativeTTL, staleTTL, dontAge, numberOfShards, deferrableInsertLock, ecsParsing);
 
       res->setKeepStaleData(keepStaleData);
+      res->setCookieHashing(cookieHashing);
 
       return res;
     });
index 7c3667e7902dffca76216c1698411036784a2532..73114563dacdc1bac6c131fbba811f48006be3fa 100644 (file)
@@ -706,6 +706,9 @@ See :doc:`../guides/cache` for a how to.
 
   .. versionadded:: 1.4.0
 
+  .. versionchanged:: 1.6.0
+    ``cookieHashing`` parameter added.
+
   Creates a new :class:`PacketCache` with the settings specified.
 
   :param int maxEntries: The maximum number of entries in this cache
@@ -722,6 +725,7 @@ See :doc:`../guides/cache` for a how to.
   * ``parseECS=false``: bool - Whether any EDNS Client Subnet option present in the query should be extracted and stored to be able to detect hash collisions involving queries with the same qname, qtype and qclass but a different incoming ECS value. Enabling this option adds a parsing cost and only makes sense if at least one backend might send different responses based on the ECS value, so it's disabled by default. Enabling this option is required for the 'zero scope' option to work
   * ``staleTTL=60``: int - When the backend servers are not reachable, and global configuration ``setStaleCacheEntriesTTL`` is set appropriately, TTL that will be used when a stale cache entry is returned.
   * ``temporaryFailureTTL=60``: int - On a SERVFAIL or REFUSED from the backend, cache for this amount of seconds..
+  * ``cookieHashing=false``: bool - Whether EDNS Cookie values will be hashed, resulting in separate entries for different cookies in the packet cache. This is required if the backend is sending answers with EDNS Cookies, otherwise a client might receive an answer with the wrong cookie.
 
 .. class:: PacketCache
 
index 0acfcddbffcc94fab1feadd4ac88d04e46949e66..7621691bb461baf17694b86d4f73459f0b5666c4 100644 (file)
@@ -1,7 +1,12 @@
 Upgrade Guide
 =============
 
-1.4.0 to 1.5.x
+1.5.x to 1.6.0
+--------------
+
+The packet cache no longer hashes EDNS Cookies by default, which means that two queries that are identical except for the content of their cookie will now be served the same answer. This only works if the backend is not returning any answer containing EDNS Cookies, otherwise the wrong cookie might be returned to a client. To prevent this, the ``cookieHashing=true`` parameter might be passed to :func:`newPacketCache` so that cookies are hashed, resulting in separate entries in the packet cache.
+
+1.4.x to 1.5.0
 --------------
 
 DOH endpoints specified in the fourth parameter of :func:`addDOHLocal` are now specified as exact paths instead of path prefixes. The default endpoint also switched from ``/`` to ``/dns-query``.
diff --git a/pdns/dnsdistdist/packetcache.hh b/pdns/dnsdistdist/packetcache.hh
new file mode 120000 (symlink)
index 0000000..b50f901
--- /dev/null
@@ -0,0 +1 @@
+../packetcache.hh
\ No newline at end of file
index d20755be732723363bcbd1dfe4f61cabe08f03ef..b10e150aa0c7e313e9f8cb10f80906832ef99e98 100644 (file)
 #include "ednsoptions.hh"
 #include "iputils.hh"
 
+bool getNextEDNSOption(const char* data, size_t dataLen, uint16_t& optionCode, uint16_t& optionLen)
+{
+  if (data == nullptr || dataLen < (sizeof(uint16_t) + sizeof(uint16_t))) {
+    return false;
+  }
+
+  size_t pos = 0;
+
+  optionCode = (static_cast<unsigned char>(data[pos]) * 256) + static_cast<unsigned char>(data[pos + 1]);
+  pos += EDNS_OPTION_CODE_SIZE;
+
+  optionLen = (static_cast<unsigned char>(data[pos]) * 256) + static_cast<unsigned char>(data[pos + 1]);
+  pos += EDNS_OPTION_LENGTH_SIZE;
+
+  return true;
+}
+
 /* extract a specific EDNS0 option from a pointer on the beginning rdLen of the OPT RR */
 int getEDNSOption(char* optRR, const size_t len, uint16_t wantedOption, char ** optionValue, size_t * optionValueSize)
 {
@@ -42,14 +59,20 @@ int getEDNSOption(char* optRR, const size_t len, uint16_t wantedOption, char **
 
   while(len >= (pos + EDNS_OPTION_CODE_SIZE + EDNS_OPTION_LENGTH_SIZE) &&
         rdLen >= (rdPos + EDNS_OPTION_CODE_SIZE + EDNS_OPTION_LENGTH_SIZE)) {
-    const uint16_t optionCode = (((unsigned char) optRR[pos]) * 256) + ((unsigned char) optRR[pos+1]);
+    uint16_t optionCode;
+    uint16_t optionLen;
+    if (!getNextEDNSOption(optRR + pos, len-pos, optionCode, optionLen)) {
+      break;
+    }
+
     pos += EDNS_OPTION_CODE_SIZE;
     rdPos += EDNS_OPTION_CODE_SIZE;
-    const uint16_t optionLen = (((unsigned char) optRR[pos]) * 256) + ((unsigned char) optRR[pos+1]);
     pos += EDNS_OPTION_LENGTH_SIZE;
     rdPos += EDNS_OPTION_LENGTH_SIZE;
-    if (optionLen > (rdLen - rdPos) || optionLen > (len - pos))
+
+    if (optionLen > (rdLen - rdPos) || optionLen > (len - pos)) {
       return EINVAL;
+    }
 
     if (optionCode == wantedOption) {
       *optionValue = optRR + pos - (EDNS_OPTION_CODE_SIZE + EDNS_OPTION_LENGTH_SIZE);
index cbd7b0d0a0209b92014a2c63bdef9d7215f19152..a8f0a87903dd4fe5a0be279135d5c87f68488c11 100644 (file)
@@ -47,5 +47,7 @@ typedef std::map<uint16_t, EDNSOptionView> EDNSOptionViewMap;
 int getEDNSOptions(const char* optRR, size_t len, EDNSOptionViewMap& options);
 /* extract all EDNS0 options from the content (so after rdLen) of the OPT RR */
 bool getEDNSOptionsFromContent(const std::string& content, std::vector<std::pair<uint16_t, std::string>>& options);
+/* parse the next EDNS option and the return the code and length. data should point to the beginning of the option code, dataLen should be maximum length of the data (minimum of remaining size in packet and remaining size in rdata) */
+bool getNextEDNSOption(const char* data, size_t dataLen, uint16_t& optionCode, uint16_t& optionLen);
 
 void generateEDNSOption(uint16_t optionCode, const std::string& payload, std::string& res);
index b34a0c3e6e2ff5c604181961c03a4c29a85680e1..79812ae4c73bba7bd66b2abdf99ee5ceb1fb14b9 100644 (file)
@@ -31,12 +31,21 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
   }
 
   /* dnsdist's version */
+  DNSDistPacketCache pcSkipCookies(10000);
+  pcSkipCookies.setECSParsingEnabled(true);
+  pcSkipCookies.setCookieHashing(false);
+
+  DNSDistPacketCache pcHashCookies(10000);
+  pcHashCookies.setECSParsingEnabled(true);
+  pcHashCookies.setCookieHashing(true);
+
   try {
     uint16_t qtype;
     uint16_t qclass;
     unsigned int consumed;
     const DNSName qname(reinterpret_cast<const char*>(data), size, sizeof(dnsheader), false, &qtype, &qclass, &consumed);
-    DNSDistPacketCache::getKey(qname.getStorage(), consumed, data, size, false);
+    pcSkipCookies.getKey(qname.getStorage(), consumed, data, size, false);
+    pcHashCookies.getKey(qname.getStorage(), consumed, data, size, false);
     boost::optional<Netmask> subnet;
     DNSDistPacketCache::getClientSubnet(reinterpret_cast<const char*>(data), consumed, size, subnet);
   }
index 1658b18f55be6d624290cce3c72adf434f8ed012..c53306c744ab795cfc37e3e6d459e585c706ad11 100644 (file)
@@ -37,7 +37,11 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
 
   /* auth's version */
   try {
-    PacketCache::canHashPacket(input);
+    static const std::unordered_set<uint16_t> optionsToIgnore{ EDNSOptionCode::COOKIE };
+
+    PacketCache::canHashPacket(input, false);
+    DNSName qname(input.data(), input.size(), sizeof(dnsheader), false);
+    PacketCache::queryMatches(input, input, qname, optionsToIgnore);
   }
   catch(const std::exception& e) {
   }
@@ -46,9 +50,11 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
 
   /* recursor's version */
   try {
-    uint16_t ecsBegin = 0;
-    uint16_t ecsEnd = 0;
-    PacketCache::canHashPacket(input, &ecsBegin, &ecsEnd);
+    static const std::unordered_set<uint16_t> optionsToIgnore{ EDNSOptionCode::COOKIE, EDNSOptionCode::ECS };
+
+    PacketCache::canHashPacket(input, true);
+    DNSName qname(input.data(), input.size(), sizeof(dnsheader), false);
+    PacketCache::queryMatches(input, input, qname, optionsToIgnore);
   }
   catch(const std::exception& e) {
   }
index 7dd0c8828e5f03a3b34414a25ae966e391c74dd5..9baa8b3dd6345526fb91b3e82a4335bd872178b1 100644 (file)
 #include "ednsoptions.hh"
 #include "misc.hh"
 #include "iputils.hh"
+#include "views.hh"
 
 class PacketCache : public boost::noncopyable
 {
 public:
-  static uint32_t canHashPacket(const std::string& packet, uint16_t* ecsBegin, uint16_t* ecsEnd)
+
+  /* hash the packet from the provided position, which should point right after tje qname. This skips:
+     - the query ID ;
+     - EDNS Cookie options, if any ;
+     - EDNS Client Subnet options, if any and skipECS is true.
+  */
+  static uint32_t hashAfterQname(const string_view& packet, uint32_t currentHash, size_t pos, bool skipECS)
   {
-    uint32_t ret = 0;
-    ret = burtle(reinterpret_cast<const unsigned char*>(packet.c_str()) + 2, sizeof(dnsheader) - 2, ret); // rest of dnsheader, skip id
-    size_t packetSize = packet.size();
-    size_t pos = sizeof(dnsheader);
-    const char* end = packet.c_str() + packetSize;
-    const char* p = packet.c_str() + pos;
-
-    for(; p < end && *p; ++p, ++pos) { // XXX if you embed a 0 in your qname we'll stop lowercasing there
-      const unsigned char l = dns_tolower(*p); // label lengths can safely be lower cased
-      ret=burtle(&l, 1, ret);
-    }                           // XXX the embedded 0 in the qname will break the subnet stripping
-
-    const struct dnsheader* dh = reinterpret_cast<const struct dnsheader*>(packet.c_str());
-    const char* skipBegin = p;
-    const char* skipEnd = p;
-    if (ecsBegin != nullptr && ecsEnd != nullptr) {
-      *ecsBegin = 0;
-      *ecsEnd = 0;
-    }
-    /* we need at least 1 (final empty label) + 2 (QTYPE) + 2 (QCLASS)
+    const size_t packetSize = packet.size();
+    assert(packetSize >= sizeof(dnsheader));
+
+    /* we need at least 2 (QTYPE) + 2 (QCLASS)
+
        + OPT root label (1), type (2), class (2) and ttl (4)
        + the OPT RR rdlen (2)
-       = 16
+       = 15
     */
-    if(ntohs(dh->arcount)==1 && (pos+16) < packetSize) {
-      char* optionBegin = nullptr;
-      size_t optionLen = 0;
-      /* skip the final empty label (1), the qtype (2), qclass (2) */
-      /* root label (1), type (2), class (2) and ttl (4) */
-      int res = getEDNSOption(const_cast<char*>(reinterpret_cast<const char*>(p)) + 14, end - (p + 14), EDNSOptionCode::ECS, &optionBegin, &optionLen);
-      if (res == 0) {
-        skipBegin = optionBegin;
-        skipEnd = optionBegin + optionLen;
-        if (ecsBegin != nullptr && ecsEnd != nullptr) {
-          *ecsBegin = optionBegin - packet.c_str();
-          *ecsEnd = *ecsBegin + optionLen;
-        }
+    const struct dnsheader* dh = reinterpret_cast<const struct dnsheader*>(packet.data());
+    if (ntohs(dh->qdcount) != 1 || ntohs(dh->ancount) != 0 || ntohs(dh->nscount) != 0 || ntohs(dh->arcount) != 1 || (pos + 15) >= packetSize) {
+      if (packetSize > pos) {
+        currentHash = burtle(reinterpret_cast<const unsigned char*>(&packet.at(pos)), packetSize - pos, currentHash);
       }
+      return currentHash;
     }
-    if (skipBegin > p) {
-      ret = burtle(reinterpret_cast<const unsigned char*>(p), skipBegin-p, ret);
+
+    currentHash = burtle(reinterpret_cast<const unsigned char*>(&packet.at(pos)), 15, currentHash);
+    /* skip the qtype (2), qclass (2) */
+    /* root label (1), type (2), class (2) and ttl (4) */
+    /* already hashed above */
+    pos += 13;
+
+    const uint16_t rdLen = ((static_cast<unsigned char>(packet.at(pos)) * 256) + static_cast<unsigned char>(packet.at(pos + 1)));
+    /* skip the rd length */
+    /* already hashed above */
+    pos += 2;
+
+    if (rdLen > (packetSize - pos)) {
+      if (pos < packetSize) {
+        currentHash = burtle(reinterpret_cast<const unsigned char*>(&packet.at(pos)), packetSize - pos, currentHash);
+      }
+      return currentHash;
     }
-    if (skipEnd < end) {
-      ret = burtle(reinterpret_cast<const unsigned char*>(skipEnd), end-skipEnd, ret);
+
+    uint16_t rdataRead = 0;
+    uint16_t optionCode;
+    uint16_t optionLen;
+
+    while (pos < packetSize && rdataRead < rdLen && getNextEDNSOption(&packet.at(pos), rdLen - rdataRead, optionCode, optionLen)) {
+      if (optionLen > (rdLen - rdataRead)) {
+        if (packetSize > pos) {
+          currentHash = burtle(reinterpret_cast<const unsigned char*>(&packet.at(pos)), packetSize - pos, currentHash);
+        }
+        return currentHash;
+      }
+
+      bool skip = false;
+      if (optionCode == EDNSOptionCode::COOKIE) {
+        skip = true;
+      }
+      else if (optionCode == EDNSOptionCode::ECS) {
+        if (skipECS) {
+          skip = true;
+        }
+      }
+
+      if (!skip) {
+        /* hash the option code, length and content */
+        currentHash = burtle(reinterpret_cast<const unsigned char*>(&packet.at(pos)), 4 + optionLen, currentHash);
+      }
+      else {
+        /* hash the option code and length */
+        currentHash = burtle(reinterpret_cast<const unsigned char*>(&packet.at(pos)), 4, currentHash);
+      }
+
+      pos += 4 + optionLen;
+      rdataRead += 4 + optionLen;
+    }
+
+    if (pos < packetSize) {
+      currentHash = burtle(reinterpret_cast<const unsigned char*>(&packet.at(pos)), packetSize - pos, currentHash);
     }
 
-    return ret;
+    return currentHash;
   }
 
-  static uint32_t canHashPacket(const std::string& packet)
+  static uint32_t hashHeaderAndQName(const std::string& packet, size_t& pos)
   {
-    uint32_t ret = 0;
-    ret = burtle(reinterpret_cast<const unsigned char*>(packet.c_str()) + 2, sizeof(dnsheader) - 2, ret); // rest of dnsheader, skip id
+    uint32_t currentHash = 0;
     size_t packetSize = packet.size();
-    size_t pos = sizeof(dnsheader);
-    const char* end = packet.c_str() + packetSize;
-    const char* p = packet.c_str() + pos;
+    assert(packetSize >= sizeof(dnsheader));
+    currentHash = burtle(reinterpret_cast<const unsigned char*>(&packet.at(2)), sizeof(dnsheader) - 2, currentHash); // rest of dnsheader, skip id
+    pos = sizeof(dnsheader);
+
+    for (; pos < packetSize; ) {
+      const unsigned char labelLen = static_cast<unsigned char>(packet.at(pos));
+      currentHash = burtle(&labelLen, 1, currentHash);
+      ++pos;
+      if (labelLen == 0) {
+        break;
+      }
+
+      for (size_t idx = 0; idx < labelLen && pos < packetSize; ++idx, ++pos) {
+        const unsigned char l = dns_tolower(packet.at(pos));
+        currentHash = burtle(&l, 1, currentHash);
+      }
+    }
 
-    for(; p < end && *p; ++p) { // XXX if you embed a 0 in your qname we'll stop lowercasing there
-      const unsigned char l = dns_tolower(*p); // label lengths can safely be lower cased
-      ret=burtle(&l, 1, ret);
-    }                           // XXX the embedded 0 in the qname will break the subnet stripping
+    return currentHash;
+  }
 
-    if (p < end) {
-      ret = burtle(reinterpret_cast<const unsigned char*>(p), end-p, ret);
+  /* hash the packet from the beginning, including the qname. This skips:
+     - the query ID ;
+     - EDNS Cookie options, if any ;
+     - EDNS Client Subnet options, if any and skipECS is true.
+  */
+  static uint32_t canHashPacket(const std::string& packet, bool skipECS)
+  {
+    size_t pos = 0;
+    uint32_t currentHash = hashHeaderAndQName(packet, pos);
+    size_t packetSize = packet.size();
+
+    if (pos >= packetSize) {
+      return currentHash;
     }
 
-    return ret;
+    return hashAfterQname(packet, currentHash, pos, skipECS);
   }
 
   static bool queryHeaderMatches(const std::string& cachedQuery, const std::string& query)
@@ -108,41 +165,80 @@ public:
     return (cachedQuery.compare(/* skip the ID */ 2, sizeof(dnsheader) - 2, query, 2, sizeof(dnsheader) - 2) == 0);
   }
 
-  static bool queryMatches(const std::string& cachedQuery, const std::string& query, const DNSName& qname)
+  static bool queryMatches(const std::string& cachedQuery, const std::string& query, const DNSName& qname, const std::unordered_set<uint16_t>& optionsToIgnore)
   {
+    const size_t querySize = query.size();
+    const size_t cachedQuerySize = cachedQuery.size();
+    if (querySize != cachedQuerySize) {
+      return false;
+    }
+
     if (!queryHeaderMatches(cachedQuery, query)) {
       return false;
     }
 
     size_t pos = sizeof(dnsheader) + qname.wirelength();
 
-    return (cachedQuery.compare(pos, cachedQuery.size() - pos, query, pos, query.size() - pos) == 0);
-  }
+    /* we need at least 2 (QTYPE) + 2 (QCLASS)
+       + OPT root label (1), type (2), class (2) and ttl (4)
+       + the OPT RR rdlen (2)
+       = 15
+    */
+    const struct dnsheader* dh = reinterpret_cast<const struct dnsheader*>(query.data());
+    if (ntohs(dh->qdcount) != 1 || ntohs(dh->ancount) != 0 || ntohs(dh->nscount) != 0 || ntohs(dh->arcount) != 1 || (pos + 15) >= querySize || optionsToIgnore.empty()) {
+      return cachedQuery.compare(pos, cachedQuerySize - pos, query, pos, querySize - pos) == 0;
+    }
 
-  static bool queryMatches(const std::string& cachedQuery, const std::string& query, const DNSName& qname, uint16_t ecsBegin, uint16_t ecsEnd)
-  {
-    if (!queryHeaderMatches(cachedQuery, query)) {
+    /* compare up to the first option, if any */
+    if (cachedQuery.compare(pos, 15, query, pos, 15) != 0) {
       return false;
     }
 
-    size_t pos = sizeof(dnsheader) + qname.wirelength();
+    /* skip the qtype (2), qclass (2) */
+    /* root label (1), type (2), class (2) and ttl (4) */
+    /* already compared above */
+    pos += 13;
 
-    if (ecsBegin != 0 && ecsBegin >= pos && ecsEnd > ecsBegin) {
-      if (cachedQuery.compare(pos, ecsBegin - pos, query, pos, ecsBegin - pos) != 0) {
-        return false;
+    const uint16_t rdLen = ((static_cast<unsigned char>(query.at(pos)) * 256) + static_cast<unsigned char>(query.at(pos + 1)));
+    /* skip the rd length */
+    /* already compared above */
+    pos += sizeof(uint16_t);
+
+    if (rdLen > (querySize - pos)) {
+      /* something is wrong, let's just compare everything */
+      return cachedQuery.compare(pos, cachedQuerySize - pos, query, pos, querySize - pos) == 0;
+    }
+
+    uint16_t rdataRead = 0;
+    uint16_t optionCode;
+    uint16_t optionLen;
+
+    while (pos < querySize && rdataRead < rdLen && getNextEDNSOption(&query.at(pos), rdLen - rdataRead, optionCode, optionLen)) {
+      if (optionLen > (rdLen - rdataRead)) {
+        return cachedQuery.compare(pos, cachedQuerySize - pos, query, pos, querySize - pos) == 0;
       }
 
-      if (cachedQuery.compare(ecsEnd, cachedQuery.size() - ecsEnd, query, ecsEnd, query.size() - ecsEnd) != 0) {
+      /* compare the option code and length */
+      if (cachedQuery.compare(pos, 4, query, pos, 4) != 0) {
         return false;
       }
-    }
-    else {
-      if (cachedQuery.compare(pos, cachedQuery.size() - pos, query, pos, query.size() - pos) != 0) {
-        return false;
+      pos += 4;
+      rdataRead += 4;
+
+      if (optionLen > 0 && optionsToIgnore.count(optionCode) == 0) {
+        if (cachedQuery.compare(pos, optionLen, query, pos, optionLen) != 0) {
+          return false;
+        }
       }
+      pos += optionLen;
+      rdataRead += optionLen;
+    }
+
+    if (pos >= querySize) {
+        return true;
     }
 
-    return true;
+    return cachedQuery.compare(pos, cachedQuerySize - pos, query, pos, querySize - pos) == 0;
   }
 
 };
index 71a61f517b85a54f66ab56f411df159d3fb77b38..8f6b796d63afc94f9f8ca58f101fb5c70ad47991 100644 (file)
@@ -344,8 +344,6 @@ struct DNSComboWriter {
   unsigned int d_tag{0};
   uint32_t d_qhash{0};
   uint32_t d_ttlCap{std::numeric_limits<uint32_t>::max()};
-  uint16_t d_ecsBegin{0};
-  uint16_t d_ecsEnd{0};
   bool d_variable{false};
   bool d_ecsFound{false};
   bool d_ecsParsed{false};
@@ -1846,8 +1844,6 @@ static void startDoResolve(void *p)
                                             pw.getHeader()->rcode == RCode::ServFail ? SyncRes::s_packetcacheservfailttl :
                                             min(minTTL,SyncRes::s_packetcachettl),
                                             dq.validationState,
-                                            dc->d_ecsBegin,
-                                            dc->d_ecsEnd,
                                             std::move(pbMessage));
       }
       //      else cerr<<"Not putting in packet cache: "<<sr.wasVariable()<<endl;
@@ -2535,8 +2531,6 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr
   EDNSSubnetOpts ednssubnet;
   bool ecsFound = false;
   bool ecsParsed = false;
-  uint16_t ecsBegin = 0;
-  uint16_t ecsEnd = 0;
   std::vector<DNSRecord> records;
   boost::optional<int> rcode = boost::none;
   uint32_t ttlCap = std::numeric_limits<uint32_t>::max();
@@ -2613,10 +2607,10 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr
        as cacheable we would cache it with a wrong tag, so better safe than sorry. */
     vState valState;
     if (qnameParsed) {
-      cacheHit = (!SyncRes::s_nopacketcache && t_packetCache->getResponsePacket(ctag, question, qname, qtype, qclass, g_now.tv_sec, &response, &age, &valState, &qhash, &ecsBegin, &ecsEnd, pbMessage ? &(*pbMessage) : nullptr));
+      cacheHit = (!SyncRes::s_nopacketcache && t_packetCache->getResponsePacket(ctag, question, qname, qtype, qclass, g_now.tv_sec, &response, &age, &valState, &qhash, pbMessage ? &(*pbMessage) : nullptr));
     }
     else {
-      cacheHit = (!SyncRes::s_nopacketcache && t_packetCache->getResponsePacket(ctag, question, qname, &qtype, &qclass, g_now.tv_sec, &response, &age, &valState, &qhash, &ecsBegin, &ecsEnd, pbMessage ? &(*pbMessage) : nullptr));
+      cacheHit = (!SyncRes::s_nopacketcache && t_packetCache->getResponsePacket(ctag, question, qname, &qtype, &qclass, g_now.tv_sec, &response, &age, &valState, &qhash, pbMessage ? &(*pbMessage) : nullptr));
     }
 
     if (cacheHit) {
@@ -2711,8 +2705,6 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr
   dc->d_tcp=false;
   dc->d_ecsFound = ecsFound;
   dc->d_ecsParsed = ecsParsed;
-  dc->d_ecsBegin = ecsBegin;
-  dc->d_ecsEnd = ecsEnd;
   dc->d_ednssubnet = ednssubnet;
   dc->d_ttlCap = ttlCap;
   dc->d_variable = variable;
@@ -4832,7 +4824,6 @@ try
     t_bogusqueryring = std::unique_ptr<boost::circular_buffer<pair<DNSName, uint16_t> > >(new boost::circular_buffer<pair<DNSName, uint16_t> >());
     t_bogusqueryring->set_capacity(ringsize);
   }
-
   MT=std::unique_ptr<MTasker<PacketID,string> >(new MTasker<PacketID,string>(::arg().asNum("stack-size")));
   threadInfo.mt = MT.get();
 
index f4d145fffcb66e60f08511f25c19a3e418062b88..0f93c5d317f7973bdc64a01fc1c45641fd4be01f 100644 (file)
@@ -39,25 +39,22 @@ int RecursorPacketCache::doWipePacketCache(const DNSName& name, uint16_t qtype,
   return count;
 }
 
-bool RecursorPacketCache::qrMatch(const packetCache_t::index<HashTag>::type::iterator& iter, const std::string& queryPacket, const DNSName& qname, uint16_t qtype, uint16_t qclass, uint16_t ecsBegin, uint16_t ecsEnd)
+bool RecursorPacketCache::qrMatch(const packetCache_t::index<HashTag>::type::iterator& iter, const std::string& queryPacket, const DNSName& qname, uint16_t qtype, uint16_t qclass)
 {
   // this ignores checking on the EDNS subnet flags! 
   if (qname != iter->d_name || iter->d_type != qtype || iter->d_class != qclass) {
     return false;
   }
 
-  if (iter->d_ecsBegin != ecsBegin || iter->d_ecsEnd != ecsEnd) {
-    return false;
-  }
-
-  return queryMatches(iter->d_query, queryPacket, qname, ecsBegin, ecsEnd);
+  static const std::unordered_set<uint16_t> optionsToSkip{ EDNSOptionCode::COOKIE, EDNSOptionCode::ECS };
+  return queryMatches(iter->d_query, queryPacket, qname, optionsToSkip);
 }
 
-bool RecursorPacketCache::checkResponseMatches(std::pair<packetCache_t::index<HashTag>::type::iterator, packetCache_t::index<HashTag>::type::iterator> range, const std::string& queryPacket, const DNSName& qname, uint16_t qtype, uint16_t qclass, time_t now, std::string* responsePacket, uint32_t* age, vState* valState, RecProtoBufMessage* protobufMessage, uint16_t ecsBegin, uint16_t ecsEnd)
+bool RecursorPacketCache::checkResponseMatches(std::pair<packetCache_t::index<HashTag>::type::iterator, packetCache_t::index<HashTag>::type::iterator> range, const std::string& queryPacket, const DNSName& qname, uint16_t qtype, uint16_t qclass, time_t now, std::string* responsePacket, uint32_t* age, vState* valState, RecProtoBufMessage* protobufMessage)
 {
   for(auto iter = range.first ; iter != range.second ; ++iter) {
     // the possibility is VERY real that we get hits that are not right - birthday paradox
-    if (!qrMatch(iter, queryPacket, qname, qtype, qclass, ecsBegin, ecsEnd)) {
+    if (!qrMatch(iter, queryPacket, qname, qtype, qclass)) {
       continue;
     }
 
@@ -84,7 +81,6 @@ bool RecursorPacketCache::checkResponseMatches(std::pair<packetCache_t::index<Ha
         }
       }
 #endif
-      
       return true;
     }
     else {
@@ -102,25 +98,21 @@ bool RecursorPacketCache::getResponsePacket(unsigned int tag, const std::string&
 {
   DNSName qname;
   uint16_t qtype, qclass;
-  uint16_t ecsBegin;
-  uint16_t ecsEnd;
   vState valState;
-  return getResponsePacket(tag, queryPacket, qname, &qtype, &qclass, now, responsePacket, age, &valState, qhash, &ecsBegin, &ecsEnd, nullptr);
+  return getResponsePacket(tag, queryPacket, qname, &qtype, &qclass, now, responsePacket, age, &valState, qhash, nullptr);
 }
 
 bool RecursorPacketCache::getResponsePacket(unsigned int tag, const std::string& queryPacket, const DNSName& qname, uint16_t qtype, uint16_t qclass, time_t now,
                                             std::string* responsePacket, uint32_t* age, uint32_t* qhash)
 {
   vState valState;
-  uint16_t ecsBegin;
-  uint16_t ecsEnd;
-  return getResponsePacket(tag, queryPacket, qname, qtype, qclass, now, responsePacket, age, &valState, qhash, &ecsBegin, &ecsEnd, nullptr);
+  return getResponsePacket(tag, queryPacket, qname, qtype, qclass, now, responsePacket, age, &valState, qhash, nullptr);
 }
 
 bool RecursorPacketCache::getResponsePacket(unsigned int tag, const std::string& queryPacket, const DNSName& qname, uint16_t qtype, uint16_t qclass, time_t now,
-                                            std::string* responsePacket, uint32_t* age, vState* valState, uint32_t* qhash, uint16_t* ecsBegin, uint16_t* ecsEnd, RecProtoBufMessage* protobufMessage)
+                                            std::string* responsePacket, uint32_t* age, vState* valState, uint32_t* qhash, RecProtoBufMessage* protobufMessage)
 {
-  *qhash = canHashPacket(queryPacket, ecsBegin, ecsEnd);
+  *qhash = canHashPacket(queryPacket, true);
   const auto& idx = d_packetCache.get<HashTag>();
   auto range = idx.equal_range(tie(tag,*qhash));
 
@@ -129,13 +121,13 @@ bool RecursorPacketCache::getResponsePacket(unsigned int tag, const std::string&
     return false;
   }
 
-  return checkResponseMatches(range, queryPacket, qname, qtype, qclass, now, responsePacket, age, valState, protobufMessage, *ecsBegin, *ecsEnd);
+  return checkResponseMatches(range, queryPacket, qname, qtype, qclass, now, responsePacket, age, valState, protobufMessage);
 }
 
 bool RecursorPacketCache::getResponsePacket(unsigned int tag, const std::string& queryPacket, DNSName& qname, uint16_t* qtype, uint16_t* qclass, time_t now,
-                                            std::string* responsePacket, uint32_t* age, vState* valState, uint32_t* qhash, uint16_t* ecsBegin, uint16_t* ecsEnd, RecProtoBufMessage* protobufMessage)
+                                            std::string* responsePacket, uint32_t* age, vState* valState, uint32_t* qhash, RecProtoBufMessage* protobufMessage)
 {
-  *qhash = canHashPacket(queryPacket, ecsBegin, ecsEnd);
+  *qhash = canHashPacket(queryPacket, true);
   const auto& idx = d_packetCache.get<HashTag>();
   auto range = idx.equal_range(tie(tag,*qhash));
 
@@ -146,11 +138,11 @@ bool RecursorPacketCache::getResponsePacket(unsigned int tag, const std::string&
 
   qname = DNSName(queryPacket.c_str(), queryPacket.length(), sizeof(dnsheader), false, qtype, qclass, 0);
 
-  return checkResponseMatches(range, queryPacket, qname, *qtype, *qclass, now, responsePacket, age, valState, protobufMessage, *ecsBegin, *ecsEnd);
+  return checkResponseMatches(range, queryPacket, qname, *qtype, *qclass, now, responsePacket, age, valState, protobufMessage);
 }
 
 
-void RecursorPacketCache::insertResponsePacket(unsigned int tag, uint32_t qhash, std::string&& query, const DNSName& qname, uint16_t qtype, uint16_t qclass, std::string&& responsePacket, time_t now, uint32_t ttl, const vState& valState, uint16_t ecsBegin, uint16_t ecsEnd, boost::optional<RecProtoBufMessage>&& protobufMessage)
+void RecursorPacketCache::insertResponsePacket(unsigned int tag, uint32_t qhash, std::string&& query, const DNSName& qname, uint16_t qtype, uint16_t qclass, std::string&& responsePacket, time_t now, uint32_t ttl, const vState& valState, boost::optional<RecProtoBufMessage>&& protobufMessage)
 {
   auto& idx = d_packetCache.get<HashTag>();
   auto range = idx.equal_range(tie(tag,qhash));
@@ -164,8 +156,6 @@ void RecursorPacketCache::insertResponsePacket(unsigned int tag, uint32_t qhash,
     moveCacheItemToBack<SequencedTag>(d_packetCache, iter);
     iter->d_packet = std::move(responsePacket);
     iter->d_query = std::move(query);
-    iter->d_ecsBegin = ecsBegin;
-    iter->d_ecsEnd = ecsEnd;
     iter->d_ttd = now + ttl;
     iter->d_creation = now;
     iter->d_vstate = valState;
@@ -181,8 +171,6 @@ void RecursorPacketCache::insertResponsePacket(unsigned int tag, uint32_t qhash,
   if(iter == range.second) { // nothing to refresh
     struct Entry e(qname, std::move(responsePacket), std::move(query));
     e.d_qhash = qhash;
-    e.d_ecsBegin = ecsBegin;
-    e.d_ecsEnd = ecsEnd;
     e.d_type = qtype;
     e.d_class = qclass;
     e.d_ttd = now+ttl;
index 75adce7d2aaffd84e6aad769af9bb33fc21b3989..8ffc61ba83764bf9757ebcc37653c9bd0115a034 100644 (file)
@@ -53,9 +53,9 @@ public:
   RecursorPacketCache();
   bool getResponsePacket(unsigned int tag, const std::string& queryPacket, time_t now, std::string* responsePacket, uint32_t* age, uint32_t* qhash);
   bool getResponsePacket(unsigned int tag, const std::string& queryPacket, const DNSName& qname, uint16_t qtype, uint16_t qclass, time_t now, std::string* responsePacket, uint32_t* age, uint32_t* qhash);
-  bool getResponsePacket(unsigned int tag, const std::string& queryPacket, const DNSName& qname, uint16_t qtype, uint16_t qclass, time_t now, std::string* responsePacket, uint32_t* age, vState* valState, uint32_t* qhash, uint16_t* ecsBegin, uint16_t* ecsEnd, RecProtoBufMessage* protobufMessage);
-  bool getResponsePacket(unsigned int tag, const std::string& queryPacket, DNSName& qname, uint16_t* qtype, uint16_t* qclass, time_t now, std::string* responsePacket, uint32_t* age, vState* valState, uint32_t* qhash, uint16_t* ecsBegin, uint16_t* ecsEnd, RecProtoBufMessage* protobufMessage);
-  void insertResponsePacket(unsigned int tag, uint32_t qhash, std::string&& query, const DNSName& qname, uint16_t qtype, uint16_t qclass, std::string&& responsePacket, time_t now, uint32_t ttl, const vState& valState, uint16_t ecsBegin, uint16_t ecsEnd, boost::optional<RecProtoBufMessage>&& protobufMessage);
+  bool getResponsePacket(unsigned int tag, const std::string& queryPacket, const DNSName& qname, uint16_t qtype, uint16_t qclass, time_t now, std::string* responsePacket, uint32_t* age, vState* valState, uint32_t* qhash, RecProtoBufMessage* protobufMessage);
+  bool getResponsePacket(unsigned int tag, const std::string& queryPacket, DNSName& qname, uint16_t* qtype, uint16_t* qclass, time_t now, std::string* responsePacket, uint32_t* age, vState* valState, uint32_t* qhash, RecProtoBufMessage* protobufMessage);
+  void insertResponsePacket(unsigned int tag, uint32_t qhash, std::string&& query, const DNSName& qname, uint16_t qtype, uint16_t qclass, std::string&& responsePacket, time_t now, uint32_t ttl, const vState& valState, boost::optional<RecProtoBufMessage>&& protobufMessage);
   void doPruneTo(size_t maxSize=250000);
   uint64_t doDump(int fd);
   int doWipePacketCache(const DNSName& name, uint16_t qtype=0xffff, bool subtree=false);
@@ -86,8 +86,6 @@ private:
     uint32_t d_tag;
     uint16_t d_type;
     uint16_t d_class;
-    mutable uint16_t d_ecsBegin;
-    mutable uint16_t d_ecsEnd;
     mutable vState d_vstate;
     inline bool operator<(const struct Entry& rhs) const;
 
@@ -109,8 +107,8 @@ private:
   
   packetCache_t d_packetCache;
 
-  static bool qrMatch(const packetCache_t::index<HashTag>::type::iterator& iter, const std::string& queryPacket, const DNSName& qname, uint16_t qtype, uint16_t qclass, uint16_t ecsBegin, uint16_t ecsEnd);
-  bool checkResponseMatches(std::pair<packetCache_t::index<HashTag>::type::iterator, packetCache_t::index<HashTag>::type::iterator> range, const std::string& queryPacket, const DNSName& qname, uint16_t qtype, uint16_t qclass, time_t now, std::string* responsePacket, uint32_t* age, vState* valState, RecProtoBufMessage* protobufMessage, uint16_t ecsBegin, uint16_t ecsEnd);
+  static bool qrMatch(const packetCache_t::index<HashTag>::type::iterator& iter, const std::string& queryPacket, const DNSName& qname, uint16_t qtype, uint16_t qclass);
+  bool checkResponseMatches(std::pair<packetCache_t::index<HashTag>::type::iterator, packetCache_t::index<HashTag>::type::iterator> range, const std::string& queryPacket, const DNSName& qname, uint16_t qtype, uint16_t qclass, time_t now, std::string* responsePacket, uint32_t* age, vState* valState, RecProtoBufMessage* protobufMessage);
 
 public:
   void preRemoval(const Entry& entry)
index 573047c75eec4a66bbcf773835a3b7c42553b4e4..4bba5b5f5f20f1a5dfb7dfd17c7f0026e95b5b5c 100644 (file)
@@ -185,6 +185,7 @@ pdns_recursor_SOURCES = \
        uuid-utils.hh uuid-utils.cc \
        validate.cc validate.hh validate-recursor.cc validate-recursor.hh \
        version.cc version.hh \
+       views.hh \
        webserver.cc webserver.hh \
        ws-api.cc ws-api.hh \
        ws-recursor.cc ws-recursor.hh \
diff --git a/pdns/recursordist/views.hh b/pdns/recursordist/views.hh
new file mode 120000 (symlink)
index 0000000..2213b7d
--- /dev/null
@@ -0,0 +1 @@
+../views.hh
\ No newline at end of file
index becd1ec2e77e9fe719c0f71465dc2df8ba564218..3ea30998c13a2fd557644ec7ba5f4e93137cde3c 100644 (file)
@@ -11,6 +11,7 @@
 #include "dnswriter.hh"
 #include "dnsdist-cache.hh"
 #include "gettime.hh"
+#include "packetcache.hh"
 
 BOOST_AUTO_TEST_SUITE(test_dnsdistpacketcache_cc)
 
@@ -418,7 +419,7 @@ BOOST_AUTO_TEST_CASE(test_PCCollision) {
   boost::optional<Netmask> subnetOut;
   bool dnssecOK = false;
 
-  /* lookup for a query with an ECS value of 10.0.118.46/32,
+  /* lookup for a query with a first ECS value,
      insert a corresponding response */
   {
     vector<uint8_t> query;
@@ -427,7 +428,7 @@ BOOST_AUTO_TEST_CASE(test_PCCollision) {
     pwQ.getHeader()->id = qid;
     DNSPacketWriter::optvect_t ednsOptions;
     EDNSSubnetOpts opt;
-    opt.source = Netmask("10.0.118.46/32");
+    opt.source = Netmask("10.0.59.220/32");
     ednsOptions.push_back(std::make_pair(EDNSOptionCode::ECS, makeEDNSSubnetOptsString(opt)));
     pwQ.addOpt(512, 0, 0, ednsOptions);
     pwQ.commit();
@@ -463,7 +464,7 @@ BOOST_AUTO_TEST_CASE(test_PCCollision) {
     BOOST_CHECK_EQUAL(subnetOut->toString(), opt.source.toString());
   }
 
-  /* now lookup for the same query with an ECS value of 10.0.123.193/32
+  /* now lookup for the same query with a different ECS value,
      we should get the same key (collision) but no match */
   {
     vector<uint8_t> query;
@@ -472,7 +473,7 @@ BOOST_AUTO_TEST_CASE(test_PCCollision) {
     pwQ.getHeader()->id = qid;
     DNSPacketWriter::optvect_t ednsOptions;
     EDNSSubnetOpts opt;
-    opt.source = Netmask("10.0.123.193/32");
+    opt.source = Netmask("10.0.167.48/32");
     ednsOptions.push_back(std::make_pair(EDNSOptionCode::ECS, makeEDNSSubnetOptsString(opt)));
     pwQ.addOpt(512, 0, 0, ednsOptions);
     pwQ.commit();
@@ -490,6 +491,47 @@ BOOST_AUTO_TEST_CASE(test_PCCollision) {
     BOOST_CHECK_EQUAL(subnetOut->toString(), opt.source.toString());
     BOOST_CHECK_EQUAL(PC.getLookupCollisions(), 1U);
   }
+
+#if 0
+  /* to be able to compute a new collision if the packet cache hashing code is updated */
+  {
+    DNSDistPacketCache pc(10000);
+    DNSPacketWriter::optvect_t ednsOptions;
+    EDNSSubnetOpts opt;
+    std::map<uint32_t, Netmask> colMap;
+    size_t collisions = 0;
+    size_t total = 0;
+    //qname = DNSName("collision-with-ecs-parsing.cache.tests.powerdns.com.");
+
+    for (size_t idxA = 0; idxA < 256; idxA++) {
+      for (size_t idxB = 0; idxB < 256; idxB++) {
+        for (size_t idxC = 0; idxC < 256; idxC++) {
+          vector<uint8_t> secondQuery;
+          DNSPacketWriter pwFQ(secondQuery, qname, QType::AAAA, QClass::IN, 0);
+          pwFQ.getHeader()->rd = 1;
+          pwFQ.getHeader()->qr = false;
+          pwFQ.getHeader()->id = 0x42;
+          opt.source = Netmask("10." + std::to_string(idxA) + "." + std::to_string(idxB) + "." + std::to_string(idxC) + "/32");
+          ednsOptions.clear();
+          ednsOptions.push_back(std::make_pair(EDNSOptionCode::ECS, makeEDNSSubnetOptsString(opt)));
+          pwFQ.addOpt(512, 0, 0, ednsOptions);
+          pwFQ.commit();
+          secondKey = pc.getKey(qname.toDNSString(), qname.wirelength(), secondQuery.data(), secondQuery.size(), false);
+          auto pair = colMap.insert(std::make_pair(secondKey, opt.source));
+          total++;
+          if (!pair.second) {
+            collisions++;
+            cerr<<"Collision between "<<colMap[secondKey].toString()<<" and "<<opt.source.toString()<<" for key "<<secondKey<<endl;
+            goto done;
+          }
+        }
+      }
+    }
+  done:
+    cerr<<"collisions: "<<collisions<<endl;
+    cerr<<"total: "<<total<<endl;
+  }
+#endif
 }
 
 BOOST_AUTO_TEST_CASE(test_PCDNSSECCollision) {
index 5fcfff8216aab30b4db38b773d2fb44a5f95c3b0..e37c4b062ee363df0912e228677b88a988b60413 100644 (file)
@@ -165,7 +165,7 @@ try
        we directly compute the hash instead of querying the
        cache because 1/ it's faster 2/ no deferred-lookup issues
     */
-    q.setHash(g_PC->canHashPacket(q.getString()));
+    q.setHash(g_PC->canHashPacket(q.getString(), false));
 
     const unsigned int maxTTL = 3600;
     g_PC->insert(q, r, maxTTL);
index 3ce89b79d854261a4340e658eefc9365a25f695d..6a55effcffe8850ce275b68591474b724338b0b0 100644 (file)
@@ -21,6 +21,8 @@ BOOST_AUTO_TEST_CASE(test_PacketCacheAuthCollision) {
   uint16_t qtype = QType::AAAA;
   EDNSSubnetOpts opt;
   DNSPacketWriter::optvect_t ednsOptions;
+  static const std::unordered_set<uint16_t> optionsToSkip{ EDNSOptionCode::COOKIE };
+  static const std::unordered_set<uint16_t> noOptionsToSkip{ };
 
   {
     /* same query, different IDs */
@@ -30,7 +32,7 @@ BOOST_AUTO_TEST_CASE(test_PacketCacheAuthCollision) {
     pw1.getHeader()->qr = false;
     pw1.getHeader()->id = 0x42;
     string spacket1((const char*)&packet[0], packet.size());
-    auto hash1 = PacketCache::canHashPacket(spacket1);
+    auto hash1 = PacketCache::canHashPacket(spacket1, false);
 
     packet.clear();
     DNSPacketWriter pw2(packet, qname, qtype);
@@ -38,10 +40,10 @@ BOOST_AUTO_TEST_CASE(test_PacketCacheAuthCollision) {
     pw2.getHeader()->qr = false;
     pw2.getHeader()->id = 0x84;
     string spacket2((const char*)&packet[0], packet.size());
-    auto hash2 = PacketCache::canHashPacket(spacket2);
+    auto hash2 = PacketCache::canHashPacket(spacket2, false);
 
     BOOST_CHECK_EQUAL(hash1, hash2);
-    BOOST_CHECK(PacketCache::queryMatches(spacket1, spacket2, qname));
+    BOOST_CHECK(PacketCache::queryMatches(spacket1, spacket2, qname, optionsToSkip));
   }
 
   {
@@ -51,32 +53,69 @@ BOOST_AUTO_TEST_CASE(test_PacketCacheAuthCollision) {
     pw1.getHeader()->rd = true;
     pw1.getHeader()->qr = false;
     pw1.getHeader()->id = 0x42;
-    opt.source = Netmask("10.0.18.199/32");
+    opt.source = Netmask("10.0.152.74/32");
     ednsOptions.clear();
     ednsOptions.push_back(std::make_pair(EDNSOptionCode::ECS, makeEDNSSubnetOptsString(opt)));
     pw1.addOpt(512, 0, 0, ednsOptions);
     pw1.commit();
 
     string spacket1((const char*)&packet[0], packet.size());
-    auto hash1 = PacketCache::canHashPacket(spacket1);
+    auto hash1 = PacketCache::canHashPacket(spacket1, false);
 
     packet.clear();
     DNSPacketWriter pw2(packet, qname, qtype);
     pw2.getHeader()->rd = true;
     pw2.getHeader()->qr = false;
     pw2.getHeader()->id = 0x84;
-    opt.source = Netmask("10.0.131.66/32");
+    opt.source = Netmask("10.2.70.250/32");
     ednsOptions.clear();
     ednsOptions.push_back(std::make_pair(EDNSOptionCode::ECS, makeEDNSSubnetOptsString(opt)));
     pw2.addOpt(512, 0, 0, ednsOptions);
     pw2.commit();
 
     string spacket2((const char*)&packet[0], packet.size());
-    auto hash2 = PacketCache::canHashPacket(spacket2);
+    auto hash2 = PacketCache::canHashPacket(spacket2, false);
 
     BOOST_CHECK_EQUAL(hash1, hash2);
     /* the hash is the same but we should _not_ match */
-    BOOST_CHECK(!PacketCache::queryMatches(spacket1, spacket2, qname));
+    BOOST_CHECK(!PacketCache::queryMatches(spacket1, spacket2, qname, optionsToSkip));
+
+#if 0
+    /* to be able to compute a new collision if the hashing function is updated */
+    {
+    std::map<uint32_t, Netmask> colMap;
+    size_t collisions = 0;
+    size_t total = 0;
+
+    for (size_t idxA = 0; idxA < 256; idxA++) {
+      for (size_t idxB = 0; idxB < 256; idxB++) {
+        for (size_t idxC = 0; idxC < 256; idxC++) {
+          vector<uint8_t> secondQuery;
+          DNSPacketWriter pwFQ(secondQuery, qname, QType::AAAA, QClass::IN, 0);
+          pwFQ.getHeader()->rd = 1;
+          pwFQ.getHeader()->qr = false;
+          pwFQ.getHeader()->id = 0x42;
+          opt.source = Netmask("10." + std::to_string(idxA) + "." + std::to_string(idxB) + "." + std::to_string(idxC) + "/32");
+          ednsOptions.clear();
+          ednsOptions.push_back(std::make_pair(EDNSOptionCode::ECS, makeEDNSSubnetOptsString(opt)));
+          pwFQ.addOpt(512, 0, 0, ednsOptions);
+          pwFQ.commit();
+          auto secondKey = PacketCache::canHashPacket(std::string(reinterpret_cast<const char *>(secondQuery.data()), secondQuery.size()), false);
+          auto pair = colMap.insert(std::make_pair(secondKey, opt.source));
+          total++;
+          if (!pair.second) {
+            collisions++;
+            cerr<<"Collision between "<<colMap[secondKey].toString()<<" and "<<opt.source.toString()<<" for key "<<secondKey<<endl;
+            goto done1;
+          }
+        }
+      }
+    }
+  done1:
+    cerr<<"collisions: "<<collisions<<endl;
+    cerr<<"total: "<<total<<endl;
+    }
+#endif
   }
 
   {
@@ -86,21 +125,21 @@ BOOST_AUTO_TEST_CASE(test_PacketCacheAuthCollision) {
     pw1.getHeader()->rd = true;
     pw1.getHeader()->qr = false;
     pw1.getHeader()->id = 0x42;
-    opt.source = Netmask("47.8.0.0/32");
+    opt.source = Netmask("10.0.34.159/32");
     ednsOptions.clear();
     ednsOptions.push_back(std::make_pair(EDNSOptionCode::ECS, makeEDNSSubnetOptsString(opt)));
     pw1.addOpt(512, 0, EDNSOpts::DNSSECOK, ednsOptions);
     pw1.commit();
 
     string spacket1((const char*)&packet[0], packet.size());
-    auto hash1 = PacketCache::canHashPacket(spacket1);
+    auto hash1 = PacketCache::canHashPacket(spacket1, false);
 
     packet.clear();
     DNSPacketWriter pw2(packet, qname, qtype);
     pw2.getHeader()->rd = true;
     pw2.getHeader()->qr = false;
     pw2.getHeader()->id = 0x84;
-    opt.source = Netmask("18.43.1.0/32");
+    opt.source = Netmask("10.0.179.58/32");
     ednsOptions.clear();
     ednsOptions.push_back(std::make_pair(EDNSOptionCode::ECS, makeEDNSSubnetOptsString(opt)));
     /* no EDNSOpts::DNSSECOK !! */
@@ -108,11 +147,11 @@ BOOST_AUTO_TEST_CASE(test_PacketCacheAuthCollision) {
     pw2.commit();
 
     string spacket2((const char*)&packet[0], packet.size());
-    auto hash2 = PacketCache::canHashPacket(spacket2);
+    auto hash2 = PacketCache::canHashPacket(spacket2, false);
 
     BOOST_CHECK_EQUAL(hash1, hash2);
     /* the hash is the same but we should _not_ match */
-    BOOST_CHECK(!PacketCache::queryMatches(spacket1, spacket2, qname));
+    BOOST_CHECK(!PacketCache::queryMatches(spacket1, spacket2, qname, optionsToSkip));
   }
 
   {
@@ -128,16 +167,12 @@ BOOST_AUTO_TEST_CASE(test_PacketCacheAuthCollision) {
     EDNSCookiesOpt cookiesOpt;
     cookiesOpt.client = string("deadbeef");
     cookiesOpt.server = string("deadbeef");
-    cookiesOpt.server[4] = -42;
-    cookiesOpt.server[5] = -6;
-    cookiesOpt.server[6] = 1;
-    cookiesOpt.server[7] = 0;
     ednsOptions.push_back(std::make_pair(EDNSOptionCode::COOKIE, makeEDNSCookiesOptString(cookiesOpt)));
     pw1.addOpt(512, 0, EDNSOpts::DNSSECOK, ednsOptions);
     pw1.commit();
 
     string spacket1((const char*)&packet[0], packet.size());
-    auto hash1 = PacketCache::canHashPacket(spacket1);
+    auto hash1 = PacketCache::canHashPacket(spacket1, false);
 
     packet.clear();
     DNSPacketWriter pw2(packet, qname, qtype);
@@ -148,21 +183,69 @@ BOOST_AUTO_TEST_CASE(test_PacketCacheAuthCollision) {
     ednsOptions.clear();
     ednsOptions.push_back(std::make_pair(EDNSOptionCode::ECS, makeEDNSSubnetOptsString(opt)));
     cookiesOpt.client = string("deadbeef");
-    cookiesOpt.server = string("deadbeef");
-    cookiesOpt.server[4] = 29;
-    cookiesOpt.server[5] = -79;
-    cookiesOpt.server[6] = 1;
-    cookiesOpt.server[7] = 0;
+    cookiesOpt.server = string("badc0fee");
     ednsOptions.push_back(std::make_pair(EDNSOptionCode::COOKIE, makeEDNSCookiesOptString(cookiesOpt)));
     pw2.addOpt(512, 0, EDNSOpts::DNSSECOK, ednsOptions);
     pw2.commit();
 
     string spacket2((const char*)&packet[0], packet.size());
-    auto hash2 = PacketCache::canHashPacket(spacket2);
+    auto hash2 = PacketCache::canHashPacket(spacket2, false);
 
     BOOST_CHECK_EQUAL(hash1, hash2);
     /* the hash is the same but we should _not_ match */
-    BOOST_CHECK(!PacketCache::queryMatches(spacket1, spacket2, qname));
+    BOOST_CHECK(!PacketCache::queryMatches(spacket1, spacket2, qname, noOptionsToSkip));
+    /* but it does match if we skip cookies, though */
+    BOOST_CHECK(PacketCache::queryMatches(spacket1, spacket2, qname, optionsToSkip));
+
+#if 0
+    {
+      /* to be able to compute a new collision if the packet cache hashing code is updated */
+    std::map<uint32_t, Netmask> colMap;
+    size_t collisions = 0;
+    size_t total = 0;
+
+    for (size_t idxA = 0; idxA < 256; idxA++) {
+      for (size_t idxB = 0; idxB < 256; idxB++) {
+        for (size_t idxC = 0; idxC < 256; idxC++) {
+          vector<uint8_t> secondQuery;
+          DNSPacketWriter pwFQ(secondQuery, qname, QType::AAAA, QClass::IN, 0);
+          pwFQ.getHeader()->rd = 1;
+          pwFQ.getHeader()->qr = false;
+          pwFQ.getHeader()->id = 0x42;
+          opt.source = Netmask("10." + std::to_string(idxA) + "." + std::to_string(idxB) + "." + std::to_string(idxC) + "/32");
+          ednsOptions.clear();
+          ednsOptions.push_back(std::make_pair(EDNSOptionCode::ECS, makeEDNSSubnetOptsString(opt)));
+          pwFQ.addOpt(512, 0, 32768, ednsOptions);
+          pwFQ.commit();
+          auto secondKey = PacketCache::canHashPacket(std::string(reinterpret_cast<const char *>(secondQuery.data()), secondQuery.size()), false);
+          colMap.insert(std::make_pair(secondKey, opt.source));
+
+          secondQuery.clear();
+          DNSPacketWriter pwSQ(secondQuery, qname, QType::AAAA, QClass::IN, 0);
+          pwSQ.getHeader()->rd = 1;
+          pwSQ.getHeader()->qr = false;
+          pwSQ.getHeader()->id = 0x42;
+          opt.source = Netmask("10." + std::to_string(idxA) + "." + std::to_string(idxB) + "." + std::to_string(idxC) + "/32");
+          ednsOptions.clear();
+          ednsOptions.push_back(std::make_pair(EDNSOptionCode::ECS, makeEDNSSubnetOptsString(opt)));
+          pwSQ.addOpt(512, 0, 0, ednsOptions);
+          pwSQ.commit();
+          secondKey = PacketCache::canHashPacket(std::string(reinterpret_cast<const char *>(secondQuery.data()), secondQuery.size()), false);
+
+          total++;
+          if (colMap.count(secondKey)) {
+            collisions++;
+            cerr<<"Collision between "<<colMap[secondKey].toString()<<" and "<<opt.source.toString()<<" for key "<<secondKey<<endl;
+            goto done2;
+          }
+        }
+      }
+    }
+  done2:
+    cerr<<"collisions: "<<collisions<<endl;
+    cerr<<"total: "<<total<<endl;
+  }
+#endif
   }
 }
 
@@ -172,8 +255,6 @@ BOOST_AUTO_TEST_CASE(test_PacketCacheRecSimple) {
   uint16_t qtype = QType::AAAA;
   EDNSSubnetOpts opt;
   DNSPacketWriter::optvect_t ednsOptions;
-  uint16_t ecsBegin;
-  uint16_t ecsEnd;
 
   {
     vector<uint8_t> packet;
@@ -191,10 +272,7 @@ BOOST_AUTO_TEST_CASE(test_PacketCacheRecSimple) {
     *(ptr + 1) = 255;
     /* truncate the end of the OPT header to try to trigger an out of bounds read */
     spacket1.resize(spacket1.size() - 6);
-    PacketCache::canHashPacket(spacket1, &ecsBegin, &ecsEnd);
-    /* no ECS */
-    BOOST_CHECK_EQUAL(ecsBegin, 0);
-    BOOST_CHECK_EQUAL(ecsEnd, 0);
+    BOOST_CHECK_NO_THROW(PacketCache::canHashPacket(spacket1, true));
   }
 }
 
@@ -205,8 +283,7 @@ BOOST_AUTO_TEST_CASE(test_PacketCacheRecCollision) {
   uint16_t qtype = QType::AAAA;
   EDNSSubnetOpts opt;
   DNSPacketWriter::optvect_t ednsOptions;
-  uint16_t ecsBegin;
-  uint16_t ecsEnd;
+  static const std::unordered_set<uint16_t> optionsToSkip{ EDNSOptionCode::COOKIE, EDNSOptionCode::ECS };
 
   {
     /* same query, different IDs */
@@ -216,10 +293,7 @@ BOOST_AUTO_TEST_CASE(test_PacketCacheRecCollision) {
     pw1.getHeader()->qr = false;
     pw1.getHeader()->id = 0x42;
     string spacket1((const char*)&packet[0], packet.size());
-    auto hash1 = PacketCache::canHashPacket(spacket1, &ecsBegin, &ecsEnd);
-    /* no ECS */
-    BOOST_CHECK_EQUAL(ecsBegin, 0);
-    BOOST_CHECK_EQUAL(ecsEnd, 0);
+    auto hash1 = PacketCache::canHashPacket(spacket1, true);
 
     packet.clear();
     DNSPacketWriter pw2(packet, qname, qtype);
@@ -227,13 +301,10 @@ BOOST_AUTO_TEST_CASE(test_PacketCacheRecCollision) {
     pw2.getHeader()->qr = false;
     pw2.getHeader()->id = 0x84;
     string spacket2((const char*)&packet[0], packet.size());
-    auto hash2 = PacketCache::canHashPacket(spacket2, &ecsBegin, &ecsEnd);
-    /* no ECS */
-    BOOST_CHECK_EQUAL(ecsBegin, 0);
-    BOOST_CHECK_EQUAL(ecsEnd, 0);
+    auto hash2 = PacketCache::canHashPacket(spacket2, true);
 
     BOOST_CHECK_EQUAL(hash1, hash2);
-    BOOST_CHECK(PacketCache::queryMatches(spacket1, spacket2, qname, ecsBegin, ecsEnd));
+    BOOST_CHECK(PacketCache::queryMatches(spacket1, spacket2, qname, optionsToSkip));
   }
 
   {
@@ -250,10 +321,7 @@ BOOST_AUTO_TEST_CASE(test_PacketCacheRecCollision) {
     pw1.commit();
 
     string spacket1((const char*)&packet[0], packet.size());
-    auto hash1 = PacketCache::canHashPacket(spacket1, &ecsBegin, &ecsEnd);
-    /* ECS value */
-    BOOST_CHECK_EQUAL(ecsBegin, sizeof(dnsheader) + qname.wirelength() + ( 2 * sizeof(uint16_t)) /* qtype */ + (2 * sizeof(uint16_t)) /* qclass */ + /* OPT root label */ 1 + sizeof(uint32_t) /* TTL */ + DNS_RDLENGTH_SIZE);
-    BOOST_CHECK_EQUAL(ecsEnd, ecsBegin + EDNS_OPTION_CODE_SIZE + EDNS_OPTION_LENGTH_SIZE + 2 /* family */ + 1 /* scope length */ + 1 /* source length */ + 4 /* IPv4 */);
+    auto hash1 = PacketCache::canHashPacket(spacket1, true);
 
     packet.clear();
     DNSPacketWriter pw2(packet, qname, qtype);
@@ -267,14 +335,11 @@ BOOST_AUTO_TEST_CASE(test_PacketCacheRecCollision) {
     pw2.commit();
 
     string spacket2((const char*)&packet[0], packet.size());
-    auto hash2 = PacketCache::canHashPacket(spacket2, &ecsBegin, &ecsEnd);
-    /* ECS value */
-    BOOST_CHECK_EQUAL(ecsBegin, sizeof(dnsheader) + qname.wirelength() + ( 2 * sizeof(uint16_t)) /* qtype */ + (2 * sizeof(uint16_t)) /* qclass */ + /* OPT root label */ 1 + sizeof(uint32_t) /* TTL */ + DNS_RDLENGTH_SIZE);
-    BOOST_CHECK_EQUAL(ecsEnd, ecsBegin + EDNS_OPTION_CODE_SIZE + EDNS_OPTION_LENGTH_SIZE + 2 /* family */ + 1 /* scope length */ + 1 /* source length */ + 4 /* IPv4 */);
+    auto hash2 = PacketCache::canHashPacket(spacket2, true);
 
     BOOST_CHECK_EQUAL(hash1, hash2);
     /* the hash is the same and we don't hash the ECS so we should match */
-    BOOST_CHECK(PacketCache::queryMatches(spacket1, spacket2, qname, ecsBegin, ecsEnd));
+    BOOST_CHECK(PacketCache::queryMatches(spacket1, spacket2, qname, optionsToSkip));
   }
 
   {
@@ -299,10 +364,7 @@ BOOST_AUTO_TEST_CASE(test_PacketCacheRecCollision) {
     pw1.commit();
 
     string spacket1((const char*)&packet[0], packet.size());
-    auto hash1 = PacketCache::canHashPacket(spacket1, &ecsBegin, &ecsEnd);
-    /* ECS value */
-    BOOST_CHECK_EQUAL(ecsBegin, sizeof(dnsheader) + qname.wirelength() + ( 2 * sizeof(uint16_t)) /* qtype */ + (2 * sizeof(uint16_t)) /* qclass */ + /* OPT root label */ 1 + sizeof(uint32_t) /* TTL */ + DNS_RDLENGTH_SIZE);
-    BOOST_CHECK_EQUAL(ecsEnd, ecsBegin + EDNS_OPTION_CODE_SIZE + EDNS_OPTION_LENGTH_SIZE + 2 /* family */ + 1 /* scope length */ + 1 /* source length */ + 4 /* IPv4 */);
+    auto hash1 = PacketCache::canHashPacket(spacket1, true);
 
     packet.clear();
     DNSPacketWriter pw2(packet, qname, qtype);
@@ -323,14 +385,15 @@ BOOST_AUTO_TEST_CASE(test_PacketCacheRecCollision) {
     pw2.commit();
 
     string spacket2((const char*)&packet[0], packet.size());
-    auto hash2 = PacketCache::canHashPacket(spacket2, &ecsBegin, &ecsEnd);
-    /* ECS value */
-    BOOST_CHECK_EQUAL(ecsBegin, sizeof(dnsheader) + qname.wirelength() + ( 2 * sizeof(uint16_t)) /* qtype */ + (2 * sizeof(uint16_t)) /* qclass */ + /* OPT root label */ 1 + sizeof(uint32_t) /* TTL */ + DNS_RDLENGTH_SIZE);
-    BOOST_CHECK_EQUAL(ecsEnd, ecsBegin + EDNS_OPTION_CODE_SIZE + EDNS_OPTION_LENGTH_SIZE + 2 /* family */ + 1 /* scope length */ + 1 /* source length */ + 4 /* IPv4 */);
+    auto hash2 = PacketCache::canHashPacket(spacket2, true);
 
     BOOST_CHECK_EQUAL(hash1, hash2);
     /* the hash is the same but we should _not_ match, even though we skip the ECS part, because the cookies are different */
-    BOOST_CHECK(!PacketCache::queryMatches(spacket1, spacket2, qname, ecsBegin, ecsEnd));
+    static const std::unordered_set<uint16_t> skipECSOnly{ EDNSOptionCode::ECS };
+    BOOST_CHECK(!PacketCache::queryMatches(spacket1, spacket2, qname, skipECSOnly));
+
+    /* we do match if we skip the cookie as well */
+    BOOST_CHECK(PacketCache::queryMatches(spacket1, spacket2, qname, optionsToSkip));
   }
 }
 
index b8038dc56e435e4a6d8838f3dc6c8695611fc5fc..fce6fcc823a91d78e50ed440bf588bfca7deb282 100644 (file)
@@ -45,16 +45,16 @@ BOOST_AUTO_TEST_CASE(test_recPacketCacheSimple) {
   pw.commit();
   string rpacket((const char*)&packet[0], packet.size());
 
-  rpc.insertResponsePacket(tag, qhash, string(qpacket), qname, QType::A, QClass::IN, string(rpacket), time(0), ttd, vState::Indeterminate, 0, 0, boost::none);
+  rpc.insertResponsePacket(tag, qhash, string(qpacket), qname, QType::A, QClass::IN, string(rpacket), time(0), ttd, vState::Indeterminate, boost::none);
   BOOST_CHECK_EQUAL(rpc.size(), 1U);
   rpc.doPruneTo(0);
   BOOST_CHECK_EQUAL(rpc.size(), 0U);
-  rpc.insertResponsePacket(tag, qhash, string(qpacket), qname, QType::A, QClass::IN, string(rpacket), time(0), ttd, vState::Indeterminate, 0, 0, boost::none);
+  rpc.insertResponsePacket(tag, qhash, string(qpacket), qname, QType::A, QClass::IN, string(rpacket), time(0), ttd, vState::Indeterminate, boost::none);
   BOOST_CHECK_EQUAL(rpc.size(), 1U);
   rpc.doWipePacketCache(qname);
   BOOST_CHECK_EQUAL(rpc.size(), 0U);
 
-  rpc.insertResponsePacket(tag, qhash, string(qpacket), qname, QType::A, QClass::IN, string(rpacket), time(0), ttd, vState::Indeterminate, 0, 0, boost::none);
+  rpc.insertResponsePacket(tag, qhash, string(qpacket), qname, QType::A, QClass::IN, string(rpacket), time(0), ttd, vState::Indeterminate, boost::none);
   BOOST_CHECK_EQUAL(rpc.size(), 1U);
   uint32_t qhash2 = 0;
   bool found = rpc.getResponsePacket(tag, qpacket, time(nullptr), &fpacket, &age, &qhash2);
@@ -140,11 +140,11 @@ BOOST_AUTO_TEST_CASE(test_recPacketCache_Tags) {
   BOOST_CHECK(r1packet != r2packet);
 
   /* inserting a response for tag1 */
-  rpc.insertResponsePacket(tag1, qhash, string(qpacket), qname, QType::A, QClass::IN, string(r1packet), time(0), ttd, vState::Indeterminate, 0, 0, boost::none);
+  rpc.insertResponsePacket(tag1, qhash, string(qpacket), qname, QType::A, QClass::IN, string(r1packet), time(0), ttd, vState::Indeterminate, boost::none);
   BOOST_CHECK_EQUAL(rpc.size(), 1U);
 
   /* inserting a different response for tag2, should not override the first one */
-  rpc.insertResponsePacket(tag2, qhash, string(qpacket), qname, QType::A, QClass::IN, string(r2packet), time(0), ttd, vState::Indeterminate, 0, 0, boost::none);
+  rpc.insertResponsePacket(tag2, qhash, string(qpacket), qname, QType::A, QClass::IN, string(r2packet), time(0), ttd, vState::Indeterminate, boost::none);
   BOOST_CHECK_EQUAL(rpc.size(), 2U);
 
   /* remove all responses from the cache */
@@ -152,10 +152,10 @@ BOOST_AUTO_TEST_CASE(test_recPacketCache_Tags) {
   BOOST_CHECK_EQUAL(rpc.size(), 0U);
 
   /* reinsert both */
-  rpc.insertResponsePacket(tag1, qhash, string(qpacket), qname, QType::A, QClass::IN, string(r1packet), time(0), ttd, vState::Indeterminate, 0, 0, boost::none);
+  rpc.insertResponsePacket(tag1, qhash, string(qpacket), qname, QType::A, QClass::IN, string(r1packet), time(0), ttd, vState::Indeterminate, boost::none);
   BOOST_CHECK_EQUAL(rpc.size(), 1U);
 
-  rpc.insertResponsePacket(tag2, qhash, string(qpacket), qname, QType::A, QClass::IN, string(r2packet), time(0), ttd, vState::Indeterminate, 0, 0, boost::none);
+  rpc.insertResponsePacket(tag2, qhash, string(qpacket), qname, QType::A, QClass::IN, string(r2packet), time(0), ttd, vState::Indeterminate, boost::none);
   BOOST_CHECK_EQUAL(rpc.size(), 2U);
 
   /* remove the responses by qname, should remove both */
@@ -163,7 +163,7 @@ BOOST_AUTO_TEST_CASE(test_recPacketCache_Tags) {
   BOOST_CHECK_EQUAL(rpc.size(), 0U);
 
   /* insert the response for tag1 */
-  rpc.insertResponsePacket(tag1, qhash, string(qpacket), qname, QType::A, QClass::IN, string(r1packet), time(0), ttd, vState::Indeterminate, 0, 0, boost::none);
+  rpc.insertResponsePacket(tag1, qhash, string(qpacket), qname, QType::A, QClass::IN, string(r1packet), time(0), ttd, vState::Indeterminate, boost::none);
   BOOST_CHECK_EQUAL(rpc.size(), 1U);
 
   /* we can retrieve it */
@@ -182,7 +182,7 @@ BOOST_AUTO_TEST_CASE(test_recPacketCache_Tags) {
   BOOST_CHECK_EQUAL(temphash, qhash);
 
   /* adding a response for the second tag */
-  rpc.insertResponsePacket(tag2, qhash, string(qpacket), qname, QType::A, QClass::IN, string(r2packet), time(0), ttd, vState::Indeterminate, 0, 0, boost::none);
+  rpc.insertResponsePacket(tag2, qhash, string(qpacket), qname, QType::A, QClass::IN, string(r2packet), time(0), ttd, vState::Indeterminate, boost::none);
   BOOST_CHECK_EQUAL(rpc.size(), 2U);
 
   /* We still get the correct response for the first tag */
diff --git a/pdns/views.hh b/pdns/views.hh
new file mode 100644 (file)
index 0000000..7e56c15
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * This file is part of PowerDNS or dnsdist.
+ * Copyright -- PowerDNS.COM B.V. and its contributors
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of version 2 of the GNU General Public License as
+ * published by the Free Software Foundation.
+ *
+ * In addition, for the avoidance of any doubt, permission is granted to
+ * link this program with OpenSSL and to (re)distribute the binaries
+ * produced as the result of such linking.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#pragma once
+
+#ifdef __cpp_lib_string_view
+using std::string_view;
+#else
+#include <boost/version.hpp>
+#if BOOST_VERSION >= 106100
+#include <boost/utility/string_view.hpp>
+using boost::string_view;
+#else
+#include <boost/utility/string_ref.hpp>
+using string_view = boost::string_ref;
+#endif
+#endif
index ccd536e3bd1ed1d3aee6e7e8ebbd0c952c5f9fee..d3f91753c2b5dc89ba7dd7cbd3a421b9e3c877b0 100644 (file)
@@ -3,6 +3,7 @@ import base64
 import time
 import dns
 import clientsubnetoption
+import cookiesoption
 from dnsdisttests import DNSDistTest
 
 class TestCaching(DNSDistTest):
@@ -468,6 +469,310 @@ class TestCaching(DNSDistTest):
 
         self.assertEquals(total, 1)
 
+    def testCacheDifferentCookies(self):
+        """
+        Cache: The content of cookies should be ignored by the cache
+        """
+        ttl = 600
+        name = 'cache-different-cookies.cache.tests.powerdns.com.'
+        eco = cookiesoption.CookiesOption(b'deadbeef', b'deadbeef')
+        query = dns.message.make_query(name, 'AAAA', 'IN', use_edns=True, payload=4096, options=[eco])
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    ttl,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.AAAA,
+                                    '::1')
+        response.answer.append(rrset)
+
+        # first query to fill the cache
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+        self.assertTrue(receivedQuery)
+        self.assertTrue(receivedResponse)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(receivedResponse, response)
+
+        eco = cookiesoption.CookiesOption(b'badc0fee', b'badc0fee')
+        query = dns.message.make_query(name, 'AAAA', 'IN', use_edns=True, payload=4096, options=[eco])
+        # second query should be served from the cache
+        (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False)
+        receivedResponse.id = response.id
+        self.assertEquals(receivedResponse, response)
+
+    def testCacheCookies(self):
+        """
+        Cache: A query with a cookie should not match one without any cookie
+        """
+        ttl = 600
+        name = 'cache-cookie.cache.tests.powerdns.com.'
+        eco = cookiesoption.CookiesOption(b'deadbeef', b'deadbeef')
+        query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096, options=[eco])
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    ttl,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.A,
+                                    '192.0.2.1')
+        response.answer.append(rrset)
+
+        # first query to fill the cache
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+        self.assertTrue(receivedQuery)
+        self.assertTrue(receivedResponse)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(receivedResponse, response)
+
+        query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096, options=[])
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    ttl,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.A,
+                                    '127.0.0.1')
+        response.answer.append(rrset)
+        # second query should NOT be served from the cache
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+        self.assertTrue(receivedQuery)
+        self.assertTrue(receivedResponse)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(receivedResponse, response)
+
+    def testCacheSameCookieDifferentECS(self):
+        """
+        Cache: The content of cookies should be ignored by the cache but not the ECS one
+        """
+        ttl = 600
+        name = 'cache-different-cookies-different-ecs.cache.tests.powerdns.com.'
+        eco = cookiesoption.CookiesOption(b'deadbeef', b'deadbeef')
+        ecso = clientsubnetoption.ClientSubnetOption('192.0.2.1', 32)
+        query = dns.message.make_query(name, 'AAAA', 'IN', use_edns=True, payload=4096, options=[eco,ecso])
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    ttl,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.AAAA,
+                                    '::1')
+        response.answer.append(rrset)
+
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+        self.assertTrue(receivedQuery)
+        self.assertTrue(receivedResponse)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(receivedResponse, response)
+
+        eco = cookiesoption.CookiesOption(b'deadbeef', b'deadbeef')
+        ecso = clientsubnetoption.ClientSubnetOption('192.0.2.2', 32)
+        query = dns.message.make_query(name, 'AAAA', 'IN', use_edns=True, payload=4096, options=[eco,ecso])
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    ttl,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.AAAA,
+                                    '::1')
+        response.answer.append(rrset)
+
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+        self.assertTrue(receivedQuery)
+        self.assertTrue(receivedResponse)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(receivedResponse, response)
+
+class TestCachingHashingCookies(DNSDistTest):
+
+    _config_template = """
+    pc = newPacketCache(100, {maxTTL=86400, minTTL=1, cookieHashing=true})
+    getPool(""):setCache(pc)
+    newServer{address="127.0.0.1:%d"}
+    """
+
+    def testCached(self):
+        """
+        Cache: Served from cache
+
+        dnsdist is configured to cache entries, we are sending several
+        identical requests and checking that the backend only receive
+        the first one.
+        """
+        numberOfQueries = 10
+        name = 'cached.cache.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'AAAA', 'IN')
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    3600,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.AAAA,
+                                    '::1')
+        response.answer.append(rrset)
+
+        # first query to fill the cache
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+        self.assertTrue(receivedQuery)
+        self.assertTrue(receivedResponse)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(receivedResponse, response)
+
+        for _ in range(numberOfQueries):
+            (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False)
+            self.assertEquals(receivedResponse, response)
+
+        total = 0
+        for key in self._responsesCounter:
+            total += self._responsesCounter[key]
+            TestCaching._responsesCounter[key] = 0
+
+        self.assertEquals(total, 1)
+
+        # TCP should not be cached
+        # first query to fill the cache
+        (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
+        self.assertTrue(receivedQuery)
+        self.assertTrue(receivedResponse)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(receivedResponse, response)
+
+        for _ in range(numberOfQueries):
+            (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False)
+            self.assertEquals(receivedResponse, response)
+
+        total = 0
+        for key in self._responsesCounter:
+            total += self._responsesCounter[key]
+            TestCaching._responsesCounter[key] = 0
+
+        self.assertEquals(total, 1)
+
+
+    def testCacheDifferentCookies(self):
+        """
+        Cache: The content of cookies should NOT be ignored by the cache (cookieHashing is set)
+        """
+        ttl = 600
+        name = 'cache-different-cookies.cache-cookie-hashing.tests.powerdns.com.'
+        eco = cookiesoption.CookiesOption(b'deadbeef', b'deadbeef')
+        query = dns.message.make_query(name, 'AAAA', 'IN', use_edns=True, payload=4096, options=[eco])
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    ttl,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.AAAA,
+                                    '::1')
+        response.answer.append(rrset)
+
+        # first query to fill the cache
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+        self.assertTrue(receivedQuery)
+        self.assertTrue(receivedResponse)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(receivedResponse, response)
+
+        eco = cookiesoption.CookiesOption(b'badc0fee', b'badc0fee')
+        query = dns.message.make_query(name, 'AAAA', 'IN', use_edns=True, payload=4096, options=[eco])
+        differentResponse = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    ttl,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.AAAA,
+                                    '2001:DB8::1')
+        differentResponse.answer.append(rrset)
+        # second query should NOT be served from the cache
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, differentResponse)
+        self.assertTrue(receivedQuery)
+        self.assertTrue(receivedResponse)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(receivedResponse, differentResponse)
+        self.assertNotEquals(receivedResponse, response)
+
+    def testCacheCookies(self):
+        """
+        Cache: A query with a cookie should not match one without any cookie (cookieHashing=true)
+        """
+        ttl = 600
+        name = 'cache-cookie.cache-cookie-hashing.tests.powerdns.com.'
+        eco = cookiesoption.CookiesOption(b'deadbeef', b'deadbeef')
+        query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096, options=[eco])
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    ttl,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.A,
+                                    '192.0.2.1')
+        response.answer.append(rrset)
+
+        # first query to fill the cache
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+        self.assertTrue(receivedQuery)
+        self.assertTrue(receivedResponse)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(receivedResponse, response)
+
+        query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096, options=[])
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    ttl,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.A,
+                                    '127.0.0.1')
+        response.answer.append(rrset)
+        # second query should NOT be served from the cache
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+        self.assertTrue(receivedQuery)
+        self.assertTrue(receivedResponse)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(receivedResponse, response)
+
+    def testCacheSameCookieDifferentECS(self):
+        """
+        Cache: The content of cookies should NOT be ignored by the cache (cookieHashing=true), even with ECS there
+        """
+        ttl = 600
+        name = 'cache-different-cookies-different-ecs.cache-cookie-hashing.tests.powerdns.com.'
+        eco = cookiesoption.CookiesOption(b'deadbeef', b'deadbeef')
+        ecso = clientsubnetoption.ClientSubnetOption('192.0.2.1', 32)
+        query = dns.message.make_query(name, 'AAAA', 'IN', use_edns=True, payload=4096, options=[eco,ecso])
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    ttl,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.AAAA,
+                                    '::1')
+        response.answer.append(rrset)
+
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+        self.assertTrue(receivedQuery)
+        self.assertTrue(receivedResponse)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(receivedResponse, response)
+
+        eco = cookiesoption.CookiesOption(b'deadbeef', b'deadbeef')
+        ecso = clientsubnetoption.ClientSubnetOption('192.0.2.2', 32)
+        query = dns.message.make_query(name, 'AAAA', 'IN', use_edns=True, payload=4096, options=[eco,ecso])
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    ttl,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.AAAA,
+                                    '::1')
+        response.answer.append(rrset)
+
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+        self.assertTrue(receivedQuery)
+        self.assertTrue(receivedResponse)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(receivedResponse, response)
+
 class TestTempFailureCacheTTLAction(DNSDistTest):
 
     _config_template = """
@@ -1863,7 +2168,7 @@ class TestCachingCollisionNoECSParsing(DNSDistTest):
         Cache: Collision with no ECS parsing
         """
         name = 'collision-no-ecs-parsing.cache.tests.powerdns.com.'
-        ecso = clientsubnetoption.ClientSubnetOption('10.0.188.3', 32)
+        ecso = clientsubnetoption.ClientSubnetOption('10.0.226.63', 32)
         query = dns.message.make_query(name, 'AAAA', 'IN', use_edns=True, options=[ecso], payload=512)
         query.flags = dns.flags.RD
         response = dns.message.make_response(query)
@@ -1885,7 +2190,7 @@ class TestCachingCollisionNoECSParsing(DNSDistTest):
         # second query will hash to the same key, triggering a collision which
         # will not be detected because the qname, qtype, qclass and flags will
         # match and EDNS Client Subnet parsing has not been enabled
-        ecso2 = clientsubnetoption.ClientSubnetOption('10.0.192.138', 32)
+        ecso2 = clientsubnetoption.ClientSubnetOption('10.1.60.19', 32)
         query2 = dns.message.make_query(name, 'AAAA', 'IN', use_edns=True, options=[ecso2], payload=512)
         query2.flags = dns.flags.RD
         (_, receivedResponse) = self.sendUDPQuery(query2, response=None, useQueue=False)
@@ -1905,7 +2210,7 @@ class TestCachingCollisionWithECSParsing(DNSDistTest):
         Cache: Collision with ECS parsing
         """
         name = 'collision-with-ecs-parsing.cache.tests.powerdns.com.'
-        ecso = clientsubnetoption.ClientSubnetOption('10.0.115.61', 32)
+        ecso = clientsubnetoption.ClientSubnetOption('10.0.150.206', 32)
         query = dns.message.make_query(name, 'AAAA', 'IN', use_edns=True, options=[ecso], payload=512)
         query.flags = dns.flags.RD
         response = dns.message.make_response(query)
@@ -1927,7 +2232,7 @@ class TestCachingCollisionWithECSParsing(DNSDistTest):
         # second query will hash to the same key, triggering a collision which
         # _will_ be detected this time because the qname, qtype, qclass and flags will
         # match but EDNS Client Subnet parsing is now enabled and will detect the issue
-        ecso2 = clientsubnetoption.ClientSubnetOption('10.0.143.21', 32)
+        ecso2 = clientsubnetoption.ClientSubnetOption('10.0.212.51', 32)
         query2 = dns.message.make_query(name, 'AAAA', 'IN', use_edns=True, options=[ecso2], payload=512)
         query2.flags = dns.flags.RD
         response2 = dns.message.make_response(query2)
diff --git a/regression-tests.recursor-dnssec/test_PacketCache.py b/regression-tests.recursor-dnssec/test_PacketCache.py
new file mode 100644 (file)
index 0000000..b658e4c
--- /dev/null
@@ -0,0 +1,144 @@
+import clientsubnetoption
+import cookiesoption
+import dns
+import os
+import requests
+
+from recursortests import RecursorTest
+
+class PacketCacheRecursorTest(RecursorTest):
+
+    _confdir = 'PacketCache'
+    _wsPort = 8042
+    _wsTimeout = 2
+    _wsPassword = 'secretpassword'
+    _apiKey = 'secretapikey'
+    _config_template = """
+    packetcache-ttl=60
+    auth-zones=example=configs/%s/example.zone
+    webserver=yes
+    webserver-port=%d
+    webserver-address=127.0.0.1
+    webserver-password=%s
+    api-key=%s
+    """ % (_confdir, _wsPort, _wsPassword, _apiKey)
+
+    @classmethod
+    def generateRecursorConfig(cls, confdir):
+        authzonepath = os.path.join(confdir, 'example.zone')
+        with open(authzonepath, 'w') as authzone:
+            authzone.write("""$ORIGIN example.
+@ 3600 IN SOA {soa}
+a 3600 IN A 192.0.2.42
+b 3600 IN A 192.0.2.42
+c 3600 IN A 192.0.2.42
+d 3600 IN A 192.0.2.42
+e 3600 IN A 192.0.2.42
+""".format(soa=cls._SOA))
+        super(PacketCacheRecursorTest, cls).generateRecursorConfig(confdir)
+
+    @classmethod
+    def setUpClass(cls):
+
+        # we don't need all the auth stuff
+        cls.setUpSockets()
+        cls.startResponders()
+
+        confdir = os.path.join('configs', cls._confdir)
+        cls.createConfigDir(confdir)
+
+        cls.generateRecursorConfig(confdir)
+        cls.startRecursor(confdir, cls._recursorPort)
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.tearDownRecursor()
+
+    def checkPacketCacheMetrics(self, expectedHits, expectedMisses):
+        headers = {'x-api-key': self._apiKey}
+        url = 'http://127.0.0.1:' + str(self._wsPort) + '/api/v1/servers/localhost/statistics'
+        r = requests.get(url, headers=headers, timeout=self._wsTimeout)
+        self.assertTrue(r)
+        self.assertEquals(r.status_code, 200)
+        self.assertTrue(r.json())
+        content = r.json()
+        foundHits = False
+        foundMisses = True
+        for entry in content:
+            if entry['name'] == 'packetcache-hits':
+                foundHits = True
+                self.assertEquals(int(entry['value']), expectedHits)
+            elif entry['name'] == 'packetcache-misses':
+                foundMisses = True
+                self.assertEquals(int(entry['value']), expectedMisses)
+
+        self.assertTrue(foundHits)
+        self.assertTrue(foundMisses)
+
+    def testPacketCache(self):
+        # first query, no cookie
+        qname = 'a.example.'
+        query = dns.message.make_query(qname, 'A', want_dnssec=True)
+        expected = dns.rrset.from_text(qname, 0, dns.rdataclass.IN, 'A', '192.0.2.42')
+
+        for method in ("sendUDPQuery", "sendTCPQuery"):
+            sender = getattr(self, method)
+            res = sender(query)
+            self.assertRcodeEqual(res, dns.rcode.NOERROR)
+            self.assertRRsetInAnswer(res, expected)
+
+        self.checkPacketCacheMetrics(0, 1)
+
+        # we should get a hit over UDP this time
+        res = self.sendUDPQuery(query)
+        self.assertRcodeEqual(res, dns.rcode.NOERROR)
+        self.assertRRsetInAnswer(res, expected)
+        self.checkPacketCacheMetrics(1, 1)
+
+        eco1 = cookiesoption.CookiesOption(b'deadbeef', b'deadbeef')
+        eco2 = cookiesoption.CookiesOption(b'deadc0de', b'deadc0de')
+        ecso1 = clientsubnetoption.ClientSubnetOption('192.0.2.1', 32)
+        ecso2 = clientsubnetoption.ClientSubnetOption('192.0.2.2', 32)
+
+        # we add a cookie, should not match anymore
+        query = dns.message.make_query(qname, 'A', want_dnssec=True, options=[eco1])
+        res = self.sendUDPQuery(query)
+        self.assertRcodeEqual(res, dns.rcode.NOERROR)
+        self.assertRRsetInAnswer(res, expected)
+        self.checkPacketCacheMetrics(1, 2)
+
+        # same cookie, should match
+        query = dns.message.make_query(qname, 'A', want_dnssec=True, options=[eco1])
+        res = self.sendUDPQuery(query)
+        self.assertRcodeEqual(res, dns.rcode.NOERROR)
+        self.assertRRsetInAnswer(res, expected)
+        self.checkPacketCacheMetrics(2, 2)
+
+        # different cookie, should still match
+        query = dns.message.make_query(qname, 'A', want_dnssec=True, options=[eco2])
+        res = self.sendUDPQuery(query)
+        self.assertRcodeEqual(res, dns.rcode.NOERROR)
+        self.assertRRsetInAnswer(res, expected)
+        self.checkPacketCacheMetrics(3, 2)
+
+        # first cookie but with an ECS option, should not match
+        query = dns.message.make_query(qname, 'A', want_dnssec=True, options=[eco1, ecso1])
+        res = self.sendUDPQuery(query)
+        self.assertRcodeEqual(res, dns.rcode.NOERROR)
+        self.assertRRsetInAnswer(res, expected)
+        self.checkPacketCacheMetrics(3, 3)
+
+        # different cookie but same ECS option, should match
+        query = dns.message.make_query(qname, 'A', want_dnssec=True, options=[eco2, ecso1])
+        res = self.sendUDPQuery(query)
+        self.assertRcodeEqual(res, dns.rcode.NOERROR)
+        self.assertRRsetInAnswer(res, expected)
+        self.checkPacketCacheMetrics(4, 3)
+
+        # first cookie but different ECS option, should still match (we ignore EDNS Client Subnet
+        # in the recursor's packet cache, but ECS-specific responses are not cached
+        query = dns.message.make_query(qname, 'A', want_dnssec=True, options=[eco1, ecso2])
+        res = self.sendUDPQuery(query)
+        self.assertRcodeEqual(res, dns.rcode.NOERROR)
+        self.assertRRsetInAnswer(res, expected)
+        self.checkPacketCacheMetrics(5, 3)