/* no TTL found, we don't want to cache this */
if (minTTL == std::numeric_limits<uint32_t>::max()) {
- return;
+ if (d_settings.d_truncatedTTL == 0) {
+ return;
+ }
+ dnsheader_aligned dh_aligned(response.data());
+ if (dh_aligned->tc == 0) {
+ return;
+ }
+ minTTL = d_settings.d_truncatedTTL;
}
if (rcode == RCode::NXDomain || (rcode == RCode::NoError && seenAuthSOA)) {
}
if (!truncatedOK) {
- dnsheader dnsHeader{};
- memcpy(&dnsHeader, value.value.data(), sizeof(dnsHeader));
- if (dnsHeader.tc != 0) {
+ dnsheader_aligned dh_aligned(value.value.data());
+ if (dh_aligned->tc != 0) {
return false;
}
}
continue;
}
- dnsheader dnsHeader{};
- memcpy(&dnsHeader, value.value.data(), sizeof(dnsheader));
- if (dnsHeader.rcode != RCode::NoError || (dnsHeader.ancount == 0 && dnsHeader.nscount == 0 && dnsHeader.arcount == 0)) {
+ dnsheader_aligned dnsHeader(value.value.data());
+ if (dnsHeader->rcode != RCode::NoError || (dnsHeader->ancount == 0 && dnsHeader->nscount == 0 && dnsHeader->arcount == 0)) {
continue;
}
continue;
}
- dnsheader dnsHeader{};
if (value.len < sizeof(dnsheader)) {
continue;
}
- memcpy(&dnsHeader, value.value.data(), sizeof(dnsheader));
- if (dnsHeader.rcode != RCode::NoError || (dnsHeader.ancount == 0 && dnsHeader.nscount == 0 && dnsHeader.arcount == 0)) {
+ dnsheader_aligned dnsHeader(value.value.data());
+ if (dnsHeader->rcode != RCode::NoError || (dnsHeader->ancount == 0 && dnsHeader->nscount == 0 && dnsHeader->arcount == 0)) {
continue;
}
uint32_t d_minTTL{0};
uint32_t d_tempFailureTTL{60};
uint32_t d_maxNegativeTTL{3600};
+ uint32_t d_truncatedTTL{0};
uint32_t d_staleTTL{60};
uint32_t d_shardCount{1};
bool d_dontAge{false};
getOptionalValue<bool>(vars, "parseECS", settings.d_parseECS);
getOptionalValue<size_t>(vars, "staleTTL", settings.d_staleTTL);
getOptionalValue<size_t>(vars, "temporaryFailureTTL", settings.d_tempFailureTTL);
+ getOptionalValue<size_t>(vars, "truncatedTTL", settings.d_truncatedTTL);
getOptionalValue<bool>(vars, "cookieHashing", cookieHashing);
getOptionalValue<size_t>(vars, "maximumEntrySize", maximumEntrySize);
#[serde(default = "crate::U32::<60>::value", skip_serializing_if = "crate::U32::<60>::is_equal")]
temporary_failure_ttl: u32,
#[serde(default, skip_serializing_if = "crate::is_default")]
+ truncated_ttl: u32,
+ #[serde(default, skip_serializing_if = "crate::is_default")]
cookie_hashing: bool,
#[serde(default = "crate::U32::<4096>::value", skip_serializing_if = "crate::U32::<4096>::is_equal")]
maximum_entry_size: u32,
type: "u32"
default: "60"
description: "On a SERVFAIL or REFUSED from the backend, cache for this amount of seconds"
+ - name: "truncated_ttl"
+ type: "u32"
+ default: 0
+ description: "On a truncated (TC=1, no records) response from the backend, cache for this amount of seconds. 0, the default, means that truncated answers are not cached"
- name: "cookie_hashing"
type: "bool"
default: "false"
.. versionchanged:: 1.9.0
``maximumEntrySize`` parameter added.
+ .. versionchanged:: 2.0.0
+ ``truncatedTTL`` parameter added.
+
Creates a new :class:`PacketCache` with the settings specified.
:param int maxEntries: The maximum number of entries in this cache
* ``numberOfShards=20``: int - Number of shards to divide the cache into, to reduce lock contention. Used to be 1 (no shards) before 1.6.0, and is now 20.
* ``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 :doc:`../advanced/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..
+ * ``temporaryFailureTTL=60``: int - On a SERVFAIL or REFUSED from the backend, cache for this amount of seconds.
+ * ``truncatedTTL=0``: int - On a truncated (TC=1, no records) response from the backend, cache for this amount of seconds. 0, the default, means that truncated answers are not cached.
* ``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.
- **parse_ecs**: Boolean ``(false)`` - 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 :doc:`../advanced/zero-scope` option to work
- **stale_ttl**: Unsigned integer ``(60)`` - 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
- **temporary_failure_ttl**: Unsigned integer ``(60)`` - On a SERVFAIL or REFUSED from the backend, cache for this amount of seconds
+- **truncated_ttl**: Unsigned integer ``(0)`` - On a truncated (TC=1, no records) response from the backend, cache for this amount of seconds. 0, the default, means that truncated answers are not cached
- **cookie_hashing**: Boolean ``(false)`` - 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
- **maximum_entry_size**: Unsigned integer ``(4096)`` - The maximum size, in bytes, of a DNS packet that can be inserted into the packet cache
- **options_to_skip**: Sequence of String ``("")`` - Extra list of EDNS option codes to skip when hashing the packet (if ``cookie_hashing`` above is false, EDNS cookie option number will be added to this list internally)
BOOST_AUTO_TEST_CASE(test_PacketCacheTruncated)
{
- const DNSDistPacketCache::CacheSettings settings{
- .d_maxEntries = 150000,
- .d_maxTTL = 86400,
- .d_minTTL = 1,
- .d_tempFailureTTL = 60,
- .d_maxNegativeTTL = 1,
- };
- DNSDistPacketCache localCache(settings);
-
InternalQueryState ids;
ids.qtype = QType::A;
ids.qclass = QClass::IN;
ids.protocol = dnsdist::Protocol::DoUDP;
ids.queryRealTime.start(); // does not have to be accurate ("realTime") in tests
+ ids.qname = DNSName("truncated");
bool dnssecOK = false;
+ PacketBuffer query;
+ GenericDNSPacketWriter<PacketBuffer> pwQ(query, ids.qname, QType::A, QClass::IN, 0);
+ pwQ.getHeader()->rd = 1;
- try {
- ids.qname = DNSName("truncated");
- PacketBuffer query;
- GenericDNSPacketWriter<PacketBuffer> pwQ(query, ids.qname, QType::A, QClass::IN, 0);
- pwQ.getHeader()->rd = 1;
+ PacketBuffer response;
+ GenericDNSPacketWriter<PacketBuffer> pwR(response, ids.qname, QType::A, QClass::IN, 0);
+ pwR.getHeader()->rd = 1;
+ pwR.getHeader()->ra = 0;
+ pwR.getHeader()->qr = 1;
+ pwR.getHeader()->tc = 1;
+ pwR.getHeader()->rcode = RCode::NoError;
+ pwR.getHeader()->id = pwQ.getHeader()->id;
+ pwR.commit();
- PacketBuffer response;
- GenericDNSPacketWriter<PacketBuffer> pwR(response, ids.qname, QType::A, QClass::IN, 0);
- pwR.getHeader()->rd = 1;
- pwR.getHeader()->ra = 0;
- pwR.getHeader()->qr = 1;
- pwR.getHeader()->tc = 1;
- pwR.getHeader()->rcode = RCode::NoError;
- pwR.getHeader()->id = pwQ.getHeader()->id;
- pwR.commit();
- pwR.startRecord(ids.qname, QType::A, 7200, QClass::IN, DNSResourceRecord::ANSWER);
- pwR.xfr32BitInt(0x01020304);
- pwR.commit();
+ uint32_t key = 0;
+ boost::optional<Netmask> subnet;
+ DNSQuestion dnsQuestion(ids, query);
+ bool allowTruncated = true;
- uint32_t key = 0;
- boost::optional<Netmask> subnet;
- DNSQuestion dnsQuestion(ids, query);
- bool found = localCache.get(dnsQuestion, 0, &key, subnet, dnssecOK, receivedOverUDP);
- BOOST_CHECK_EQUAL(found, false);
+ {
+ /* truncated answers are not cached by default */
+ const DNSDistPacketCache::CacheSettings settings{
+ .d_maxEntries = 150000,
+ .d_maxTTL = 86400,
+ .d_minTTL = 1,
+ .d_tempFailureTTL = 60,
+ .d_maxNegativeTTL = 1,
+ };
+ DNSDistPacketCache localCache(settings);
+ BOOST_CHECK_EQUAL(localCache.getSize(), 0U);
+
+ bool found = localCache.get(dnsQuestion, 0, &key, subnet, dnssecOK, receivedOverUDP, 0, false, allowTruncated);
+ BOOST_REQUIRE_EQUAL(found, false);
BOOST_CHECK(!subnet);
- localCache.insert(key, subnet, *(getFlagsFromDNSHeader(dnsQuestion.getHeader().get())), dnssecOK, ids.qname, QType::A, QClass::IN, response, receivedOverUDP, RCode::NXDomain, boost::none);
+ localCache.insert(key, subnet, *(getFlagsFromDNSHeader(dnsQuestion.getHeader().get())), dnssecOK, ids.qname, QType::A, QClass::IN, response, receivedOverUDP, 0, boost::none);
- bool allowTruncated = true;
- found = localCache.get(dnsQuestion, pwR.getHeader()->id, &key, subnet, dnssecOK, receivedOverUDP, 0, true, allowTruncated);
- BOOST_CHECK_EQUAL(found, true);
+ found = localCache.get(dnsQuestion, pwR.getHeader()->id, &key, subnet, dnssecOK, receivedOverUDP, 0, false, allowTruncated);
+ BOOST_REQUIRE_EQUAL(found, false);
+ }
+
+ {
+ /* enable caching of truncated answers */
+ const DNSDistPacketCache::CacheSettings settings{
+ .d_maxEntries = 150000,
+ .d_maxTTL = 86400,
+ .d_minTTL = 1,
+ .d_truncatedTTL = 60,
+ };
+ DNSDistPacketCache localCache(settings);
+ BOOST_CHECK_EQUAL(localCache.getSize(), 0U);
+
+ bool found = localCache.get(dnsQuestion, 0, &key, subnet, dnssecOK, receivedOverUDP, 0, false, allowTruncated);
+ BOOST_REQUIRE_EQUAL(found, false);
BOOST_CHECK(!subnet);
+ localCache.insert(key, subnet, *(getFlagsFromDNSHeader(dnsQuestion.getHeader().get())), dnssecOK, ids.qname, QType::A, QClass::IN, response, receivedOverUDP, 0, boost::none);
+
allowTruncated = false;
- found = localCache.get(dnsQuestion, pwR.getHeader()->id, &key, subnet, dnssecOK, receivedOverUDP, 0, true, allowTruncated);
- BOOST_CHECK_EQUAL(found, false);
- }
- catch (const PDNSException& e) {
- cerr << "Had error: " << e.reason << endl;
- throw;
+ found = localCache.get(dnsQuestion, pwR.getHeader()->id, &key, subnet, dnssecOK, receivedOverUDP, 0, false, allowTruncated);
+ BOOST_REQUIRE_EQUAL(found, false);
+
+ allowTruncated = true;
+ found = localCache.get(dnsQuestion, pwR.getHeader()->id, &key, subnet, dnssecOK, receivedOverUDP, 0, false, allowTruncated);
+ BOOST_REQUIRE_EQUAL(found, true);
+ BOOST_REQUIRE_EQUAL(dnsQuestion.getData().size(), response.size());
+ int match = memcmp(dnsQuestion.getData().data(), response.data(), dnsQuestion.getData().size());
+ BOOST_CHECK_EQUAL(match, 0);
+ BOOST_CHECK(!subnet);
}
}
self.assertEqual(total, 1)
+ def testEmptyTruncated(self):
+ """
+ Cache: Empty TC=1 is not cached by default
+ """
+ name = 'empty-tc.cache.tests.powerdns.com.'
+ query = dns.message.make_query(name, 'AAAA', 'IN')
+ response = dns.message.make_response(query)
+ response.flags |= dns.flags.TC
+
+ for _ in range(2):
+ (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+ self.assertTrue(receivedQuery)
+ self.assertTrue(receivedResponse)
+ receivedQuery.id = query.id
+ self.assertEqual(query, receivedQuery)
+ self.assertEqual(receivedResponse, response)
+
def testDOCached(self):
"""
Cache: Served from cache, query has DO bit set
self.assertFalse(receivedResponse)
receivedQuery.id = query.id
self.assertEqual(query, receivedQuery)
+
+class TestCacheEmptyTC(DNSDistTest):
+
+ _truncated_ttl = 42
+ _config_template = """
+ pc = newPacketCache(100, {maxTTL=86400, minTTL=1, truncatedTTL=%d})
+ getPool(""):setCache(pc)
+ newServer{address="127.0.0.1:%d"}
+ """
+ _config_params = ['_truncated_ttl', '_testServerPort']
+
+ def testEmptyTruncated(self):
+ """
+ Cache: Empty TC=1 should be cached
+ """
+ name = 'cache-empty-tc.cache.tests.powerdns.com.'
+ query = dns.message.make_query(name, 'AAAA', 'IN')
+ response = dns.message.make_response(query)
+ response.flags |= dns.flags.TC
+
+ # first to fill the cache
+ for method in ("sendUDPQuery", "sendTCPQuery"):
+ sender = getattr(self, method)
+ (receivedQuery, receivedResponse) = sender(query, response)
+ self.assertTrue(receivedQuery)
+ self.assertTrue(receivedResponse)
+ receivedQuery.id = query.id
+ self.assertEqual(query, receivedQuery)
+ self.assertEqual(receivedResponse, response)
+
+ # now it should be cached
+ for method in ("sendUDPQuery", "sendTCPQuery"):
+ sender = getattr(self, method)
+ (_, receivedResponse) = sender(query, response=None, useQueue=False)
+ self.assertEqual(receivedResponse, response)