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