From: Remi Gacogne Date: Thu, 2 Apr 2020 12:33:01 +0000 (+0200) Subject: Skip EDNS Cookies in the packet cache X-Git-Tag: rec-4.5.0-alpha0~3^2~2 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=fa980c5912024c69c620363763f0d87db59eb8cd;p=thirdparty%2Fpdns.git Skip EDNS Cookies in the packet cache --- diff --git a/pdns/Makefile.am b/pdns/Makefile.am index 71a3fe86c9..c3b33f5aa6 100644 --- a/pdns/Makefile.am +++ b/pdns/Makefile.am @@ -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 \ diff --git a/pdns/auth-packetcache.cc b/pdns/auth-packetcache.cc index 1cb554f44e..4ed8112fbd 100644 --- a/pdns/auth-packetcache.cc +++ b/pdns/auth-packetcache.cc @@ -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::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 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) diff --git a/pdns/dnsdist-cache.cc b/pdns/dnsdist-cache.cc index 9990f5abab..fa38fbb9dd 100644 --- a/pdns/dnsdist-cache.cc +++ b/pdns/dnsdist-cache.cc @@ -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(dq.dh), dq.len, dq.tcp); - if (keyOut) + if (keyOut) { *keyOut = key; + } if (d_parseECS) { getClientSubnet(reinterpret_cast(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(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; diff --git a/pdns/dnsdist-cache.hh b/pdns/dnsdist-cache.hh index 79200c4b39..89ccbfec91 100644 --- a/pdns/dnsdist-cache.hh +++ b/pdns/dnsdist-cache.hh @@ -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& subnet); private: @@ -133,4 +145,5 @@ private: bool d_deferrableInsertLock; bool d_parseECS; bool d_keepStaleData{false}; + bool d_cookieHashing{false}; }; diff --git a/pdns/dnsdistdist/Makefile.am b/pdns/dnsdistdist/Makefile.am index b0ca67ad42..c21e3628ef 100644 --- a/pdns/dnsdistdist/Makefile.am +++ b/pdns/dnsdistdist/Makefile.am @@ -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 \ diff --git a/pdns/dnsdistdist/dnsdist-lua-bindings-packetcache.cc b/pdns/dnsdistdist/dnsdist-lua-bindings-packetcache.cc index b6d687a759..4dc93e25e4 100644 --- a/pdns/dnsdistdist/dnsdist-lua-bindings-packetcache.cc +++ b/pdns/dnsdistdist/dnsdist-lua-bindings-packetcache.cc @@ -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((*vars)["temporaryFailureTTL"]); } + + if (vars->count("cookieHashing")) { + cookieHashing = boost::get((*vars)["cookieHashing"]); + } } auto res = std::make_shared(maxEntries, maxTTL, minTTL, tempFailTTL, maxNegativeTTL, staleTTL, dontAge, numberOfShards, deferrableInsertLock, ecsParsing); res->setKeepStaleData(keepStaleData); + res->setCookieHashing(cookieHashing); return res; }); diff --git a/pdns/dnsdistdist/docs/reference/config.rst b/pdns/dnsdistdist/docs/reference/config.rst index 7c3667e790..73114563da 100644 --- a/pdns/dnsdistdist/docs/reference/config.rst +++ b/pdns/dnsdistdist/docs/reference/config.rst @@ -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 diff --git a/pdns/dnsdistdist/docs/upgrade_guide.rst b/pdns/dnsdistdist/docs/upgrade_guide.rst index 0acfcddbff..7621691bb4 100644 --- a/pdns/dnsdistdist/docs/upgrade_guide.rst +++ b/pdns/dnsdistdist/docs/upgrade_guide.rst @@ -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 index 0000000000..b50f901511 --- /dev/null +++ b/pdns/dnsdistdist/packetcache.hh @@ -0,0 +1 @@ +../packetcache.hh \ No newline at end of file diff --git a/pdns/ednsoptions.cc b/pdns/ednsoptions.cc index d20755be73..b10e150aa0 100644 --- a/pdns/ednsoptions.cc +++ b/pdns/ednsoptions.cc @@ -23,6 +23,23 @@ #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(data[pos]) * 256) + static_cast(data[pos + 1]); + pos += EDNS_OPTION_CODE_SIZE; + + optionLen = (static_cast(data[pos]) * 256) + static_cast(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); diff --git a/pdns/ednsoptions.hh b/pdns/ednsoptions.hh index cbd7b0d0a0..a8f0a87903 100644 --- a/pdns/ednsoptions.hh +++ b/pdns/ednsoptions.hh @@ -47,5 +47,7 @@ typedef std::map 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>& 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); diff --git a/pdns/fuzz_dnsdistcache.cc b/pdns/fuzz_dnsdistcache.cc index b34a0c3e6e..79812ae4c7 100644 --- a/pdns/fuzz_dnsdistcache.cc +++ b/pdns/fuzz_dnsdistcache.cc @@ -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(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 subnet; DNSDistPacketCache::getClientSubnet(reinterpret_cast(data), consumed, size, subnet); } diff --git a/pdns/fuzz_packetcache.cc b/pdns/fuzz_packetcache.cc index 1658b18f55..c53306c744 100644 --- a/pdns/fuzz_packetcache.cc +++ b/pdns/fuzz_packetcache.cc @@ -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 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 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) { } diff --git a/pdns/packetcache.hh b/pdns/packetcache.hh index 7dd0c8828e..9baa8b3dd6 100644 --- a/pdns/packetcache.hh +++ b/pdns/packetcache.hh @@ -23,80 +23,137 @@ #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(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(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(reinterpret_cast(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(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(&packet.at(pos)), packetSize - pos, currentHash); } + return currentHash; } - if (skipBegin > p) { - ret = burtle(reinterpret_cast(p), skipBegin-p, ret); + + currentHash = burtle(reinterpret_cast(&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(packet.at(pos)) * 256) + static_cast(packet.at(pos + 1))); + /* skip the rd length */ + /* already hashed above */ + pos += 2; + + if (rdLen > (packetSize - pos)) { + if (pos < packetSize) { + currentHash = burtle(reinterpret_cast(&packet.at(pos)), packetSize - pos, currentHash); + } + return currentHash; } - if (skipEnd < end) { - ret = burtle(reinterpret_cast(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(&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(&packet.at(pos)), 4 + optionLen, currentHash); + } + else { + /* hash the option code and length */ + currentHash = burtle(reinterpret_cast(&packet.at(pos)), 4, currentHash); + } + + pos += 4 + optionLen; + rdataRead += 4 + optionLen; + } + + if (pos < packetSize) { + currentHash = burtle(reinterpret_cast(&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(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(&packet.at(2)), sizeof(dnsheader) - 2, currentHash); // rest of dnsheader, skip id + pos = sizeof(dnsheader); + + for (; pos < packetSize; ) { + const unsigned char labelLen = static_cast(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(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& 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(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(query.at(pos)) * 256) + static_cast(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; } }; diff --git a/pdns/pdns_recursor.cc b/pdns/pdns_recursor.cc index 71a61f517b..8f6b796d63 100644 --- a/pdns/pdns_recursor.cc +++ b/pdns/pdns_recursor.cc @@ -344,8 +344,6 @@ struct DNSComboWriter { unsigned int d_tag{0}; uint32_t d_qhash{0}; uint32_t d_ttlCap{std::numeric_limits::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: "< records; boost::optional rcode = boost::none; uint32_t ttlCap = std::numeric_limits::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 > >(new boost::circular_buffer >()); t_bogusqueryring->set_capacity(ringsize); } - MT=std::unique_ptr >(new MTasker(::arg().asNum("stack-size"))); threadInfo.mt = MT.get(); diff --git a/pdns/recpacketcache.cc b/pdns/recpacketcache.cc index f4d145fffc..0f93c5d317 100644 --- a/pdns/recpacketcache.cc +++ b/pdns/recpacketcache.cc @@ -39,25 +39,22 @@ int RecursorPacketCache::doWipePacketCache(const DNSName& name, uint16_t qtype, return count; } -bool RecursorPacketCache::qrMatch(const packetCache_t::index::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::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 optionsToSkip{ EDNSOptionCode::COOKIE, EDNSOptionCode::ECS }; + return queryMatches(iter->d_query, queryPacket, qname, optionsToSkip); } -bool RecursorPacketCache::checkResponseMatches(std::pair::type::iterator, packetCache_t::index::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::type::iterator, packetCache_t::index::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(); 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(); 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&& 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&& protobufMessage) { auto& idx = d_packetCache.get(); auto range = idx.equal_range(tie(tag,qhash)); @@ -164,8 +156,6 @@ void RecursorPacketCache::insertResponsePacket(unsigned int tag, uint32_t qhash, moveCacheItemToBack(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; diff --git a/pdns/recpacketcache.hh b/pdns/recpacketcache.hh index 75adce7d2a..8ffc61ba83 100644 --- a/pdns/recpacketcache.hh +++ b/pdns/recpacketcache.hh @@ -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&& 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&& 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::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::type::iterator, packetCache_t::index::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::type::iterator& iter, const std::string& queryPacket, const DNSName& qname, uint16_t qtype, uint16_t qclass); + bool checkResponseMatches(std::pair::type::iterator, packetCache_t::index::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) diff --git a/pdns/recursordist/Makefile.am b/pdns/recursordist/Makefile.am index 573047c75e..4bba5b5f5f 100644 --- a/pdns/recursordist/Makefile.am +++ b/pdns/recursordist/Makefile.am @@ -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 index 0000000000..2213b7d900 --- /dev/null +++ b/pdns/recursordist/views.hh @@ -0,0 +1 @@ +../views.hh \ No newline at end of file diff --git a/pdns/test-dnsdistpacketcache_cc.cc b/pdns/test-dnsdistpacketcache_cc.cc index becd1ec2e7..3ea30998c1 100644 --- a/pdns/test-dnsdistpacketcache_cc.cc +++ b/pdns/test-dnsdistpacketcache_cc.cc @@ -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 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 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 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 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 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 "<canHashPacket(q.getString())); + q.setHash(g_PC->canHashPacket(q.getString(), false)); const unsigned int maxTTL = 3600; g_PC->insert(q, r, maxTTL); diff --git a/pdns/test-packetcache_hh.cc b/pdns/test-packetcache_hh.cc index 3ce89b79d8..6a55effcff 100644 --- a/pdns/test-packetcache_hh.cc +++ b/pdns/test-packetcache_hh.cc @@ -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 optionsToSkip{ EDNSOptionCode::COOKIE }; + static const std::unordered_set 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 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 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(secondQuery.data()), secondQuery.size()), false); + auto pair = colMap.insert(std::make_pair(secondKey, opt.source)); + total++; + if (!pair.second) { + collisions++; + cerr<<"Collision between "<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 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 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(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(secondQuery.data()), secondQuery.size()), false); + + total++; + if (colMap.count(secondKey)) { + collisions++; + cerr<<"Collision between "< 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 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 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)); } } diff --git a/pdns/test-recpacketcache_cc.cc b/pdns/test-recpacketcache_cc.cc index b8038dc56e..fce6fcc823 100644 --- a/pdns/test-recpacketcache_cc.cc +++ b/pdns/test-recpacketcache_cc.cc @@ -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 index 0000000000..7e56c1590d --- /dev/null +++ b/pdns/views.hh @@ -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 +#if BOOST_VERSION >= 106100 +#include +using boost::string_view; +#else +#include +using string_view = boost::string_ref; +#endif +#endif diff --git a/regression-tests.dnsdist/test_Caching.py b/regression-tests.dnsdist/test_Caching.py index ccd536e3bd..d3f91753c2 100644 --- a/regression-tests.dnsdist/test_Caching.py +++ b/regression-tests.dnsdist/test_Caching.py @@ -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 index 0000000000..b658e4c24d --- /dev/null +++ b/regression-tests.recursor-dnssec/test_PacketCache.py @@ -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)