void DNSDistPacketCache::insert(uint32_t key, const boost::optional<Netmask>& 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<uint32_t> tempFailureTTL)
{
- if (response.size() < sizeof(dnsheader)) {
+ if (response.size() < sizeof(dnsheader) || response.size() > getMaximumEntrySize()) {
return;
}
+
if (qtype == QType::AXFR || qtype == QType::IXFR) {
return;
}
return addresses;
}
+
+void DNSDistPacketCache::setMaximumEntrySize(size_t maxSize)
+{
+ d_maximumEntrySize = maxSize;
+}
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);
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};
};
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<size_t>(512), s_maxPacketCacheEntrySize));
+ d_buffer.resize(d_querySize);
d_currentPos = 0;
}
else {
std::set<std::string> 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)
return false;
}
- if (dr.ids.packetCache && !dr.ids.selfGenerated && !dr.ids.skipCache && response.size() <= s_maxPacketCacheEntrySize) {
+ if (dr.ids.packetCache && !dr.ids.selfGenerated && !dr.ids.skipCache && (!dr.ids.forwardedOverUDP || response.size() <= s_maxUDPResponsePacketSize)) {
if (!dr.ids.useZeroScope) {
/* if the query was not suitable for zero-scope, for
example because it had an existing ECS entry so the hash is
extern std::set<std::string> 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<DownstreamState>& selectedBackend);
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;
getOptionalValue<size_t>(vars, "staleTTL", staleTTL);
getOptionalValue<size_t>(vars, "temporaryFailureTTL", tempFailTTL);
getOptionalValue<bool>(vars, "cookieHashing", cookieHashing);
+ getOptionalValue<size_t>(vars, "maximumEntrySize", maxEntrySize);
if (getOptionalValue<decltype(skipOptions)>(vars, "skipOptions", skipOptions) > 0) {
for (const auto& option : skipOptions) {
res->setKeepStaleData(keepStaleData);
res->setSkippedOptions(optionsToSkip);
+ if (maxEntrySize >= sizeof(dnsheader)) {
+ res->setMaximumEntrySize(maxEntrySize);
+ }
return res;
});
class TCPConnectionToBackend : public ConnectionToBackend
{
public:
- TCPConnectionToBackend(const std::shared_ptr<DownstreamState>& ds, std::unique_ptr<FDMultiplexer>& 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<DownstreamState>& ds, std::unique_ptr<FDMultiplexer>& 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)
{
}
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<IOStateHandler>(*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<IOStateHandler>(*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;
.. 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
* ``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
}
}
+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<PacketBuffer> pwQ(query, ids.qname, QType::AAAA, QClass::IN, 0);
+ pwQ.getHeader()->rd = 1;
+ queryID = pwQ.getHeader()->id;
+ }
+
+ PacketBuffer response;
+ {
+ GenericDNSPacketWriter<PacketBuffer> 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<Netmask> 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<Netmask> 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<Netmask> 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<Netmask> 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<PacketBuffer> 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<Netmask> 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<Netmask> 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)
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)