]> granicus.if.org Git - pdns/commitdiff
Implement RFC 8020
authorPieter Lexis <pieter.lexis@powerdns.com>
Tue, 1 Oct 2019 10:25:58 +0000 (12:25 +0200)
committerPieter Lexis <pieter.lexis@powerdns.com>
Mon, 21 Oct 2019 08:57:26 +0000 (10:57 +0200)
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
pdns/recursordist/docs/settings.rst
pdns/recursordist/test-syncres_cc.cc
pdns/recursordist/test-syncres_cc2.cc
pdns/syncres.cc
pdns/syncres.hh

index 3637d11ae707b4081f329d12d4b294195d77f93f..84f7669e77d0e4c79883ffca64655245867d7007 100644 (file)
@@ -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";
index d7c9f4ac176ed0ce32eeb3211e65d8f4aaa5a4ee..11e4a40a2d21942f55e864bb5639e10f9053eca0 100644 (file)
@@ -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``
index 33e72921aa9c54e71512566ee99747e656fdcd07..d74197807893aa7786b9670a4ad591cf0451bac9 100644 (file)
@@ -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";
index 91f5e17cdd155e5863007dee506278fd57023458..e42a45442e971878e56d6e71cec1107aa9265755 100644 (file)
@@ -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<SyncRes> 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<Netmask>& srcmask, boost::optional<const ResolveContext&> 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<DNSRecord> 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<SyncRes> 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<Netmask>& srcmask, boost::optional<const ResolveContext&> 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<DNSRecord> 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<SyncRes> 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<Netmask>& srcmask, boost::optional<const ResolveContext&> 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<DNSRecord> 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<SyncRes> sr;
   initSR(sr);
index 1fd25ad5529c6c1fa28c1141106e93656fe09fe4..8419150aa19db196c39338812c4502bcc923166d 100644 (file)
@@ -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 <<Logger::Warning << x; } else if(d_lm == Store) { d_trace << x; }
 
@@ -1387,7 +1388,7 @@ bool SyncRes::doCacheCheck(const DNSName &qname, const DNSName& authname, bool w
   const NegCache::NegCacheEntry* ne = nullptr;
 
   if(s_rootNXTrust &&
-     t_sstorage.negcache.getRootNXTrust(qname, d_now, &ne) &&
+      t_sstorage.negcache.getRootNXTrust(qname, d_now, &ne) &&
       ne->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<<qname<<": "<<qtype.getName()<<" is negatively cached via '"<<ne->d_auth<<"' for another "<<sttl<<" seconds"<<endl);
         res = RCode::NoError;
+      } else {
+        LOG(prefix<<qname<<": Entire name '"<<qname<<" is negatively cached via '"<<ne->d_auth<<"' for another "<<sttl<<" seconds"<<endl);
       }
-      else {
-        LOG(prefix<<qname<<": Entire name '"<<qname<<"', is negatively cached via '"<<ne->d_auth<<"' for another "<<sttl<<" seconds"<<endl);
+    }
+  } else if (s_hardenNXD && !qname.isRoot() && !wasForwardedOrAuthZone) {
+    auto labels = qname.getRawLabels();
+    DNSName negCacheName(g_rootdnsname);
+    negCacheName.prependRawLabel(labels.back());
+    labels.pop_back();
+    while(!labels.empty()) {
+      if (t_sstorage.negcache.get(negCacheName, QType(0), d_now, &ne, true)) {
         res = RCode::NXDomain;
+        sttl = ne->d_ttd - d_now.tv_sec;
+        giveNegative = true;
+        cachedState = ne->d_validationState;
+        LOG(prefix<<qname<<": Name '"<<negCacheName<<"' and below, is negatively cached via '"<<ne->d_auth<<"' for another "<<sttl<<" seconds"<<endl);
+        break;
       }
+      negCacheName.prependRawLabel(labels.back());
+      labels.pop_back();
     }
   }
 
index 728a067bb43e250fc86076ddbd3b90a779b813a7..9efa001fdf9e16acd74babd3287186e0bbcba7ce 100644 (file)
@@ -752,6 +752,7 @@ public:
   static bool s_rootNXTrust;
   static bool s_nopacketcache;
   static bool s_qnameminimization;
+  static bool s_hardenNXD;
 
   std::unordered_map<std::string,bool> d_discardedPolicies;
   DNSFilterEngine::Policy d_appliedPolicy;