From: Otto Moerbeek Date: Tue, 14 Jun 2022 13:42:41 +0000 (+0200) Subject: Implementation of serve-stale from record cache. X-Git-Tag: rec-4.8.0-alpha1~24^2~10 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=edabff75e8f628b68be876497b8a6b771a08a3d1;p=thirdparty%2Fpdns.git Implementation of serve-stale from record cache. If a resolve fails, we try it again with serveStale is true. If serveStale is true or a record is already being server stale, the record cache is willing to produce (and extend the ttd) of stale records. It wil also keep a count of the extensions, to be able to limit those and trigger a task te refresh once every while. If we (potentially) serve stale, we are less aggessive evicting stale records from the record cache. Enable by setting server-stale-extensions (default 0). The unit is 30s. So a value of 2880 will keep serving the record for 24 hours, even if it cannot be refreshed. If the original ttl of a record is less than 30, the extension unit will be that ttl. --- diff --git a/pdns/cachecleaner.hh b/pdns/cachecleaner.hh index 578b469094..f489a9d88f 100644 --- a/pdns/cachecleaner.hh +++ b/pdns/cachecleaner.hh @@ -26,7 +26,7 @@ #include "dnsname.hh" #include "lock.hh" -// this function can clean any cache that has a getTTD() method on its entries, a preRemoval() method and a 'sequence' index as its second index +// this function can clean any cache that has an isStale() method on its entries, a preRemoval() method and a 'sequence' index as its second index // the ritual is that the oldest entries are in *front* of the sequence collection, so on a hit, move an item to the end // and optionally, on a miss, move it to the beginning template @@ -49,7 +49,7 @@ void pruneCollection(C& container, T& collection, size_t maxCached, size_t scanF size_t tried = 0, erased = 0; for (auto iter = sidx.begin(); iter != sidx.end() && tried < lookAt; ++tried) { - if (iter->getTTD() < now) { + if (iter->isStale(now)) { iter = sidx.erase(iter); erased++; } @@ -157,7 +157,7 @@ uint64_t pruneMutexCollectionsVector(C& container, std::vector& maps, uint64_ auto& sidx = boost::multi_index::get(mc->d_map); uint64_t erased = 0, lookedAt = 0; for (auto i = sidx.begin(); i != sidx.end(); lookedAt++) { - if (i->getTTD() < now) { + if (i->isStale(now)) { container.preRemoval(*mc, *i); i = sidx.erase(i); erased++; diff --git a/pdns/dnsseckeeper.hh b/pdns/dnsseckeeper.hh index 4dfe4c1b1d..b3082d80d7 100644 --- a/pdns/dnsseckeeper.hh +++ b/pdns/dnsseckeeper.hh @@ -251,9 +251,9 @@ private: { typedef vector keys_t; - uint32_t getTTD() const + uint32_t isStale(time_t now) const { - return d_ttd; + return d_ttd < now; } DNSName d_domain; @@ -263,9 +263,9 @@ private: struct METACacheEntry { - time_t getTTD() const + time_t isStale(time_t now) const { - return d_ttd; + return d_ttd < now; } DNSName d_domain; diff --git a/pdns/recpacketcache.cc b/pdns/recpacketcache.cc index 4e15cbed07..6ca11910f7 100644 --- a/pdns/recpacketcache.cc +++ b/pdns/recpacketcache.cc @@ -65,7 +65,7 @@ bool RecursorPacketCache::checkResponseMatches(std::paird_submitted = true; - pushAlmostExpiredTask(qname, qtype, iter->d_ttd); + pushAlmostExpiredTask(qname, qtype, iter->d_ttd, Netmask()); } } *responsePacket = iter->d_packet; diff --git a/pdns/recpacketcache.hh b/pdns/recpacketcache.hh index 6e15ee4752..45ba4298ee 100644 --- a/pdns/recpacketcache.hh +++ b/pdns/recpacketcache.hh @@ -118,9 +118,9 @@ private: bool d_tcp; // whether this entry was created from a TCP query inline bool operator<(const struct Entry& rhs) const; - time_t getTTD() const + bool isStale(time_t now) const { - return d_ttd; + return d_ttd < now; } uint32_t getOrigTTL() const diff --git a/pdns/recursor_cache.cc b/pdns/recursor_cache.cc index 5b44c0e20b..9b6238a779 100644 --- a/pdns/recursor_cache.cc +++ b/pdns/recursor_cache.cc @@ -15,6 +15,8 @@ #include "cachecleaner.hh" #include "rec-taskqueue.hh" +uint16_t MemRecursorCache::s_maxServedStaleExtensions; + MemRecursorCache::MemRecursorCache(size_t mapsCount) : d_maps(mapsCount) { @@ -152,7 +154,31 @@ time_t MemRecursorCache::handleHit(MapCombo::LockedContent& content, MemRecursor return ttd; } -MemRecursorCache::cache_t::const_iterator MemRecursorCache::getEntryUsingECSIndex(MapCombo::LockedContent& map, time_t now, const DNSName& qname, const QType qtype, bool requireAuth, const ComboAddress& who) +static void pushRefreshTask(const DNSName& qname, QType qtype, time_t deadline, const Netmask& netmask) +{ + if (qtype == QType::ADDR) { + pushAlmostExpiredTask(qname, QType::A, deadline, netmask); + pushAlmostExpiredTask(qname, QType::AAAA, deadline, netmask); + } + else { + pushAlmostExpiredTask(qname, qtype, deadline, netmask); + } +} + +void MemRecursorCache::updateStaleEntry(time_t now, MemRecursorCache::OrderedTagIterator_t& entry) +{ + // We need to take care a infrequently access stale item cannot be extended past + // s_maxServedStaleExtension * s_serveStaleExtensionPeriod + // We we look how old the entry is, and increase d_servedStale accordingly, taking care not to overflow + const time_t howlong = std::max(static_cast(1), now - entry->d_ttd); + const uint32_t extension = std::max(1U, std::min(entry->d_orig_ttl, s_serveStaleExtensionPeriod)); + entry->d_servedStale = std::min(entry->d_servedStale + 1 + howlong / extension, static_cast(s_maxServedStaleExtensions)); + entry->d_ttd = now + std::min(entry->d_orig_ttl, s_serveStaleExtensionPeriod); + + pushRefreshTask(entry->d_qname, entry->d_qtype, entry->d_ttd, entry->d_netmask); +} + +MemRecursorCache::cache_t::const_iterator MemRecursorCache::getEntryUsingECSIndex(MapCombo::LockedContent& map, time_t now, const DNSName& qname, const QType qtype, bool requireAuth, const ComboAddress& who, bool serveStale) { // MUTEX SHOULD BE ACQUIRED (as indicated by the reference to the content which is protected by a lock) auto ecsIndexKey = std::tie(qname, qtype); @@ -176,7 +202,12 @@ MemRecursorCache::cache_t::const_iterator MemRecursorCache::getEntryUsingECSInde } continue; } - + // If we are serving this record stale (or *should*) and the + // ttd has passed increase ttd to the future and remember that + // we did. Also push a refresh task. + if ((serveStale || entry->d_servedStale > 0) && entry->d_ttd <= now && entry->d_servedStale < s_maxServedStaleExtensions) { + updateStaleEntry(now, entry); + } if (entry->d_ttd > now) { if (!requireAuth || entry->d_auth) { return entry; @@ -187,11 +218,14 @@ MemRecursorCache::cache_t::const_iterator MemRecursorCache::getEntryUsingECSInde else { /* this netmask-specific entry has expired */ moveCacheItemToFront(map.d_map, entry); - ecsIndex->removeNetmask(best); - if (ecsIndex->isEmpty()) { - map.d_ecsIndex.erase(ecsIndex); - break; + // but is is also stale wrt serve-stale? + if (entry->isStale(now)) { + ecsIndex->removeNetmask(best); + if (ecsIndex->isEmpty()) { + map.d_ecsIndex.erase(ecsIndex); + } } + break; } } } @@ -200,6 +234,12 @@ MemRecursorCache::cache_t::const_iterator MemRecursorCache::getEntryUsingECSInde auto key = std::make_tuple(qname, qtype, boost::none, Netmask()); auto entry = map.d_map.find(key); if (entry != map.d_map.end()) { + // If we are serving this record stale (or *should*) and the + // ttd has passed increase ttd to the future and remember that + // we did. Also push a refresh task. + if ((serveStale || entry->d_servedStale > 0) && entry->d_ttd <= now && entry->d_servedStale < s_maxServedStaleExtensions) { + updateStaleEntry(now, entry); + } if (entry->d_ttd > now) { if (!requireAuth || entry->d_auth) { return entry; @@ -243,6 +283,12 @@ bool MemRecursorCache::entryMatches(MemRecursorCache::OrderedTagIterator_t& entr time_t MemRecursorCache::fakeTTD(MemRecursorCache::OrderedTagIterator_t& entry, const DNSName& qname, QType qtype, time_t ret, time_t now, uint32_t origTTL, bool refresh) { time_t ttl = ret - now; + // If we are checking an entry being served stale in refresh mode, + // we always consider it stale so a real refresh attempt will be + // kicked by SyncRes + if (refresh && entry->d_servedStale > 0) { + return -1; + } if (ttl > 0 && SyncRes::s_refresh_ttlperc > 0) { const uint32_t deadline = origTTL * SyncRes::s_refresh_ttlperc / 100; const bool almostExpired = static_cast(ttl) <= deadline; @@ -252,13 +298,7 @@ time_t MemRecursorCache::fakeTTD(MemRecursorCache::OrderedTagIterator_t& entry, } else { if (!entry->d_submitted) { - if (qtype == QType::ADDR) { - pushAlmostExpiredTask(qname, QType::A, entry->d_ttd); - pushAlmostExpiredTask(qname, QType::AAAA, entry->d_ttd); - } - else { - pushAlmostExpiredTask(qname, qtype, entry->d_ttd); - } + pushRefreshTask(qname, qtype, entry->d_ttd, entry->d_netmask); entry->d_submitted = true; } } @@ -271,6 +311,8 @@ time_t MemRecursorCache::get(time_t now, const DNSName& qname, const QType qt, F { bool requireAuth = flags & RequireAuth; bool refresh = flags & Refresh; + bool serveStale = flags & ServeStale; + boost::optional cachedState{boost::none}; uint32_t origTTL; @@ -293,11 +335,11 @@ time_t MemRecursorCache::get(time_t now, const DNSName& qname, const QType qt, F if (qtype == QType::ADDR) { time_t ret = -1; - auto entryA = getEntryUsingECSIndex(*map, now, qname, QType::A, requireAuth, who); + auto entryA = getEntryUsingECSIndex(*map, now, qname, QType::A, requireAuth, who, serveStale); if (entryA != map->d_map.end()) { ret = handleHit(*map, entryA, qname, origTTL, res, signatures, authorityRecs, variable, cachedState, wasAuth, fromAuthZone, fromAuthIP); } - auto entryAAAA = getEntryUsingECSIndex(*map, now, qname, QType::AAAA, requireAuth, who); + auto entryAAAA = getEntryUsingECSIndex(*map, now, qname, QType::AAAA, requireAuth, who, serveStale); if (entryAAAA != map->d_map.end()) { time_t ttdAAAA = handleHit(*map, entryAAAA, qname, origTTL, res, signatures, authorityRecs, variable, cachedState, wasAuth, fromAuthZone, fromAuthIP); if (ret > 0) { @@ -315,7 +357,7 @@ time_t MemRecursorCache::get(time_t now, const DNSName& qname, const QType qt, F return ret > 0 ? (ret - now) : ret; } else { - auto entry = getEntryUsingECSIndex(*map, now, qname, qtype, requireAuth, who); + auto entry = getEntryUsingECSIndex(*map, now, qname, qtype, requireAuth, who, serveStale); if (entry != map->d_map.end()) { time_t ret = handleHit(*map, entry, qname, origTTL, res, signatures, authorityRecs, variable, cachedState, wasAuth, fromAuthZone, fromAuthIP); if (state && cachedState) { @@ -337,7 +379,8 @@ time_t MemRecursorCache::get(time_t now, const DNSName& qname, const QType qt, F for (auto i = entries.first; i != entries.second; ++i) { firstIndexIterator = map->d_map.project(i); - if (i->d_ttd <= now) { + // When serving stale, we consider expired records + if (i->d_ttd <= now && !serveStale && i->d_servedStale == 0) { moveCacheItemToFront(map->d_map, firstIndexIterator); continue; } @@ -346,6 +389,12 @@ time_t MemRecursorCache::get(time_t now, const DNSName& qname, const QType qt, F continue; } found = true; + // If we are serving this record stale (or *should*) and the + // ttd has passed increase ttd to the future and remember that + // we did. Also push a refresh task. + if ((serveStale || i->d_servedStale > 0) && i->d_ttd <= now && i->d_servedStale < s_maxServedStaleExtensions) { + updateStaleEntry(now, firstIndexIterator); + } ttd = handleHit(*map, firstIndexIterator, qname, origTTL, res, signatures, authorityRecs, variable, cachedState, wasAuth, fromAuthZone, fromAuthIP); if (qt != QType::ANY && qt != QType::ADDR) { // normally if we have a hit, we are done @@ -374,7 +423,8 @@ time_t MemRecursorCache::get(time_t now, const DNSName& qname, const QType qt, F for (auto i = entries.first; i != entries.second; ++i) { firstIndexIterator = map->d_map.project(i); - if (i->d_ttd <= now) { + // When serving stale, we consider expired records + if (i->d_ttd <= now && !serveStale && i->d_servedStale == 0) { moveCacheItemToFront(map->d_map, firstIndexIterator); continue; } @@ -382,8 +432,13 @@ time_t MemRecursorCache::get(time_t now, const DNSName& qname, const QType qt, F if (!entryMatches(firstIndexIterator, qtype, requireAuth, who)) { continue; } - found = true; + // If we are serving this record stale (or *should*) and the + // ttd has passed increase ttd to the future and remember that + // we did. Also push a refresh task. + if ((serveStale || i->d_servedStale > 0) && i->d_ttd <= now && i->d_servedStale < s_maxServedStaleExtensions) { + updateStaleEntry(now, firstIndexIterator); + } ttd = handleHit(*map, firstIndexIterator, qname, origTTL, res, signatures, authorityRecs, variable, cachedState, wasAuth, fromAuthZone, fromAuthIP); if (qt != QType::ANY && qt != QType::ADDR) { // normally if we have a hit, we are done @@ -498,6 +553,7 @@ void MemRecursorCache::replace(time_t now, const DNSName& qname, const QType qt, moveCacheItemToBack(map->d_map, stored); } ce.d_submitted = false; + ce.d_servedStale = 0; map->d_map.replace(stored, ce); } @@ -611,7 +667,7 @@ bool MemRecursorCache::updateValidationStatus(time_t now, const DNSName& qname, bool updated = false; if (!map->d_ecsIndex.empty() && !routingTag) { - auto entry = getEntryUsingECSIndex(*map, now, qname, qtype, requireAuth, who); + auto entry = getEntryUsingECSIndex(*map, now, qname, qtype, requireAuth, who, false); // XXX serveStale? if (entry == map->d_map.end()) { return false; } @@ -668,7 +724,7 @@ uint64_t MemRecursorCache::doDump(int fd) for (const auto& j : i.d_records) { count++; try { - fprintf(fp.get(), "%s %" PRIu32 " %" PRId64 " IN %s %s ; (%s) auth=%i zone=%s from=%s %s %s\n", i.d_qname.toString().c_str(), i.d_orig_ttl, static_cast(i.d_ttd - now), i.d_qtype.toString().c_str(), j->getZoneRepresentation().c_str(), vStateToString(i.d_state).c_str(), i.d_auth, i.d_authZone.toLogString().c_str(), i.d_from.toString().c_str(), i.d_netmask.empty() ? "" : i.d_netmask.toString().c_str(), !i.d_rtag ? "" : i.d_rtag.get().c_str()); + fprintf(fp.get(), "%s %" PRIu32 " %" PRId64 " IN %s %s ; (%s) auth=%i zone=%s from=%s nm=%s rtag=%s ss=%hd\n", i.d_qname.toString().c_str(), i.d_orig_ttl, static_cast(i.d_ttd - now), i.d_qtype.toString().c_str(), j->getZoneRepresentation().c_str(), vStateToString(i.d_state).c_str(), i.d_auth, i.d_authZone.toLogString().c_str(), i.d_from.toString().c_str(), i.d_netmask.empty() ? "" : i.d_netmask.toString().c_str(), !i.d_rtag ? "" : i.d_rtag.get().c_str(), i.d_servedStale); } catch (...) { fprintf(fp.get(), "; error printing '%s'\n", i.d_qname.empty() ? "EMPTY" : i.d_qname.toString().c_str()); diff --git a/pdns/recursor_cache.hh b/pdns/recursor_cache.hh index ae58095f63..285dba7039 100644 --- a/pdns/recursor_cache.hh +++ b/pdns/recursor_cache.hh @@ -49,6 +49,11 @@ class MemRecursorCache : public boost::noncopyable // : public RecursorCache public: MemRecursorCache(size_t mapsCount = 1024); + // The number of times a state cache entry is extended + static uint16_t s_maxServedStaleExtensions; + // The time a stale cache entry is extended + static constexpr uint32_t s_serveStaleExtensionPeriod = 30; + size_t size() const; size_t bytes(); pair stats(); @@ -79,14 +84,21 @@ private: struct CacheEntry { CacheEntry(const std::tuple& key, bool auth) : - d_qname(std::get<0>(key)), d_netmask(std::get<3>(key).getNormalized()), d_rtag(std::get<2>(key)), d_state(vState::Indeterminate), d_ttd(0), d_orig_ttl{0}, d_qtype(std::get<1>(key)), d_auth(auth), d_submitted(false) + d_qname(std::get<0>(key)), d_netmask(std::get<3>(key).getNormalized()), d_rtag(std::get<2>(key)), d_state(vState::Indeterminate), d_ttd(0), d_orig_ttl{0}, d_servedStale(0), d_qtype(std::get<1>(key)), d_auth(auth), d_submitted(false) { } typedef vector> records_t; - time_t getTTD() const + + bool isStale(time_t now) const { - return d_ttd; + // We like to keep things in cache when we (potentially) should serve stale + if (s_maxServedStaleExtensions > 0) { + return d_ttd + s_maxServedStaleExtensions * std::min(s_serveStaleExtensionPeriod, d_orig_ttl) < now; + } + else { + return d_ttd < now; + } } records_t d_records; @@ -100,6 +112,7 @@ private: mutable vState d_state; mutable time_t d_ttd; uint32_t d_orig_ttl; + mutable uint16_t d_servedStale; QType d_qtype; bool d_auth; mutable bool d_submitted; // whether this entry has been queued for refetch @@ -255,9 +268,10 @@ private: bool entryMatches(OrderedTagIterator_t& entry, QType qt, bool requireAuth, const ComboAddress& who); Entries getEntries(MapCombo::LockedContent& content, const DNSName& qname, const QType qt, const OptTag& rtag); - cache_t::const_iterator getEntryUsingECSIndex(MapCombo::LockedContent& content, time_t now, const DNSName& qname, QType qtype, bool requireAuth, const ComboAddress& who); + cache_t::const_iterator getEntryUsingECSIndex(MapCombo::LockedContent& content, time_t now, const DNSName& qname, QType qtype, bool requireAuth, const ComboAddress& who, bool serveStale); time_t handleHit(MapCombo::LockedContent& content, OrderedTagIterator_t& entry, const DNSName& qname, uint32_t& origTTL, vector* res, vector>* signatures, std::vector>* authorityRecs, bool* variable, boost::optional& state, bool* wasAuth, DNSName* authZone, ComboAddress* fromAuthIP); + void updateStaleEntry(time_t now, OrderedTagIterator_t& entry); public: void preRemoval(MapCombo::LockedContent& map, const CacheEntry& entry) diff --git a/pdns/recursordist/negcache.hh b/pdns/recursordist/negcache.hh index 204406cbd2..83667edefe 100644 --- a/pdns/recursordist/negcache.hh +++ b/pdns/recursordist/negcache.hh @@ -65,9 +65,9 @@ public: mutable time_t d_ttd; // Timestamp when this entry should die mutable vState d_validationState{vState::Indeterminate}; QType d_qtype; // The denied type - time_t getTTD() const + bool isStale(time_t now) const { - return d_ttd; + return d_ttd < now; }; }; diff --git a/pdns/recursordist/rec-main.cc b/pdns/recursordist/rec-main.cc index 3240d2272f..c5971b9b14 100644 --- a/pdns/recursordist/rec-main.cc +++ b/pdns/recursordist/rec-main.cc @@ -1511,6 +1511,7 @@ static int serviceMain(int argc, char* argv[], Logr::log_t log) SyncRes::s_event_trace_enabled = ::arg().asNum("event-trace-enabled"); SyncRes::s_save_parent_ns_set = ::arg().mustDo("save-parent-ns-set"); SyncRes::s_max_busy_dot_probes = ::arg().asNum("max-busy-dot-probes"); + MemRecursorCache::s_maxServedStaleExtensions = ::arg().asNum("serve-stale-extensions"); if (SyncRes::s_tcp_fast_open_connect) { checkFastOpenSysctl(true, log); @@ -2810,6 +2811,7 @@ int main(int argc, char** argv) ::arg().set("structured-logging-backend", "Structured logging backend") = "default"; ::arg().setSwitch("save-parent-ns-set", "Save parent NS set to be used if child NS set fails") = "yes"; ::arg().set("max-busy-dot-probes", "Maximum number of concurrent DoT probes") = "0"; + ::arg().set("serve-stale-extensions", "Number of times a record's ttl is extended by 30s to be served stale") = "0"; ::arg().setCmd("help", "Provide a helpful message"); ::arg().setCmd("version", "Print version string"); diff --git a/pdns/recursordist/rec-taskqueue.cc b/pdns/recursordist/rec-taskqueue.cc index 98b5f6296d..c3aa7d2704 100644 --- a/pdns/recursordist/rec-taskqueue.cc +++ b/pdns/recursordist/rec-taskqueue.cc @@ -114,14 +114,15 @@ static struct taskstats s_resolve_tasks; static void resolve(const struct timeval& now, bool logErrors, const pdns::ResolveTask& task) noexcept { - auto log = g_slog->withName("taskq")->withValues("name", Logging::Loggable(task.d_qname), "qtype", Logging::Loggable(QType(task.d_qtype).toString())); + auto log = g_slog->withName("taskq")->withValues("name", Logging::Loggable(task.d_qname), "qtype", Logging::Loggable(QType(task.d_qtype).toString()), "netmask", Logging::Loggable(task.d_netmask.empty() ? "" : task.d_netmask.toString())); const string msg = "Exception while running a background ResolveTask"; SyncRes sr(now); vector ret; sr.setRefreshAlmostExpired(task.d_refreshMode); + sr.setQuerySource(task.d_netmask); bool ex = true; try { - log->info(Logr::Debug, "resolving"); + log->info(Logr::Debug, "resolving", "refresh", Logging::Loggable(task.d_refreshMode)); int res = sr.beginResolve(task.d_qname, QType(task.d_qtype), QClass::IN, ret); ex = false; log->info(Logr::Debug, "done", "rcode", Logging::Loggable(res), "records", Logging::Loggable(ret.size())); @@ -231,14 +232,14 @@ bool runTaskOnce(bool logErrors) return true; } -void pushAlmostExpiredTask(const DNSName& qname, uint16_t qtype, time_t deadline) +void pushAlmostExpiredTask(const DNSName& qname, uint16_t qtype, time_t deadline, const Netmask& netmask) { if (SyncRes::isUnsupported(qtype)) { - auto log = g_slog->withName("taskq")->withValues("name", Logging::Loggable(qname), "qtype", Logging::Loggable(QType(qtype).toString())); + auto log = g_slog->withName("taskq")->withValues("name", Logging::Loggable(qname), "qtype", Logging::Loggable(QType(qtype).toString()), "netmask", Logging::Loggable(netmask.empty() ? "" : netmask.toString())); log->error(Logr::Error, "Cannot push task", "qtype unsupported"); return; } - pdns::ResolveTask task{qname, qtype, deadline, true, resolve, {}, {}}; + pdns::ResolveTask task{qname, qtype, deadline, true, resolve, {}, {}, netmask}; if (s_taskQueue.lock()->queue.push(std::move(task))) { ++s_almost_expired_tasks.pushed; } @@ -251,7 +252,7 @@ void pushResolveTask(const DNSName& qname, uint16_t qtype, time_t now, time_t de log->error(Logr::Error, "Cannot push task", "qtype unsupported"); return; } - pdns::ResolveTask task{qname, qtype, deadline, false, resolve, {}, {}}; + pdns::ResolveTask task{qname, qtype, deadline, false, resolve, {}, {}, {}}; auto lock = s_taskQueue.lock(); bool inserted = lock->rateLimitSet.insert(now, task); if (inserted) { @@ -269,7 +270,7 @@ bool pushTryDoTTask(const DNSName& qname, uint16_t qtype, const ComboAddress& ip return false; } - pdns::ResolveTask task{qname, qtype, deadline, false, tryDoT, ip, nsname}; + pdns::ResolveTask task{qname, qtype, deadline, false, tryDoT, ip, nsname, {}}; bool pushed = s_taskQueue.lock()->queue.push(std::move(task)); if (pushed) { ++s_almost_expired_tasks.pushed; diff --git a/pdns/recursordist/rec-taskqueue.hh b/pdns/recursordist/rec-taskqueue.hh index 868b33e0af..67bc6e0285 100644 --- a/pdns/recursordist/rec-taskqueue.hh +++ b/pdns/recursordist/rec-taskqueue.hh @@ -26,6 +26,7 @@ class DNSName; union ComboAddress; +class Netmask; namespace pdns { @@ -33,7 +34,7 @@ struct ResolveTask; } void runTasks(size_t max, bool logErrors); bool runTaskOnce(bool logErrors); -void pushAlmostExpiredTask(const DNSName& qname, uint16_t qtype, time_t deadline); +void pushAlmostExpiredTask(const DNSName& qname, uint16_t qtype, time_t deadline, const Netmask& netmask); void pushResolveTask(const DNSName& qname, uint16_t qtype, time_t now, time_t deadline); bool pushTryDoTTask(const DNSName& qname, uint16_t qtype, const ComboAddress& ip, time_t deadline, const DNSName& nsname); void taskQueueClear(); diff --git a/pdns/recursordist/taskqueue.hh b/pdns/recursordist/taskqueue.hh index 40df635dfd..0d43f06dc3 100644 --- a/pdns/recursordist/taskqueue.hh +++ b/pdns/recursordist/taskqueue.hh @@ -31,7 +31,7 @@ size_t hash_value(const ComboAddress&); } #include -#include +#include #include #include #include @@ -61,10 +61,11 @@ struct ResolveTask ComboAddress d_ip; // NS name used by DoT probe task, not part of index and not used by operator<() DNSName d_nsname; + Netmask d_netmask; bool operator<(const ResolveTask& a) const { - return std::tie(d_qname, d_qtype, d_refreshMode, d_func, d_ip) < std::tie(a.d_qname, a.d_qtype, a.d_refreshMode, a.d_func, d_ip); + return std::tie(d_qname, d_qtype, d_refreshMode, d_func, d_ip, d_netmask) < std::tie(a.d_qname, a.d_qtype, a.d_refreshMode, a.d_func, a.d_ip, a.d_netmask); } bool run(bool logErrors); @@ -118,13 +119,14 @@ private: typedef multi_index_container< ResolveTask, indexed_by< - hashed_unique, + ordered_unique, composite_key, member, member, member, - member>>, + member, + member>>, sequenced>>> queue_t; diff --git a/pdns/recursordist/test-rec-taskqueue.cc b/pdns/recursordist/test-rec-taskqueue.cc index 8c997e0696..17ad0be38b 100644 --- a/pdns/recursordist/test-rec-taskqueue.cc +++ b/pdns/recursordist/test-rec-taskqueue.cc @@ -14,16 +14,16 @@ BOOST_AUTO_TEST_SUITE(rec_taskqueue) BOOST_AUTO_TEST_CASE(test_almostexpired_queue_no_dups) { taskQueueClear(); - pushAlmostExpiredTask(DNSName("foo"), QType::AAAA, 0); - pushAlmostExpiredTask(DNSName("foo"), QType::AAAA, 0); - pushAlmostExpiredTask(DNSName("foo"), QType::A, 0); + pushAlmostExpiredTask(DNSName("foo"), QType::AAAA, 0, Netmask()); + pushAlmostExpiredTask(DNSName("foo"), QType::AAAA, 0, Netmask()); + pushAlmostExpiredTask(DNSName("foo"), QType::A, 0, Netmask()); BOOST_CHECK_EQUAL(getTaskSize(), 2U); taskQueuePop(); taskQueuePop(); BOOST_CHECK_EQUAL(getTaskSize(), 0U); // AE queue is not rate limited - pushAlmostExpiredTask(DNSName("foo"), QType::A, 0); + pushAlmostExpiredTask(DNSName("foo"), QType::A, 0, Netmask()); BOOST_CHECK_EQUAL(getTaskSize(), 1U); } diff --git a/pdns/syncres.cc b/pdns/syncres.cc index 15c09b62f9..8268da90ee 100644 --- a/pdns/syncres.cc +++ b/pdns/syncres.cc @@ -1802,212 +1802,221 @@ int SyncRes::doResolveNoQNameMinimization(const DNSName &qname, const QType qtyp } int res=0; - // This is a difficult way of expressing "this is a normal query", i.e. not getRootNS. - if(!(d_updatingRootNS && qtype.getCode()==QType::NS && qname.isRoot())) { - if(d_cacheonly) { // very limited OOB support - LWResult lwr; - LOG(prefix<end()) { - if(iter->second.isAuth()) { - ret.clear(); - d_wasOutOfBand = doOOBResolve(qname, qtype, ret, depth, res); - if (fromCache) - *fromCache = d_wasOutOfBand; - return res; - } - else if (considerforwards) { - const vector& servers = iter->second.d_servers; - const ComboAddress remoteIP = servers.front(); - LOG(prefix< nm; - bool chained = false; - // forwardes are "anonymous", so plug in an empty name; some day we might have a fancier config language... - auto resolveRet = asyncresolveWrapper(remoteIP, d_doDNSSEC, qname, authname, qtype.getCode(), false, false, &d_now, nm, &lwr, &chained, DNSName()); - - d_totUsec += lwr.d_usec; - accountAuthLatency(lwr.d_usec, remoteIP.sin4.sin_family); - if (fromCache) - *fromCache = true; - - // filter out the good stuff from lwr.result() - if (resolveRet == LWResult::Result::Success) { - for(const auto& rec : lwr.d_records) { - if(rec.d_place == DNSResourceRecord::ANSWER) - ret.push_back(rec); - } - return 0; + + const int iterations = !d_refresh && MemRecursorCache::s_maxServedStaleExtensions > 0 ? 2 : 1; + for (int loop = 0; loop < iterations; loop++) { + + // First try a regular resolve + const bool serveStale = loop == 1; + + // When we're not on the last iteration, a timeout is not fatal + d_exceptionOnTimeout = loop == iterations - 1; + + // This is a difficult way of expressing "this is a normal query", i.e. not getRootNS. + if(!(d_updatingRootNS && qtype.getCode()==QType::NS && qname.isRoot())) { + if(d_cacheonly) { // very limited OOB support + LWResult lwr; + LOG(prefix<end()) { + if(iter->second.isAuth()) { + ret.clear(); + d_wasOutOfBand = doOOBResolve(qname, qtype, ret, depth, res); + if (fromCache) + *fromCache = d_wasOutOfBand; + return res; } - else { - return RCode::ServFail; + else if (considerforwards) { + const vector& servers = iter->second.d_servers; + const ComboAddress remoteIP = servers.front(); + LOG(prefix< nm; + bool chained = false; + // forwardes are "anonymous", so plug in an empty name; some day we might have a fancier config language... + auto resolveRet = asyncresolveWrapper(remoteIP, d_doDNSSEC, qname, authname, qtype.getCode(), false, false, &d_now, nm, &lwr, &chained, DNSName()); + + d_totUsec += lwr.d_usec; + accountAuthLatency(lwr.d_usec, remoteIP.sin4.sin_family); + if (fromCache) + *fromCache = true; + + // filter out the good stuff from lwr.result() + if (resolveRet == LWResult::Result::Success) { + for(const auto& rec : lwr.d_records) { + if(rec.d_place == DNSResourceRecord::ANSWER) + ret.push_back(rec); + } + return 0; + } + else { + return RCode::ServFail; + } } } } - } - - DNSName authname(qname); - bool wasForwardedOrAuthZone = false; - bool wasAuthZone = false; - bool wasForwardRecurse = false; - domainmap_t::const_iterator iter = getBestAuthZone(&authname); - if(iter != t_sstorage.domainmap->end()) { - const auto& domain = iter->second; - wasForwardedOrAuthZone = true; - - if (domain.isAuth()) { - wasAuthZone = true; - } else if (domain.shouldRecurse()) { - wasForwardRecurse = true; - } - } - /* When we are looking for a DS, we want to the non-CNAME cache check first - because we can actually have a DS (from the parent zone) AND a CNAME (from - the child zone), and what we really want is the DS */ - if (qtype != QType::DS && doCNAMECacheCheck(qname, qtype, ret, depth, res, state, wasAuthZone, wasForwardRecurse)) { // will reroute us if needed - d_wasOutOfBand = wasAuthZone; - // Here we have an issue. If we were prevented from going out to the network (cache-only was set, possibly because we - // are in QM Step0) we might have a CNAME but not the corresponding target. - // It means that we will sometimes go to the next steps when we are in fact done, but that's fine since - // we will get the records from the cache, resulting in a small overhead. - // This might be a real problem if we had a RPZ hit, though, because we do not want the processing to continue, since - // RPZ rules will not be evaluated anymore (we already matched). - const bool stoppedByPolicyHit = d_appliedPolicy.wasHit(); + DNSName authname(qname); + bool wasForwardedOrAuthZone = false; + bool wasAuthZone = false; + bool wasForwardRecurse = false; + domainmap_t::const_iterator iter = getBestAuthZone(&authname); + if(iter != t_sstorage.domainmap->end()) { + const auto& domain = iter->second; + wasForwardedOrAuthZone = true; - if (fromCache && (!d_cacheonly || stoppedByPolicyHit)) { - *fromCache = true; + if (domain.isAuth()) { + wasAuthZone = true; + } else if (domain.shouldRecurse()) { + wasForwardRecurse = true; + } } - /* Apply Post filtering policies */ - if (d_wantsRPZ && !stoppedByPolicyHit) { - auto luaLocal = g_luaconfs.getLocal(); - if (luaLocal->dfe.getPostPolicy(ret, d_discardedPolicies, d_appliedPolicy)) { - mergePolicyTags(d_policyTags, d_appliedPolicy.getTags()); - bool done = false; - handlePolicyHit(prefix, qname, qtype, ret, done, res, depth); - if (done && fromCache) { - *fromCache = true; + /* When we are looking for a DS, we want to the non-CNAME cache check first + because we can actually have a DS (from the parent zone) AND a CNAME (from + the child zone), and what we really want is the DS */ + if (qtype != QType::DS && doCNAMECacheCheck(qname, qtype, ret, depth, res, state, wasAuthZone, wasForwardRecurse, serveStale)) { // will reroute us if needed + d_wasOutOfBand = wasAuthZone; + // Here we have an issue. If we were prevented from going out to the network (cache-only was set, possibly because we + // are in QM Step0) we might have a CNAME but not the corresponding target. + // It means that we will sometimes go to the next steps when we are in fact done, but that's fine since + // we will get the records from the cache, resulting in a small overhead. + // This might be a real problem if we had a RPZ hit, though, because we do not want the processing to continue, since + // RPZ rules will not be evaluated anymore (we already matched). + const bool stoppedByPolicyHit = d_appliedPolicy.wasHit(); + + if (fromCache && (!d_cacheonly || stoppedByPolicyHit)) { + *fromCache = true; + } + /* Apply Post filtering policies */ + + if (d_wantsRPZ && !stoppedByPolicyHit) { + auto luaLocal = g_luaconfs.getLocal(); + if (luaLocal->dfe.getPostPolicy(ret, d_discardedPolicies, d_appliedPolicy)) { + mergePolicyTags(d_policyTags, d_appliedPolicy.getTags()); + bool done = false; + handlePolicyHit(prefix, qname, qtype, ret, done, res, depth); + if (done && fromCache) { + *fromCache = true; + } } } + return res; } - return res; - } - - if (doCacheCheck(qname, authname, wasForwardedOrAuthZone, wasAuthZone, wasForwardRecurse, qtype, ret, depth, res, state)) { - // we done - d_wasOutOfBand = wasAuthZone; - if (fromCache) { - *fromCache = true; - } - - if (d_wantsRPZ && !d_appliedPolicy.wasHit()) { - auto luaLocal = g_luaconfs.getLocal(); - if (luaLocal->dfe.getPostPolicy(ret, d_discardedPolicies, d_appliedPolicy)) { - mergePolicyTags(d_policyTags, d_appliedPolicy.getTags()); - bool done = false; - handlePolicyHit(prefix, qname, qtype, ret, done, res, depth); + if (doCacheCheck(qname, authname, wasForwardedOrAuthZone, wasAuthZone, wasForwardRecurse, qtype, ret, depth, res, state, serveStale)) { + // we done + d_wasOutOfBand = wasAuthZone; + if (fromCache) { + *fromCache = true; } - } - return res; - } - - /* if we have not found a cached DS (or denial of), now is the time to look for a CNAME */ - if (qtype == QType::DS && doCNAMECacheCheck(qname, qtype, ret, depth, res, state, wasAuthZone, wasForwardRecurse)) { // will reroute us if needed - d_wasOutOfBand = wasAuthZone; - // Here we have an issue. If we were prevented from going out to the network (cache-only was set, possibly because we - // are in QM Step0) we might have a CNAME but not the corresponding target. - // It means that we will sometimes go to the next steps when we are in fact done, but that's fine since - // we will get the records from the cache, resulting in a small overhead. - // This might be a real problem if we had a RPZ hit, though, because we do not want the processing to continue, since - // RPZ rules will not be evaluated anymore (we already matched). - const bool stoppedByPolicyHit = d_appliedPolicy.wasHit(); + if (d_wantsRPZ && !d_appliedPolicy.wasHit()) { + auto luaLocal = g_luaconfs.getLocal(); + if (luaLocal->dfe.getPostPolicy(ret, d_discardedPolicies, d_appliedPolicy)) { + mergePolicyTags(d_policyTags, d_appliedPolicy.getTags()); + bool done = false; + handlePolicyHit(prefix, qname, qtype, ret, done, res, depth); + } + } - if (fromCache && (!d_cacheonly || stoppedByPolicyHit)) { - *fromCache = true; + return res; } - /* Apply Post filtering policies */ - if (d_wantsRPZ && !stoppedByPolicyHit) { - auto luaLocal = g_luaconfs.getLocal(); - if (luaLocal->dfe.getPostPolicy(ret, d_discardedPolicies, d_appliedPolicy)) { - mergePolicyTags(d_policyTags, d_appliedPolicy.getTags()); - bool done = false; - handlePolicyHit(prefix, qname, qtype, ret, done, res, depth); - if (done && fromCache) { - *fromCache = true; + /* if we have not found a cached DS (or denial of), now is the time to look for a CNAME */ + if (qtype == QType::DS && doCNAMECacheCheck(qname, qtype, ret, depth, res, state, wasAuthZone, wasForwardRecurse, serveStale)) { // will reroute us if needed + d_wasOutOfBand = wasAuthZone; + // Here we have an issue. If we were prevented from going out to the network (cache-only was set, possibly because we + // are in QM Step0) we might have a CNAME but not the corresponding target. + // It means that we will sometimes go to the next steps when we are in fact done, but that's fine since + // we will get the records from the cache, resulting in a small overhead. + // This might be a real problem if we had a RPZ hit, though, because we do not want the processing to continue, since + // RPZ rules will not be evaluated anymore (we already matched). + const bool stoppedByPolicyHit = d_appliedPolicy.wasHit(); + + if (fromCache && (!d_cacheonly || stoppedByPolicyHit)) { + *fromCache = true; + } + /* Apply Post filtering policies */ + + if (d_wantsRPZ && !stoppedByPolicyHit) { + auto luaLocal = g_luaconfs.getLocal(); + if (luaLocal->dfe.getPostPolicy(ret, d_discardedPolicies, d_appliedPolicy)) { + mergePolicyTags(d_policyTags, d_appliedPolicy.getTags()); + bool done = false; + handlePolicyHit(prefix, qname, qtype, ret, done, res, depth); + if (done && fromCache) { + *fromCache = true; + } } } - } - return res; + return res; + } } - } - - if (d_cacheonly) { - return 0; - } - - LOG(prefix<> fallBack; - { - auto lock = s_savedParentNSSet.lock(); - auto domainData= lock->find(subdomain); - if (domainData != lock->end() && domainData->d_nsAddresses.size() > 0) { - nsset.clear(); - // Build the nsset arg and fallBack data for the fallback doResolveAt() attempt - // Take a copy to be able to release the lock, NsSet is actually a map, go figure - for (const auto& ns : domainData->d_nsAddresses) { - nsset.emplace(ns.first, pair(std::vector(), false)); - fallBack.emplace(ns.first, ns.second); + // the two retries allow getBestNSNamesFromCache&co to reprime the root + // hints, in case they ever go missing + for(int tries=0;tries<2 && nsset.empty();++tries) { + subdomain=getBestNSNamesFromCache(subdomain, qtype, nsset, &flawedNSSet, depth, beenthere); // pass beenthere to both occasions + } + + res = doResolveAt(nsset, subdomain, flawedNSSet, qname, qtype, ret, depth, beenthere, state, stopAtDelegation, nullptr); + + if (res == -1 && s_save_parent_ns_set) { + // It did not work out, lets check if we have a saved parent NS set + map> fallBack; + { + auto lock = s_savedParentNSSet.lock(); + auto domainData= lock->find(subdomain); + if (domainData != lock->end() && domainData->d_nsAddresses.size() > 0) { + nsset.clear(); + // Build the nsset arg and fallBack data for the fallback doResolveAt() attempt + // Take a copy to be able to release the lock, NsSet is actually a map, go figure + for (const auto& ns : domainData->d_nsAddresses) { + nsset.emplace(ns.first, pair(std::vector(), false)); + fallBack.emplace(ns.first, ns.second); + } + } + } + if (fallBack.size() > 0) { + LOG(prefix<inc(subdomain); } } } - if (fallBack.size() > 0) { - LOG(prefix<inc(subdomain); + /* Apply Post filtering policies */ + if (d_wantsRPZ && !d_appliedPolicy.wasHit()) { + auto luaLocal = g_luaconfs.getLocal(); + if (luaLocal->dfe.getPostPolicy(ret, d_discardedPolicies, d_appliedPolicy)) { + mergePolicyTags(d_policyTags, d_appliedPolicy.getTags()); + bool done = false; + handlePolicyHit(prefix, qname, qtype, ret, done, res, depth); } } - } - /* Apply Post filtering policies */ - if (d_wantsRPZ && !d_appliedPolicy.wasHit()) { - auto luaLocal = g_luaconfs.getLocal(); - if (luaLocal->dfe.getPostPolicy(ret, d_discardedPolicies, d_appliedPolicy)) { - mergePolicyTags(d_policyTags, d_appliedPolicy.getTags()); - bool done = false; - handlePolicyHit(prefix, qname, qtype, ret, done, res, depth); + + if (!res) { + return 0; } - } - if (!res) { - return 0; + LOG(prefix<& recor return false; } -bool SyncRes::doCNAMECacheCheck(const DNSName &qname, const QType qtype, vector& ret, unsigned int depth, int &res, vState& state, bool wasAuthZone, bool wasForwardRecurse) +bool SyncRes::doCNAMECacheCheck(const DNSName &qname, const QType qtype, vector& ret, unsigned int depth, int &res, vState& state, bool wasAuthZone, bool wasForwardRecurse, bool serveStale) { string prefix; if(doLog()) { @@ -2430,6 +2439,9 @@ bool SyncRes::doCNAMECacheCheck(const DNSName &qname, const QType qtype, vector< if (d_refresh) { flags |= MemRecursorCache::Refresh; } + if (serveStale) { + flags |= MemRecursorCache::ServeStale; + } if (g_recCache->get(d_now.tv_sec, qname, QType::CNAME, flags, &cset, d_cacheRemote, d_routingTag, d_doDNSSEC ? &signatures : nullptr, d_doDNSSEC ? &authorityRecs : nullptr, &d_wasVariable, &state, &wasAuth, &authZone, &d_fromAuthIP) > 0) { foundName = qname; foundQT = QType::CNAME; @@ -2720,7 +2732,7 @@ void SyncRes::computeNegCacheValidationStatus(const NegCache::NegCacheEntry& ne, } } -bool SyncRes::doCacheCheck(const DNSName &qname, const DNSName& authname, bool wasForwardedOrAuthZone, bool wasAuthZone, bool wasForwardRecurse, QType qtype, vector&ret, unsigned int depth, int &res, vState& state) +bool SyncRes::doCacheCheck(const DNSName &qname, const DNSName& authname, bool wasForwardedOrAuthZone, bool wasAuthZone, bool wasForwardRecurse, QType qtype, vector&ret, unsigned int depth, int &res, vState& state, bool serveStale) { bool giveNegative=false; @@ -2838,7 +2850,9 @@ bool SyncRes::doCacheCheck(const DNSName &qname, const DNSName& authname, bool w if (d_refresh) { flags |= MemRecursorCache::Refresh; } - + if (serveStale) { + flags |= MemRecursorCache::ServeStale; + } if(g_recCache->get(d_now.tv_sec, sqname, sqt, flags, &cset, d_cacheRemote, d_routingTag, d_doDNSSEC ? &signatures : nullptr, d_doDNSSEC ? &authorityRecs : nullptr, &d_wasVariable, &cachedState, &wasCachedAuth, nullptr, &d_fromAuthIP) > 0) { LOG(prefix< s_maxtotusec) { - throw ImmediateServFailException("Too much time waiting for "+qname.toLogString()+"|"+qtype.toString()+", timeouts: "+std::to_string(d_timeouts) +", throttles: "+std::to_string(d_throttledqueries) + ", queries: "+std::to_string(d_outqueries)+", "+std::to_string(d_totUsec/1000)+"msec"); + if (d_exceptionOnTimeout) { + throw ImmediateServFailException("Too much time waiting for "+qname.toLogString()+"|"+qtype.toString()+", timeouts: "+std::to_string(d_timeouts) +", throttles: "+std::to_string(d_throttledqueries) + ", queries: "+std::to_string(d_outqueries)+", "+std::to_string(d_totUsec/1000)+"msec"); + } else { + return false; + } } if(doTCP) { @@ -5689,6 +5707,16 @@ int SyncRes::doResolveAt(NsSet &nameservers, DNSName auth, bool flawedNSSet, con return -1; } +void SyncRes::setQuerySource(const Netmask& netmask) +{ + if (!netmask.empty()) { + d_outgoingECSNetwork = netmask; + } + else { + d_outgoingECSNetwork = boost::none; + } +} + void SyncRes::setQuerySource(const ComboAddress& requestor, boost::optional incomingECS) { d_requestor = requestor; diff --git a/pdns/syncres.hh b/pdns/syncres.hh index fd18d37a8a..ef1575c198 100644 --- a/pdns/syncres.hh +++ b/pdns/syncres.hh @@ -392,6 +392,7 @@ public: } void setQuerySource(const ComboAddress& requestor, boost::optional incomingECS); + void setQuerySource(const Netmask& netmask); void setInitialRequestId(boost::optional initialRequestId) { @@ -561,8 +562,8 @@ private: bool isRecursiveForwardOrAuth(const DNSName &qname) const; bool isForwardOrAuth(const DNSName &qname) const; domainmap_t::const_iterator getBestAuthZone(DNSName* qname) const; - bool doCNAMECacheCheck(const DNSName &qname, QType qtype, vector&ret, unsigned int depth, int &res, vState& state, bool wasAuthZone, bool wasForwardRecurse); - bool doCacheCheck(const DNSName &qname, const DNSName& authname, bool wasForwardedOrAuthZone, bool wasAuthZone, bool wasForwardRecurse, QType qtype, vector&ret, unsigned int depth, int &res, vState& state); + bool doCNAMECacheCheck(const DNSName &qname, QType qtype, vector&ret, unsigned int depth, int &res, vState& state, bool wasAuthZone, bool wasForwardRecurse, bool serveStale); + bool doCacheCheck(const DNSName &qname, const DNSName& authname, bool wasForwardedOrAuthZone, bool wasAuthZone, bool wasForwardRecurse, QType qtype, vector&ret, unsigned int depth, int &res, vState& state, bool serveStale); void getBestNSFromCache(const DNSName &qname, QType qtype, vector&bestns, bool* flawedNSSet, unsigned int depth, set& beenthere, const boost::optional& cutOffDomain = boost::none); DNSName getBestNSNamesFromCache(const DNSName &qname, QType qtype, NsSet& nsset, bool* flawedNSSet, unsigned int depth, set&beenthere); @@ -646,7 +647,8 @@ private: bool d_queryReceivedOverTCP{false}; bool d_followCNAME{true}; bool d_refresh{false}; - + bool d_exceptionOnTimeout{true}; + LogMode d_lm; };