From: Remi Gacogne Date: Fri, 24 Nov 2023 14:48:24 +0000 (+0100) Subject: dnsdist: Make the max size of entries in the packet cache configurable X-Git-Tag: dnsdist-1.9.0-alpha4~23^2~3 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=7b71c89bf83ac4952e268a8790f1543a345462ec;p=thirdparty%2Fpdns.git dnsdist: Make the max size of entries in the packet cache configurable It used to be set to 4096 bytes, which is also a hard limit for UDP responses anyway, because of the internal buffer size, but the limit can now be raised for responses received over TCP (including DoT and DoH). --- diff --git a/pdns/dnsdist-cache.cc b/pdns/dnsdist-cache.cc index 67de6226a0..c3b0e75ef6 100644 --- a/pdns/dnsdist-cache.cc +++ b/pdns/dnsdist-cache.cc @@ -119,9 +119,10 @@ void DNSDistPacketCache::insertLocked(CacheShard& shard, std::unordered_map& subnet, uint16_t queryFlags, bool dnssecOK, const DNSName& qname, uint16_t qtype, uint16_t qclass, const PacketBuffer& response, bool receivedOverUDP, uint8_t rcode, boost::optional tempFailureTTL) { - if (response.size() < sizeof(dnsheader)) { + if (response.size() < sizeof(dnsheader) || response.size() > getMaximumEntrySize()) { return; } + if (qtype == QType::AXFR || qtype == QType::IXFR) { return; } @@ -620,3 +621,8 @@ std::set DNSDistPacketCache::getRecordsForDomain(const DNSName& do return addresses; } + +void DNSDistPacketCache::setMaximumEntrySize(size_t maxSize) +{ + d_maximumEntrySize = maxSize; +} diff --git a/pdns/dnsdist-cache.hh b/pdns/dnsdist-cache.hh index 5b2ba2c78d..95667bd4cd 100644 --- a/pdns/dnsdist-cache.hh +++ b/pdns/dnsdist-cache.hh @@ -75,12 +75,14 @@ public: d_keepStaleData = keep; } - void setECSParsingEnabled(bool enabled) { d_parseECS = enabled; } + void setMaximumEntrySize(size_t maxSize); + size_t getMaximumEntrySize() const { return d_maximumEntrySize; } + uint32_t getKey(const DNSName::string_t& qname, size_t qnameWireLength, const PacketBuffer& packet, bool receivedOverUDP); static uint32_t getMinTTL(const char* packet, uint16_t length, bool* seenNoDataSOA); @@ -139,15 +141,16 @@ private: pdns::stat_t d_ttlTooShorts{0}; pdns::stat_t d_cleanupCount{0}; - size_t d_maxEntries; - 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; - bool d_deferrableInsertLock; + const size_t d_maxEntries; + size_t d_maximumEntrySize{4096}; + const uint32_t d_shardCount; + const uint32_t d_maxTTL; + const uint32_t d_tempFailureTTL; + const uint32_t d_maxNegativeTTL; + const uint32_t d_minTTL; + const uint32_t d_staleTTL; + const bool d_dontAge; + const bool d_deferrableInsertLock; bool d_parseECS; bool d_keepStaleData{false}; }; diff --git a/pdns/dnsdist-tcp.cc b/pdns/dnsdist-tcp.cc index 9ef56c62c1..53cf163976 100644 --- a/pdns/dnsdist-tcp.cc +++ b/pdns/dnsdist-tcp.cc @@ -1015,9 +1015,7 @@ void IncomingTCPConnectionState::handleIO() return; } - /* allocate a bit more memory to be able to spoof the content, get an answer from the cache - or to add ECS without allocating a new buffer */ - d_buffer.resize(std::max(d_querySize + static_cast(512), s_maxPacketCacheEntrySize)); + d_buffer.resize(d_querySize); d_currentPos = 0; } else { diff --git a/pdns/dnsdist.cc b/pdns/dnsdist.cc index b5f8cc0f3f..dd4c96d33b 100644 --- a/pdns/dnsdist.cc +++ b/pdns/dnsdist.cc @@ -153,7 +153,9 @@ uint32_t g_socketUDPRecvBuffer{0}; std::set g_capabilitiesToRetain; -static size_t const s_initialUDPPacketBufferSize = s_maxPacketCacheEntrySize + DNSCRYPT_MAX_RESPONSE_PADDING_AND_MAC_SIZE; +// we are not willing to receive a bigger UDP response than that, no matter what +static constexpr size_t s_maxUDPResponsePacketSize{4096U}; +static size_t const s_initialUDPPacketBufferSize = s_maxUDPResponsePacketSize + DNSCRYPT_MAX_RESPONSE_PADDING_AND_MAC_SIZE; static_assert(s_initialUDPPacketBufferSize <= UINT16_MAX, "Packet size should fit in a uint16_t"); static ssize_t sendfromto(int sock, const void* data, size_t len, int flags, const ComboAddress& from, const ComboAddress& to) @@ -550,7 +552,7 @@ bool processResponseAfterRules(PacketBuffer& response, const std::vector g_capabilitiesToRetain; static const uint16_t s_udpIncomingBufferSize{1500}; // don't accept UDP queries larger than this value -static const size_t s_maxPacketCacheEntrySize{4096}; // don't cache responses larger than this value enum class ProcessQueryResult : uint8_t { Drop, SendAnswer, PassToBackend, Asynchronous }; ProcessQueryResult processQuery(DNSQuestion& dq, LocalHolders& holders, std::shared_ptr& selectedBackend); diff --git a/pdns/dnsdistdist/dnsdist-lua-bindings-packetcache.cc b/pdns/dnsdistdist/dnsdist-lua-bindings-packetcache.cc index fd62eb5318..f71bf37516 100644 --- a/pdns/dnsdistdist/dnsdist-lua-bindings-packetcache.cc +++ b/pdns/dnsdistdist/dnsdist-lua-bindings-packetcache.cc @@ -41,6 +41,7 @@ void setupLuaBindingsPacketCache(LuaContext& luaCtx, bool client) size_t maxNegativeTTL = 3600; size_t staleTTL = 60; size_t numberOfShards = 20; + size_t maxEntrySize{0}; bool dontAge = false; bool deferrableInsertLock = true; bool ecsParsing = false; @@ -59,6 +60,7 @@ void setupLuaBindingsPacketCache(LuaContext& luaCtx, bool client) getOptionalValue(vars, "staleTTL", staleTTL); getOptionalValue(vars, "temporaryFailureTTL", tempFailTTL); getOptionalValue(vars, "cookieHashing", cookieHashing); + getOptionalValue(vars, "maximumEntrySize", maxEntrySize); if (getOptionalValue(vars, "skipOptions", skipOptions) > 0) { for (const auto& option : skipOptions) { @@ -87,6 +89,9 @@ void setupLuaBindingsPacketCache(LuaContext& luaCtx, bool client) res->setKeepStaleData(keepStaleData); res->setSkippedOptions(optionsToSkip); + if (maxEntrySize >= sizeof(dnsheader)) { + res->setMaximumEntrySize(maxEntrySize); + } return res; }); diff --git a/pdns/dnsdistdist/dnsdist-tcp-downstream.hh b/pdns/dnsdistdist/dnsdist-tcp-downstream.hh index 81c87570b5..a165dc18cb 100644 --- a/pdns/dnsdistdist/dnsdist-tcp-downstream.hh +++ b/pdns/dnsdistdist/dnsdist-tcp-downstream.hh @@ -226,7 +226,7 @@ protected: class TCPConnectionToBackend : public ConnectionToBackend { public: - TCPConnectionToBackend(const std::shared_ptr& ds, std::unique_ptr& mplexer, const struct timeval& now, std::string&& /* proxyProtocolPayload*, unused but there to match the HTTP2 connections, so we can use the same templated connections manager class */): ConnectionToBackend(ds, mplexer, now), d_responseBuffer(s_maxPacketCacheEntrySize) + TCPConnectionToBackend(const std::shared_ptr& ds, std::unique_ptr& mplexer, const struct timeval& now, std::string&& /* proxyProtocolPayload*, unused but there to match the HTTP2 connections, so we can use the same templated connections manager class */): ConnectionToBackend(ds, mplexer, now), d_responseBuffer(512) { } diff --git a/pdns/dnsdistdist/dnsdist-tcp-upstream.hh b/pdns/dnsdistdist/dnsdist-tcp-upstream.hh index 4c8c473238..2c081fd27e 100644 --- a/pdns/dnsdistdist/dnsdist-tcp-upstream.hh +++ b/pdns/dnsdistdist/dnsdist-tcp-upstream.hh @@ -30,7 +30,7 @@ public: enum class QueryProcessingResult : uint8_t { Forwarded, TooSmall, InvalidHeaders, Dropped, SelfAnswered, NoBackend, Asynchronous }; enum class ProxyProtocolResult : uint8_t { Reading, Done, Error }; - IncomingTCPConnectionState(ConnectionInfo&& ci, TCPClientThreadData& threadData, const struct timeval& now): d_buffer(s_maxPacketCacheEntrySize), d_ci(std::move(ci)), d_handler(d_ci.fd, timeval{g_tcpRecvTimeout,0}, d_ci.cs->tlsFrontend ? d_ci.cs->tlsFrontend->getContext() : (d_ci.cs->dohFrontend ? d_ci.cs->dohFrontend->d_tlsContext.getContext() : nullptr), now.tv_sec), d_connectionStartTime(now), d_ioState(make_unique(*threadData.mplexer, d_ci.fd)), d_threadData(threadData), d_creatorThreadID(std::this_thread::get_id()) + IncomingTCPConnectionState(ConnectionInfo&& ci, TCPClientThreadData& threadData, const struct timeval& now): d_buffer(sizeof(uint16_t)), d_ci(std::move(ci)), d_handler(d_ci.fd, timeval{g_tcpRecvTimeout,0}, d_ci.cs->tlsFrontend ? d_ci.cs->tlsFrontend->getContext() : (d_ci.cs->dohFrontend ? d_ci.cs->dohFrontend->d_tlsContext.getContext() : nullptr), now.tv_sec), d_connectionStartTime(now), d_ioState(make_unique(*threadData.mplexer, d_ci.fd)), d_threadData(threadData), d_creatorThreadID(std::this_thread::get_id()) { d_origDest.reset(); d_origDest.sin4.sin_family = d_ci.remote.sin4.sin_family; diff --git a/pdns/dnsdistdist/docs/reference/config.rst b/pdns/dnsdistdist/docs/reference/config.rst index 44f69a0896..2d4f959270 100644 --- a/pdns/dnsdistdist/docs/reference/config.rst +++ b/pdns/dnsdistdist/docs/reference/config.rst @@ -916,6 +916,9 @@ See :doc:`../guides/cache` for a how to. .. versionchanged:: 1.7.0 ``skipOptions`` parameter added. + .. versionchanged:: 1.9.0 + ``maximumEntrySize`` parameter added. + Creates a new :class:`PacketCache` with the settings specified. :param int maxEntries: The maximum number of entries in this cache @@ -934,6 +937,7 @@ See :doc:`../guides/cache` for a how to. * ``temporaryFailureTTL=60``: int - On a SERVFAIL or REFUSED from the backend, cache for this amount of seconds.. * ``cookieHashing=false``: bool - If true, 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. * ``skipOptions={}``: Extra list of EDNS option codes to skip when hashing the packet (if ``cookieHashing`` above is false, EDNS cookie option number will be added to this list internally). + * ``maximumEntrySize=4096``: int - The maximum size, in bytes, of a DNS packet that can be inserted into the packet cache. Default is 4096 bytes, which was the fixed size before 1.9.0, and is also a hard limit for UDP responses. .. class:: PacketCache diff --git a/pdns/test-dnsdistpacketcache_cc.cc b/pdns/test-dnsdistpacketcache_cc.cc index 14fceb06fa..bee70fc5ca 100644 --- a/pdns/test-dnsdistpacketcache_cc.cc +++ b/pdns/test-dnsdistpacketcache_cc.cc @@ -512,6 +512,155 @@ BOOST_AUTO_TEST_CASE(test_PacketCacheTruncated) { } } +BOOST_AUTO_TEST_CASE(test_PacketCacheMaximumSize) { + const size_t maxEntries = 150000; + DNSDistPacketCache packetCache(maxEntries, 86400, 1); + InternalQueryState ids; + ids.qtype = QType::A; + ids.qclass = QClass::IN; + ids.protocol = dnsdist::Protocol::DoUDP; + + ComboAddress remote; + bool dnssecOK = false; + ids.qname = DNSName("maximum.size"); + + PacketBuffer query; + uint16_t queryID{0}; + { + GenericDNSPacketWriter pwQ(query, ids.qname, QType::AAAA, QClass::IN, 0); + pwQ.getHeader()->rd = 1; + queryID = pwQ.getHeader()->id; + } + + PacketBuffer response; + { + GenericDNSPacketWriter pwR(response, ids.qname, QType::AAAA, QClass::IN, 0); + pwR.getHeader()->rd = 1; + pwR.getHeader()->ra = 1; + pwR.getHeader()->qr = 1; + pwR.getHeader()->id = queryID; + pwR.startRecord(ids.qname, QType::AAAA, 7200, QClass::IN, DNSResourceRecord::ANSWER); + ComboAddress v6("2001:db8::1"); + pwR.xfrCAWithoutPort(6, v6); + pwR.commit(); + } + + /* first, we set the maximum entry size to the response packet size */ + packetCache.setMaximumEntrySize(response.size()); + + { + /* UDP */ + uint32_t key = 0; + boost::optional subnet; + DNSQuestion dq(ids, query); + bool found = packetCache.get(dq, 0, &key, subnet, dnssecOK, receivedOverUDP); + BOOST_CHECK_EQUAL(found, false); + BOOST_CHECK(!subnet); + + packetCache.insert(key, subnet, *(getFlagsFromDNSHeader(dq.getHeader().get())), dnssecOK, ids.qname, QType::A, QClass::IN, response, receivedOverUDP, RCode::NoError, boost::none); + found = packetCache.get(dq, queryID, &key, subnet, dnssecOK, receivedOverUDP, 0, true); + BOOST_CHECK_EQUAL(found, true); + BOOST_CHECK(!subnet); + } + + { + /* same but over TCP */ + uint32_t key = 0; + boost::optional subnet; + ids.protocol = dnsdist::Protocol::DoTCP; + DNSQuestion dq(ids, query); + bool found = packetCache.get(dq, 0, &key, subnet, dnssecOK, !receivedOverUDP); + BOOST_CHECK_EQUAL(found, false); + BOOST_CHECK(!subnet); + + packetCache.insert(key, subnet, *(getFlagsFromDNSHeader(dq.getHeader().get())), dnssecOK, ids.qname, QType::A, QClass::IN, response, !receivedOverUDP, RCode::NoError, boost::none); + found = packetCache.get(dq, queryID, &key, subnet, dnssecOK, !receivedOverUDP, 0, true); + BOOST_CHECK_EQUAL(found, true); + BOOST_CHECK(!subnet); + } + + /* then we set it slightly below response packet size */ + packetCache.expunge(0); + packetCache.setMaximumEntrySize(response.size() - 1); + { + /* UDP */ + uint32_t key = 0; + boost::optional subnet; + DNSQuestion dq(ids, query); + bool found = packetCache.get(dq, 0, &key, subnet, dnssecOK, receivedOverUDP); + BOOST_CHECK_EQUAL(found, false); + BOOST_CHECK(!subnet); + + packetCache.insert(key, subnet, *(getFlagsFromDNSHeader(dq.getHeader().get())), dnssecOK, ids.qname, QType::A, QClass::IN, response, receivedOverUDP, RCode::NoError, boost::none); + found = packetCache.get(dq, queryID, &key, subnet, dnssecOK, receivedOverUDP, 0, true); + BOOST_CHECK_EQUAL(found, false); + } + + { + /* same but over TCP */ + uint32_t key = 0; + boost::optional subnet; + ids.protocol = dnsdist::Protocol::DoTCP; + DNSQuestion dq(ids, query); + bool found = packetCache.get(dq, 0, &key, subnet, dnssecOK, !receivedOverUDP); + BOOST_CHECK_EQUAL(found, false); + BOOST_CHECK(!subnet); + + packetCache.insert(key, subnet, *(getFlagsFromDNSHeader(dq.getHeader().get())), dnssecOK, ids.qname, QType::A, QClass::IN, response, !receivedOverUDP, RCode::NoError, boost::none); + found = packetCache.get(dq, queryID, &key, subnet, dnssecOK, !receivedOverUDP, 0, true); + BOOST_CHECK_EQUAL(found, false); + } + + /* now we generate a very big response packet, it should be cached over TCP and UDP (although in practice dnsdist will refuse to cache it for the UDP case) */ + packetCache.expunge(0); + response.clear(); + { + GenericDNSPacketWriter pwR(response, ids.qname, QType::AAAA, QClass::IN, 0); + pwR.getHeader()->rd = 1; + pwR.getHeader()->ra = 1; + pwR.getHeader()->qr = 1; + pwR.getHeader()->id = queryID; + for (size_t idx = 0; idx < 1000; idx++) { + pwR.startRecord(ids.qname, QType::AAAA, 7200, QClass::IN, DNSResourceRecord::ANSWER); + ComboAddress v6("2001:db8::1"); + pwR.xfrCAWithoutPort(6, v6); + } + pwR.commit(); + } + + BOOST_REQUIRE_GT(response.size(), 4096U); + packetCache.setMaximumEntrySize(response.size()); + + { + /* UDP */ + uint32_t key = 0; + boost::optional subnet; + DNSQuestion dq(ids, query); + bool found = packetCache.get(dq, 0, &key, subnet, dnssecOK, receivedOverUDP); + BOOST_CHECK_EQUAL(found, false); + BOOST_CHECK(!subnet); + + packetCache.insert(key, subnet, *(getFlagsFromDNSHeader(dq.getHeader().get())), dnssecOK, ids.qname, QType::A, QClass::IN, response, receivedOverUDP, RCode::NoError, boost::none); + found = packetCache.get(dq, queryID, &key, subnet, dnssecOK, receivedOverUDP, 0, true); + BOOST_CHECK_EQUAL(found, true); + } + + { + /* same but over TCP */ + uint32_t key = 0; + boost::optional subnet; + ids.protocol = dnsdist::Protocol::DoTCP; + DNSQuestion dq(ids, query); + bool found = packetCache.get(dq, 0, &key, subnet, dnssecOK, !receivedOverUDP); + BOOST_CHECK_EQUAL(found, false); + BOOST_CHECK(!subnet); + + packetCache.insert(key, subnet, *(getFlagsFromDNSHeader(dq.getHeader().get())), dnssecOK, ids.qname, QType::A, QClass::IN, response, !receivedOverUDP, RCode::NoError, boost::none); + found = packetCache.get(dq, queryID, &key, subnet, dnssecOK, !receivedOverUDP, 0, true); + BOOST_CHECK_EQUAL(found, true); + } +} + static DNSDistPacketCache g_PC(500000); static void threadMangler(unsigned int offset) diff --git a/regression-tests.dnsdist/test_Caching.py b/regression-tests.dnsdist/test_Caching.py index d7539836e8..7af2be9c9c 100644 --- a/regression-tests.dnsdist/test_Caching.py +++ b/regression-tests.dnsdist/test_Caching.py @@ -2916,3 +2916,71 @@ class TestAPICache(DNSDistTest): receivedQuery.id = query.id self.assertEqual(query, receivedQuery) self.assertEqual(receivedResponse, response) + +class TestCachingOfVeryLargeAnswers(DNSDistTest): + + _config_template = """ + pc = newPacketCache(100, {maxTTL=86400, minTTL=1, maximumEntrySize=8192}) + getPool(""):setCache(pc) + newServer{address="127.0.0.1:%d"} + """ + + def testVeryLargeAnswer(self): + """ + Cache: Check that we can cache (and retrieve) VERY large answers + + We should be able to get answers as large as 8192 bytes this time + """ + numberOfQueries = 10 + name = 'very-large-answer.cache.tests.powerdns.com.' + query = dns.message.make_query(name, 'TXT', 'IN') + response = dns.message.make_response(query) + # we prepare a large answer + # for i in range(44): + # if len(content) > 0: + # content = content + ', ' + # content = content + (str(i)*50) + # # pad up to 4096 + # content = content + 'A'*42 + content = '' + for i in range(31): + if len(content) > 0: + content = content + ' ' + content = content + 'A' * 255 + # pad up to 8192 + content = content + ' ' + 'B' * 183 + + rrset = dns.rrset.from_text(name, + 3600, + dns.rdataclass.IN, + dns.rdatatype.TXT, + content) + response.answer.append(rrset) + self.assertEqual(len(response.to_wire()), 8192) + + # # first query to fill the cache, over TCP + (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = query.id + self.assertEqual(query, receivedQuery) + self.assertEqual(receivedResponse, response) + + for _ in range(numberOfQueries): + (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False) + self.assertEqual(receivedResponse, response) + + total = 0 + for key in self._responsesCounter: + total += self._responsesCounter[key] + TestCachingOfVeryLargeAnswers._responsesCounter[key] = 0 + + self.assertEqual(total, 1) + + # UDP should not be cached, dnsdist has a hard limit to 4096 bytes for UDP + # actually we will never get an answer, because dnsdist will not be able to get it from the backend + (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response) + self.assertTrue(receivedQuery) + self.assertFalse(receivedResponse) + receivedQuery.id = query.id + self.assertEqual(query, receivedQuery)