From 157647ec045d5c666d2cef37fa4593d37542ab72 Mon Sep 17 00:00:00 2001 From: Pieter Lexis Date: Tue, 1 Oct 2019 12:25:58 +0200 Subject: [PATCH] Implement RFC 8020 This commit implements the "NXDOMAIN: There Really Is Nothing Underneath". When enabled (the default), the SyncRes will check the negative cache if there exists a higher denied name and uses that data to send an NXDOMAIN to the client. In essence, it is a more aggressive version of root-nx-trust (which could be removed in the future). There are several advantages: * We potentially send fewer queries to the internet * The record cache is not "polluted" with useless NXDOMAINs --- pdns/pdns_recursor.cc | 2 + pdns/recursordist/docs/settings.rst | 15 ++ pdns/recursordist/test-syncres_cc.cc | 1 + pdns/recursordist/test-syncres_cc2.cc | 239 ++++++++++++++++++++++++++ pdns/syncres.cc | 29 +++- pdns/syncres.hh | 1 + 6 files changed, 280 insertions(+), 7 deletions(-) diff --git a/pdns/pdns_recursor.cc b/pdns/pdns_recursor.cc index 3637d11ae..84f7669e7 100644 --- a/pdns/pdns_recursor.cc +++ b/pdns/pdns_recursor.cc @@ -3918,6 +3918,7 @@ static int serviceMain(int argc, char*argv[]) SyncRes::s_ecscachelimitttl = ::arg().asNum("ecs-cache-limit-ttl"); SyncRes::s_qnameminimization = ::arg().mustDo("qname-minimization"); + SyncRes::s_hardenNXD = ::arg().mustDo("nothing-below-nxdomain"); if (!::arg().isEmpty("ecs-scope-zero-address")) { ComboAddress scopeZero(::arg()["ecs-scope-zero-address"]); @@ -4647,6 +4648,7 @@ int main(int argc, char **argv) ::arg().set("public-suffix-list-file", "Path to the Public Suffix List file, if any")=""; ::arg().set("distribution-load-factor", "The load factor used when PowerDNS is distributing queries to worker threads")="0.0"; ::arg().setSwitch("qname-minimization", "Use Query Name Minimization")="no"; + ::arg().setSwitch("nothing-below-nxdomain", "When an NXDOMAIN exists in cache for a name with fewer labels than the qname, send NXDOMAIN without doing a lookup (see RFC 8020)")="yes"; #ifdef NOD_ENABLED ::arg().set("new-domain-tracking", "Track newly observed domains (i.e. never seen before).")="no"; ::arg().set("new-domain-log", "Log newly observed domains.")="yes"; diff --git a/pdns/recursordist/docs/settings.rst b/pdns/recursordist/docs/settings.rst index d7c9f4ac1..11e4a40a2 100644 --- a/pdns/recursordist/docs/settings.rst +++ b/pdns/recursordist/docs/settings.rst @@ -1106,6 +1106,21 @@ a new domain is observed. Number of milliseconds to wait for a remote authoritative server to respond. +.. _setting-nothing-below-nxdomain: + +``nothing-below-nxdomain`` +-------------------------- +.. versionadded:: 4.3.0 + +- Boolean +- Default: true + +Enables :rfc:`8020` handling of cached NXDOMAIN responses. +This RFC specifies that NXDOMAIN means that the DNS tree under the denied name MUST be empty. +When an NXDOMAIN exists in the cache for a shorter name than the qname, no lookup is done and an NXDOMAIN is sent to the client. + +For instance, when ``foo.example.net`` is negatively cached, any query matching ``*.foo.example.net`` will be answered with NXDOMAIN directly without consulting authoritative servers. + .. _setting-nsec3-max-iterations: ``nsec3-max-iterations`` diff --git a/pdns/recursordist/test-syncres_cc.cc b/pdns/recursordist/test-syncres_cc.cc index 33e72921a..d74197807 100644 --- a/pdns/recursordist/test-syncres_cc.cc +++ b/pdns/recursordist/test-syncres_cc.cc @@ -125,6 +125,7 @@ void initSR(bool debug) SyncRes::s_ecsipv6cachelimit = 56; SyncRes::s_ecscachelimitttl = 0; SyncRes::s_rootNXTrust = true; + SyncRes::s_hardenNXD = true; SyncRes::s_minimumTTL = 0; SyncRes::s_minimumECSTTL = 0; SyncRes::s_serverID = "PowerDNS Unit Tests Server ID"; diff --git a/pdns/recursordist/test-syncres_cc2.cc b/pdns/recursordist/test-syncres_cc2.cc index 91f5e17cd..e42a45442 100644 --- a/pdns/recursordist/test-syncres_cc2.cc +++ b/pdns/recursordist/test-syncres_cc2.cc @@ -415,6 +415,245 @@ BOOST_AUTO_TEST_CASE(test_root_nx_dont_trust) { BOOST_CHECK_EQUAL(queriesCount, 3U); } +BOOST_AUTO_TEST_CASE(test_rfc8020_nothing_underneath) { + std::unique_ptr sr; + initSR(sr); + + primeHints(); + + const DNSName target1("www.powerdns.com."); // will be denied + const DNSName target2("foo.www.powerdns.com."); + const DNSName target3("bar.www.powerdns.com."); + const DNSName target4("quux.bar.www.powerdns.com."); + const ComboAddress ns("192.0.2.1:53"); + size_t queriesCount = 0; + + sr->setAsyncCallback([ns, &queriesCount](const ComboAddress& ip, const DNSName& domain, int type, bool doTCP, bool sendRDQuery, int EDNS0Level, struct timeval* now, boost::optional& srcmask, boost::optional context, LWResult* res, bool* chained) { + + queriesCount++; + + if (isRootServer(ip)) { + setLWResult(res, 0, false, false, true); + addRecordToLW(res, "powerdns.com.", QType::NS, "ns1.powerdns.com.", DNSResourceRecord::AUTHORITY, 172800); + addRecordToLW(res, "ns1.powerdns.com.", QType::A, ns.toString(), DNSResourceRecord::ADDITIONAL, 3600); + return 1; + } else if (ip == ns) { + setLWResult(res, RCode::NXDomain, true, false, false); + addRecordToLW(res, "powerdns.com.", QType::SOA, "ns1.powerdns.com. hostmaster.powerdns.com. 2017032800 1800 900 604800 86400", DNSResourceRecord::AUTHORITY, 86400); + return 1; + } + return 0; + }); + + vector ret; + int res = sr->beginResolve(target1, QType(QType::A), QClass::IN, ret); + BOOST_CHECK_EQUAL(res, RCode::NXDomain); + BOOST_CHECK_EQUAL(ret.size(), 1); + BOOST_CHECK_EQUAL(queriesCount, 2); + BOOST_CHECK_EQUAL(SyncRes::getNegCacheSize(), 1); + + ret.clear(); + res = sr->beginResolve(target2, QType(QType::A), QClass::IN, ret); + BOOST_CHECK_EQUAL(res, RCode::NXDomain); + BOOST_CHECK_EQUAL(ret.size(), 1); + BOOST_CHECK_EQUAL(queriesCount, 2); + BOOST_CHECK_EQUAL(SyncRes::getNegCacheSize(), 1); + + ret.clear(); + res = sr->beginResolve(target3, QType(QType::A), QClass::IN, ret); + BOOST_CHECK_EQUAL(res, RCode::NXDomain); + BOOST_CHECK_EQUAL(ret.size(), 1); + BOOST_CHECK_EQUAL(queriesCount, 2); + BOOST_CHECK_EQUAL(SyncRes::getNegCacheSize(), 1); + + ret.clear(); + res = sr->beginResolve(target4, QType(QType::A), QClass::IN, ret); + BOOST_CHECK_EQUAL(res, RCode::NXDomain); + BOOST_CHECK_EQUAL(ret.size(), 1); + BOOST_CHECK_EQUAL(queriesCount, 2); + BOOST_CHECK_EQUAL(SyncRes::getNegCacheSize(), 1); + + // Now test without RFC 8020 to see the cache and query count grow + SyncRes::s_hardenNXD = false; + + // Already cached + ret.clear(); + res = sr->beginResolve(target1, QType(QType::A), QClass::IN, ret); + BOOST_CHECK_EQUAL(res, RCode::NXDomain); + BOOST_CHECK_EQUAL(ret.size(), 1); + BOOST_CHECK_EQUAL(queriesCount, 2); + BOOST_CHECK_EQUAL(SyncRes::getNegCacheSize(), 1); + + // New query + ret.clear(); + res = sr->beginResolve(target2, QType(QType::A), QClass::IN, ret); + BOOST_CHECK_EQUAL(res, RCode::NXDomain); + BOOST_CHECK_EQUAL(ret.size(), 1); + BOOST_CHECK_EQUAL(queriesCount, 3); + BOOST_CHECK_EQUAL(SyncRes::getNegCacheSize(), 2); + + ret.clear(); + res = sr->beginResolve(target3, QType(QType::A), QClass::IN, ret); + BOOST_CHECK_EQUAL(res, RCode::NXDomain); + BOOST_CHECK_EQUAL(ret.size(), 1); + BOOST_CHECK_EQUAL(queriesCount, 4); + BOOST_CHECK_EQUAL(SyncRes::getNegCacheSize(), 3); + + ret.clear(); + res = sr->beginResolve(target4, QType(QType::A), QClass::IN, ret); + BOOST_CHECK_EQUAL(res, RCode::NXDomain); + BOOST_CHECK_EQUAL(ret.size(), 1); + BOOST_CHECK_EQUAL(queriesCount, 5); + BOOST_CHECK_EQUAL(SyncRes::getNegCacheSize(), 4); + + // reset + SyncRes::s_hardenNXD = true; +} + +BOOST_AUTO_TEST_CASE(test_rfc8020_nodata) { + std::unique_ptr sr; + initSR(sr); + + primeHints(); + + const DNSName target1("www.powerdns.com."); // TXT record will be denied + const DNSName target2("bar.www.powerdns.com."); // will be NXD, but the www. NODATA should not interfere with 8020 processing + const DNSName target3("quux.bar.www.powerdns.com."); // will be NXD, but will not yield a query + const ComboAddress ns("192.0.2.1:53"); + size_t queriesCount = 0; + + sr->setAsyncCallback([ns, target1, target2, target3, &queriesCount](const ComboAddress& ip, const DNSName& domain, int type, bool doTCP, bool sendRDQuery, int EDNS0Level, struct timeval* now, boost::optional& srcmask, boost::optional context, LWResult* res, bool* chained) { + + queriesCount++; + + if (isRootServer(ip)) { + setLWResult(res, 0, false, false, true); + addRecordToLW(res, "powerdns.com.", QType::NS, "ns1.powerdns.com.", DNSResourceRecord::AUTHORITY, 172800); + addRecordToLW(res, "ns1.powerdns.com.", QType::A, ns.toString(), DNSResourceRecord::ADDITIONAL, 3600); + return 1; + } else if (ip == ns) { + if (domain == target1) { // NODATA for TXT, NOERROR for A + if (type == QType::TXT) { + setLWResult(res, RCode::NoError, true); + addRecordToLW(res, "powerdns.com.", QType::SOA, "ns1.powerdns.com. hostmaster.powerdns.com. 2017032800 1800 900 604800 86400", DNSResourceRecord::AUTHORITY, 86400); + return 1; + } + if (type == QType::A) { + setLWResult(res, RCode::NoError, true); + addRecordToLW(res, domain, QType::A, "192.0.2.1", DNSResourceRecord::ANSWER, 86400); + return 1; + } + } + if (domain == target2 || domain == target3) { + setLWResult(res, RCode::NXDomain, true); + addRecordToLW(res, "powerdns.com.", QType::SOA, "ns1.powerdns.com. hostmaster.powerdns.com. 2017032800 1800 900 604800 86400", DNSResourceRecord::AUTHORITY, 86400); + return 1; + } + } + return 0; + }); + + vector ret; + int res = sr->beginResolve(target1, QType(QType::TXT), QClass::IN, ret); + BOOST_CHECK_EQUAL(res, RCode::NoError); + BOOST_CHECK_EQUAL(ret.size(), 1); + BOOST_CHECK_EQUAL(queriesCount, 2); + BOOST_CHECK_EQUAL(SyncRes::getNegCacheSize(), 1); + + ret.clear(); + res = sr->beginResolve(target1, QType(QType::A), QClass::IN, ret); + BOOST_CHECK_EQUAL(res, RCode::NoError); + BOOST_CHECK_EQUAL(ret.size(), 1); + BOOST_CHECK_EQUAL(queriesCount, 3); + BOOST_CHECK_EQUAL(SyncRes::getNegCacheSize(), 1); + + ret.clear(); + res = sr->beginResolve(target2, QType(QType::A), QClass::IN, ret); + BOOST_CHECK_EQUAL(res, RCode::NXDomain); + BOOST_CHECK_EQUAL(ret.size(), 1); + BOOST_CHECK_EQUAL(queriesCount, 4); + BOOST_CHECK_EQUAL(SyncRes::getNegCacheSize(), 2); + + ret.clear(); + res = sr->beginResolve(target3, QType(QType::A), QClass::IN, ret); + BOOST_CHECK_EQUAL(res, RCode::NXDomain); + BOOST_CHECK_EQUAL(ret.size(), 1); + BOOST_CHECK_EQUAL(queriesCount, 4); + BOOST_CHECK_EQUAL(SyncRes::getNegCacheSize(), 2); +} + +BOOST_AUTO_TEST_CASE(test_rfc8020_nodata_bis) { + std::unique_ptr sr; + initSR(sr); + + primeHints(); + + const DNSName target1("www.powerdns.com."); // TXT record will be denied + const DNSName target2("bar.www.powerdns.com."); // will be NXD, but the www. NODATA should not interfere with 8020 processing + const DNSName target3("quux.bar.www.powerdns.com."); // will be NXD, but will not yield a query + const ComboAddress ns("192.0.2.1:53"); + size_t queriesCount = 0; + + sr->setAsyncCallback([ns, target1, target2, target3, &queriesCount](const ComboAddress& ip, const DNSName& domain, int type, bool doTCP, bool sendRDQuery, int EDNS0Level, struct timeval* now, boost::optional& srcmask, boost::optional context, LWResult* res, bool* chained) { + + queriesCount++; + + if (isRootServer(ip)) { + setLWResult(res, 0, false, false, true); + addRecordToLW(res, "powerdns.com.", QType::NS, "ns1.powerdns.com.", DNSResourceRecord::AUTHORITY, 172800); + addRecordToLW(res, "ns1.powerdns.com.", QType::A, ns.toString(), DNSResourceRecord::ADDITIONAL, 3600); + return 1; + } else if (ip == ns) { + if (domain == target1) { // NODATA for TXT, NOERROR for A + if (type == QType::TXT) { + setLWResult(res, RCode::NoError, true); + addRecordToLW(res, "powerdns.com.", QType::SOA, "ns1.powerdns.com. hostmaster.powerdns.com. 2017032800 1800 900 604800 86400", DNSResourceRecord::AUTHORITY, 86400); + return 1; + } + if (type == QType::A) { + setLWResult(res, RCode::NoError, true); + addRecordToLW(res, domain, QType::A, "192.0.2.1", DNSResourceRecord::ANSWER, 86400); + return 1; + } + } + if (domain == target2 || domain == target3) { + setLWResult(res, RCode::NXDomain, true); + addRecordToLW(res, "powerdns.com.", QType::SOA, "ns1.powerdns.com. hostmaster.powerdns.com. 2017032800 1800 900 604800 86400", DNSResourceRecord::AUTHORITY, 86400); + return 1; + } + } + return 0; + }); + + vector ret; + int res = sr->beginResolve(target1, QType(QType::TXT), QClass::IN, ret); + BOOST_CHECK_EQUAL(res, RCode::NoError); + BOOST_CHECK_EQUAL(ret.size(), 1); + BOOST_CHECK_EQUAL(queriesCount, 2); + BOOST_CHECK_EQUAL(SyncRes::getNegCacheSize(), 1); + + ret.clear(); + res = sr->beginResolve(target1, QType(QType::A), QClass::IN, ret); + BOOST_CHECK_EQUAL(res, RCode::NoError); + BOOST_CHECK_EQUAL(ret.size(), 1); + BOOST_CHECK_EQUAL(queriesCount, 3); + BOOST_CHECK_EQUAL(SyncRes::getNegCacheSize(), 1); + + ret.clear(); + res = sr->beginResolve(target2, QType(QType::TXT), QClass::IN, ret); + BOOST_CHECK_EQUAL(res, RCode::NXDomain); + BOOST_CHECK_EQUAL(ret.size(), 1); + BOOST_CHECK_EQUAL(queriesCount, 4); + BOOST_CHECK_EQUAL(SyncRes::getNegCacheSize(), 2); + + ret.clear(); + res = sr->beginResolve(target3, QType(QType::TXT), QClass::IN, ret); + BOOST_CHECK_EQUAL(res, RCode::NXDomain); + BOOST_CHECK_EQUAL(ret.size(), 1); + BOOST_CHECK_EQUAL(queriesCount, 4); + BOOST_CHECK_EQUAL(SyncRes::getNegCacheSize(), 2); +} + BOOST_AUTO_TEST_CASE(test_skip_negcache_for_variable_response) { std::unique_ptr sr; initSR(sr); diff --git a/pdns/syncres.cc b/pdns/syncres.cc index 1fd25ad55..8419150aa 100644 --- a/pdns/syncres.cc +++ b/pdns/syncres.cc @@ -88,6 +88,7 @@ bool SyncRes::s_nopacketcache; bool SyncRes::s_rootNXTrust; bool SyncRes::s_noEDNS; bool SyncRes::s_qnameminimization; +bool SyncRes::s_hardenNXD; #define LOG(x) if(d_lm == Log) { g_log <d_auth.isRoot() && !(wasForwardedOrAuthZone && !authname.isRoot())) { // when forwarding, the root may only neg-cache if it was forwarded to. sttl = ne->d_ttd - d_now.tv_sec; @@ -1395,25 +1396,39 @@ bool SyncRes::doCacheCheck(const DNSName &qname, const DNSName& authname, bool w res = RCode::NXDomain; giveNegative = true; cachedState = ne->d_validationState; - } - else if (t_sstorage.negcache.get(qname, qtype, d_now, &ne)) { + } else if (t_sstorage.negcache.get(qname, qtype, d_now, &ne)) { /* If we are looking for a DS, discard NXD if auth == qname and ask for a specific denial instead */ if (qtype != QType::DS || ne->d_qtype.getCode() || ne->d_auth != qname || t_sstorage.negcache.get(qname, qtype, d_now, &ne, true)) { - res = 0; + res = RCode::NXDomain; sttl = ne->d_ttd - d_now.tv_sec; giveNegative = true; cachedState = ne->d_validationState; - if(ne->d_qtype.getCode()) { + if (ne->d_qtype.getCode()) { LOG(prefix<d_auth<<"' for another "<d_auth<<"' for another "<d_auth<<"' for another "<d_ttd - d_now.tv_sec; + giveNegative = true; + cachedState = ne->d_validationState; + LOG(prefix<d_auth<<"' for another "< d_discardedPolicies; DNSFilterEngine::Policy d_appliedPolicy; -- 2.40.0