]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
rec: Synthesize wildcard answers from the aggressive NSEC cache
authorRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 4 Jan 2021 17:43:12 +0000 (18:43 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 22 Feb 2021 17:43:05 +0000 (18:43 +0100)
pdns/recursordist/aggressive_nsec.cc
pdns/recursordist/aggressive_nsec.hh
pdns/recursordist/test-aggressive_nsec_cc.cc
pdns/recursordist/test-syncres_cc.cc
pdns/syncres.cc

index 325daadd78286dfb5a621fbd978e503ddbe76cc1..0f57213714cffd93b0fea39f00af749b12fbf2c1 100644 (file)
@@ -23,6 +23,7 @@
 #include "aggressive_nsec.hh"
 #include "cachecleaner.hh"
 #include "recursor_cache.hh"
+#include "logger.hh"
 #include "validate.hh"
 
 std::unique_ptr<AggressiveNSECCache> g_aggressiveNSECCache{nullptr};
@@ -136,7 +137,7 @@ bool AggressiveNSECCache::getNSECBefore(time_t now, std::shared_ptr<AggressiveNS
     return false;
   }
 
-#if 1
+#if 0
   cerr<<"We have:"<<endl;
   for (const auto& ent : zoneEntry->d_entries) {
     cerr<<"- "<<ent.d_owner<<" -> "<<ent.d_next<<endl;
@@ -149,7 +150,6 @@ bool AggressiveNSECCache::getNSECBefore(time_t now, std::shared_ptr<AggressiveNS
   bool wrapped = false;
 
   if (it == idx.begin() && it->d_owner != name) {
-    //cerr<<"the lower bound is already the first entry, let's if the end is a wrap"<<endl;
     it = idx.end();
     // we know the map is not empty
     it--;
@@ -166,19 +166,14 @@ bool AggressiveNSECCache::getNSECBefore(time_t now, std::shared_ptr<AggressiveNS
     }
     else {
       it--;
-      // cerr<<"looping with "<<it->d_owner<<endl;
     }
   }
 
   if (end) {
-    //cerr<<"nothing left"<<endl;
     return false;
   }
 
-  //cerr<<"considering "<<it->d_owner<<" "<<it->d_next<<endl;
-
   if (it->d_ttd <= now) {
-    //cerr<<"not using it"<<endl;
     moveCacheItemToFront<ZoneEntry::SequencedTag>(zoneEntry->d_entries, it);
     return false;
   }
@@ -218,7 +213,7 @@ bool AggressiveNSECCache::getNSEC3(time_t now, std::shared_ptr<AggressiveNSECCac
   return false;
 }
 
-static void addToRRSet(const time_t now, std::vector<DNSRecord>& recordSet, std::vector<std::shared_ptr<RRSIGRecordContent>> signatures, const DNSName& owner, bool doDNSSEC, std::vector<DNSRecord>& ret)
+static void addToRRSet(const time_t now, std::vector<DNSRecord>& recordSet, std::vector<std::shared_ptr<RRSIGRecordContent>> signatures, const DNSName& owner, bool doDNSSEC, std::vector<DNSRecord>& ret, DNSResourceRecord::Place place=DNSResourceRecord::AUTHORITY)
 {
   uint32_t ttl = 0;
 
@@ -228,8 +223,9 @@ static void addToRRSet(const time_t now, std::vector<DNSRecord>& recordSet, std:
     }
 
     record.d_ttl -= now;
+    record.d_name = owner;
     ttl = record.d_ttl;
-    record.d_place = DNSResourceRecord::AUTHORITY;
+    record.d_place = place;
     ret.push_back(std::move(record));
   }
 
@@ -240,7 +236,7 @@ static void addToRRSet(const time_t now, std::vector<DNSRecord>& recordSet, std:
       dr.d_name = owner;
       dr.d_ttl = ttl;
       dr.d_content = std::move(signature);
-      dr.d_place = DNSResourceRecord::AUTHORITY;
+      dr.d_place = place;
       dr.d_class = QClass::IN;
       ret.push_back(std::move(dr));
     }
@@ -272,6 +268,48 @@ static void addRecordToRRSet(time_t now, const DNSName& owner, const QType& type
   }
 }
 
+#define LOG(x) if (g_dnssecLOG) { g_log <<Logger::Warning << x; }
+
+bool AggressiveNSECCache::synthesizeFromNSEC3Wildcard(time_t now, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC, ZoneEntry::CacheEntry& nextCloser, const DNSName& wildcardName)
+{
+  vState cachedState;
+
+  std::vector<DNSRecord> wcSet;
+  std::vector<std::shared_ptr<RRSIGRecordContent>> wcSignatures;
+
+  if (g_recCache->get(now, wildcardName, type, true, &wcSet, ComboAddress("127.0.0.1"), false, boost::none, doDNSSEC ? &wcSignatures : nullptr, nullptr, nullptr, &cachedState) <= 0 || cachedState != vState::Secure) {
+    LOG("Unfortunately we don't have a valid entry for "<<wildcardName<<", so we cannot synthesize from that wildcard"<<endl);
+    return false;
+  }
+
+  addToRRSet(now, wcSet, wcSignatures, name, doDNSSEC, ret, DNSResourceRecord::ANSWER);
+  /* no need for closest encloser proof, the wildcard is there */
+  addRecordToRRSet(now, nextCloser.d_owner, QType::NSEC3, nextCloser.d_ttd - now, nextCloser.d_record, nextCloser.d_signatures, doDNSSEC, ret);
+  /* and of course we won't deny the wildcard either */
+
+  LOG("Synthesized valid answer from NSEC3s and wildcard!"<<endl);
+  return true;
+}
+
+bool AggressiveNSECCache::synthesizeFromNSECWildcard(time_t now, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC, ZoneEntry::CacheEntry& nsec, const DNSName& wildcardName)
+{
+  vState cachedState;
+
+  std::vector<DNSRecord> wcSet;
+  std::vector<std::shared_ptr<RRSIGRecordContent>> wcSignatures;
+
+  if (g_recCache->get(now, wildcardName, type, true, &wcSet, ComboAddress("127.0.0.1"), false, boost::none, doDNSSEC ? &wcSignatures : nullptr, nullptr, nullptr, &cachedState) <= 0 || cachedState != vState::Secure) {
+    LOG("Unfortunately we don't have a valid entry for "<<wildcardName<<", so we cannot synthesize from that wildcard"<<endl);
+    return false;
+  }
+
+  addToRRSet(now, wcSet, wcSignatures, name, doDNSSEC, ret, DNSResourceRecord::ANSWER);
+  addRecordToRRSet(now, nsec.d_owner, QType::NSEC3, nsec.d_ttd - now, nsec.d_record, nsec.d_signatures, doDNSSEC, ret);
+
+  LOG("Synthesized valid answer from NSECs and wildcard!"<<endl);
+  return true;
+}
+
 bool AggressiveNSECCache::getNSEC3Denial(time_t now, std::shared_ptr<AggressiveNSECCache::ZoneEntry>& zoneEntry, std::vector<DNSRecord>& soaSet, std::vector<std::shared_ptr<RRSIGRecordContent>>& soaSignatures, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC)
 {
   const auto& salt = zoneEntry->d_salt;
@@ -280,16 +318,17 @@ bool AggressiveNSECCache::getNSEC3Denial(time_t now, std::shared_ptr<AggressiveN
 
   auto nameHash = DNSName(toBase32Hex(hashQNameWithSalt(salt, iterations, name))) + zone;
 
-  cerr<<"looking for nsec3 "<<nameHash<<endl;
   ZoneEntry::CacheEntry exactNSEC3;
   if (getNSEC3(now, zoneEntry, nameHash, exactNSEC3)) {
-    cerr<<"found direct match "<<nameHash<<endl;
+    LOG("Found a direct NSEC3 match for "<<nameHash);
     auto nsec3 = std::dynamic_pointer_cast<NSEC3RecordContent>(exactNSEC3.d_record);
     if (!nsec3) {
+      LOG(" but the content is not valid"<<endl);
       return false;
     }
 
     if (!isTypeDenied(nsec3, type)) {
+      LOG(" but the requested type ("<<type.getName()<<") does exist"<<endl);
       return false;
     }
 
@@ -301,33 +340,33 @@ bool AggressiveNSECCache::getNSEC3Denial(time_t now, std::shared_ptr<AggressiveN
          that (original) owner name other than DS RRs, and all RRs below that
          owner name regardless of type.
       */
+      LOG(" but this is an ancetor delegation NSEC3"<<endl);
       return false;
     }
 
-    cerr<<"Direct match, done!"<<endl;
+    LOG(": done!"<<endl);
     res = RCode::NoError;
     addToRRSet(now, soaSet, soaSignatures, zoneEntry->d_zone, doDNSSEC, ret);
     addRecordToRRSet(now, exactNSEC3.d_owner, QType::NSEC3, exactNSEC3.d_ttd - now, exactNSEC3.d_record, exactNSEC3.d_signatures, doDNSSEC, ret);
     return true;
   }
 
-  cerr<<"no direct match, looking for closest encloser"<<endl;
+  LOG("No direct NSEC3 match found for "<<nameHash<<", looking for closest encloser"<<endl);
   DNSName closestEncloser(name);
   bool found = false;
   ZoneEntry::CacheEntry closestNSEC3;
   while (!found && closestEncloser.chopOff()) {
     auto closestHash = DNSName(toBase32Hex(hashQNameWithSalt(salt, iterations, closestEncloser))) + zone;
-    cerr<<"looking for nsec3 "<<closestHash<<endl;
 
     if (getNSEC3(now, zoneEntry, closestHash, closestNSEC3)) {
-      cerr<<"found next closest encloser at "<<closestEncloser<<endl;
+      LOG("Found closest encloser at "<<closestEncloser<<" ("<<closestHash<<")"<<endl);
       found = true;
       break;
     }
   }
 
   if (!found) {
-    cerr<<"nothing found in aggressive cache either"<<endl;
+    LOG("Nothing found for the closest encloser in NSEC3 aggressive cache either"<<endl);
     return false;
   }
 
@@ -340,44 +379,49 @@ bool AggressiveNSECCache::getNSEC3Denial(time_t now, std::shared_ptr<AggressiveN
   DNSName nextCloser(closestEncloser);
   nextCloser.prependRawLabel(name.getRawLabel(labelIdx - 1));
   auto nextCloserHash = toBase32Hex(hashQNameWithSalt(salt, iterations, nextCloser));
-  cerr<<"looking for a NSEC3 covering the next closer "<<nextCloser<<": "<<nextCloserHash<<endl;
+  LOG("Looking for a NSEC3 covering the next closer "<<nextCloser<<" ("<<nextCloserHash<<")"<<endl);
 
   ZoneEntry::CacheEntry nextCloserEntry;
   if (!getNSECBefore(now, zoneEntry, DNSName(nextCloserHash) + zone, nextCloserEntry)) {
-    cerr<<"nothing found for the next closer in aggressive cache"<<endl;
+    LOG("Nothing found for the next encloser in NSEC3 aggressive cache"<<endl);
     return false;
   }
 
   if (!isCoveredByNSEC3Hash(DNSName(nextCloserHash) + zone, nextCloserEntry.d_owner, nextCloserEntry.d_next)) {
-    cerr<<"no covering record found for the next closer in aggressive cache"<<endl;
+    LOG("No covering record found for the next encloser in NSEC3 aggressive cache"<<endl);
     return false;
   }
 
   DNSName wildcard(g_wildcarddnsname + closestEncloser);
   auto wcHash = toBase32Hex(hashQNameWithSalt(salt, iterations, wildcard));
-  cerr<<"looking for a NSEC3 covering the wildcard "<<wildcard<<": "<<wcHash<<endl;
+  LOG("Looking for a NSEC3 covering the wildcard "<<wildcard<<" ("<<wcHash<<")"<<endl);
 
   ZoneEntry::CacheEntry wcEntry;
   if (!getNSECBefore(now, zoneEntry, DNSName(wcHash) + zone, wcEntry)) {
-    cerr<<"nothing found for the wildcard in aggressive cache"<<endl;
+    LOG("Nothing found for the wildcard in NSEC3 aggressive cache"<<endl);
     return false;
   }
 
   if ((DNSName(wcHash) + zone) == wcEntry.d_owner) {
+    LOG("Found an exact match for the wildcard");
+
     auto nsec3 = std::dynamic_pointer_cast<NSEC3RecordContent>(wcEntry.d_record);
     if (!nsec3) {
+      LOG(" but the content is not valid"<<endl);
       return false;
     }
 
     if (!isTypeDenied(nsec3, type)) {
-      return false;
+      LOG(" but the requested type ("<<type.getName()<<") does exist"<<endl);
+      return synthesizeFromNSEC3Wildcard(now, name, type, ret, res, doDNSSEC, nextCloserEntry, wildcard);
     }
 
     res = RCode::NoError;
+    LOG(endl);
   }
   else {
     if (!isCoveredByNSEC3Hash(DNSName(wcHash) + zone, wcEntry.d_owner, wcEntry.d_next)) {
-      cerr<<"no covering record found for the wildcard in aggressive cache"<<endl;
+      LOG("No covering record found for the wildcard in aggressive cache"<<endl);
       return false;
     }
     res = RCode::NXDomain;
@@ -388,7 +432,7 @@ bool AggressiveNSECCache::getNSEC3Denial(time_t now, std::shared_ptr<AggressiveN
   addRecordToRRSet(now, nextCloserEntry.d_owner, QType::NSEC3, nextCloserEntry.d_ttd - now, nextCloserEntry.d_record, nextCloserEntry.d_signatures, doDNSSEC, ret);
   addRecordToRRSet(now, wcEntry.d_owner, QType::NSEC3, wcEntry.d_ttd - now, wcEntry.d_record, wcEntry.d_signatures, doDNSSEC, ret);
 
-  cerr<<"Done!"<<endl;
+  LOG("Found valid NSEC3s covering the requested name and type!"<<endl);
   return true;
 }
 
@@ -396,7 +440,6 @@ bool AggressiveNSECCache::getDenial(time_t now, const DNSName& name, const QType
 {
   auto zoneEntry = getBestZone(name);
   if (!zoneEntry) {
-    cerr<<"zone info not found"<<endl;
     return false;
   }
 
@@ -404,12 +447,11 @@ bool AggressiveNSECCache::getDenial(time_t now, const DNSName& name, const QType
   std::vector<DNSRecord> soaSet;
   std::vector<std::shared_ptr<RRSIGRecordContent>> soaSignatures;
   if (g_recCache->get(now, zoneEntry->d_zone, QType::SOA, true, &soaSet, who, false, routingTag, doDNSSEC ? &soaSignatures : nullptr, nullptr, nullptr, &cachedState) <= 0 || cachedState != vState::Secure) {
-    cerr<<"could not find SOA"<<endl;
+    LOG("No valid SOA found for "<<zoneEntry->d_zone<<", which is the best match for "<<name<<endl);
     return false;
   }
 
   if (zoneEntry->d_nsec3) {
-    cerr<<"nsec 3"<<endl;
     return getNSEC3Denial(now, zoneEntry, soaSet, soaSignatures, name, type, ret, res, doDNSSEC);
   }
 
@@ -418,9 +460,9 @@ bool AggressiveNSECCache::getDenial(time_t now, const DNSName& name, const QType
   bool covered = false;
   bool needWildcard = false;
 
-  cerr<<"looking for nsec before "<<name<<endl;
+  LOG("Looking for a NSEC before "<<name);
   if (!getNSECBefore(now, zoneEntry, name, entry)) {
-    cerr<<"nothing found in aggressive cache either"<<endl;
+    LOG(": nothing found in the aggressive cache"<<endl);
     return false;
   }
 
@@ -429,18 +471,19 @@ bool AggressiveNSECCache::getDenial(time_t now, const DNSName& name, const QType
     return false;
   }
 
-  cerr<<"nsecFound "<<entry.d_owner<<endl;
+  LOG(": found a possible NSEC at "<<entry.d_owner<<" ");
   auto denial = matchesNSEC(name, type.getCode(), entry.d_owner, content, entry.d_signatures);
   if (denial == dState::NODENIAL || denial == dState::INCONCLUSIVE) {
-    cerr<<"no dice"<<endl;
+    LOG(" but it does no cover us"<<endl);
     return false;
   }
   else if (denial == dState::NXQTYPE) {
     covered = true;
-    cerr<<"nx qtype"<<endl;
+    LOG(" and it proves that the type does not exist"<<endl);
     res = RCode::NoError;
   }
   else if (denial == dState::NXDOMAIN) {
+    LOG(" and it proves that the name does not exist"<<endl);
     const DNSName commonLabels = entry.d_owner.getCommonLabels(entry.d_next);
     DNSName wc(name);
     auto labelsCount = wc.countLabels();
@@ -453,41 +496,38 @@ bool AggressiveNSECCache::getDenial(time_t now, const DNSName& name, const QType
     }
     wc = g_wildcarddnsname + wc;
 
-    cerr<<"looking for nsec before "<<wc<<endl;
+    LOG("Now looking for a NSEC before the wildcard "<<wc);
     if (!getNSECBefore(now, zoneEntry, wc, wcEntry)) {
-      cerr<<"nothing found in aggressive cache for Wildcard"<<endl;
+      LOG(": nothing found in the aggressive cache"<<endl);
       return false;
     }
 
-    cerr<<"wc nsec found "<<wcEntry.d_owner<<endl;
-    if (wcEntry.d_owner == entry.d_owner) {
+    LOG(": found a possible NSEC at "<<wcEntry.d_owner<<" ");
+    auto nsecContent = std::dynamic_pointer_cast<NSECRecordContent>(wcEntry.d_record);
+
+    denial = matchesNSEC(wc, type.getCode(), wcEntry.d_owner, nsecContent, wcEntry.d_signatures);
+    if (denial == dState::NODENIAL || denial == dState::INCONCLUSIVE) {
+      LOG(" but it does no cover us"<<endl);
+
+      if (wcEntry.d_owner == wc) {
+        return synthesizeFromNSECWildcard(now, name, type, ret, res, doDNSSEC, entry, wc);
+      }
+
+      return false;
+    }
+    else if (denial == dState::NXQTYPE) {
+      LOG(" and it proves that there is a matching wildcard, but the type does not exist"<<endl);
+      covered = true;
+      res = RCode::NoError;
+    }
+    else if (denial == dState::NXDOMAIN) {
+      LOG(" and it proves that there is no matching wildcard"<<endl);
       covered = true;
       res = RCode::NXDomain;
     }
-    else {
-      auto nsecContent = std::dynamic_pointer_cast<NSECRecordContent>(wcEntry.d_record);
-      denial = matchesNSEC(wc, type.getCode(), wcEntry.d_owner, nsecContent, wcEntry.d_signatures);
-      if (denial == dState::NODENIAL || denial == dState::INCONCLUSIVE) {
-        /* too complicated for now */
-        /* we would need:
-           - to store wildcard entries in the non-expanded form in the record cache, in addition to their expanded form ;
-           - do a lookup to retrieve them ;
-           - expand them and the NSEC
-        */
-        return false;
-      }
-      else if (denial == dState::NXQTYPE) {
-        covered = true;
-        res = RCode::NoError;
-      }
-      else if (denial == dState::NXDOMAIN) {
-        covered = true;
-        res = RCode::NXDomain;
-      }
 
-      if (wcEntry.d_owner != wc) {
-        needWildcard = true;
-      }
+    if (wcEntry.d_owner != wc && wcEntry.d_owner != entry.d_owner) {
+      needWildcard = true;
     }
   }
 
@@ -504,5 +544,6 @@ bool AggressiveNSECCache::getDenial(time_t now, const DNSName& name, const QType
     addRecordToRRSet(now, wcEntry.d_owner, QType::NSEC, wcEntry.d_ttd - now, wcEntry.d_record, wcEntry.d_signatures, doDNSSEC, ret);
   }
 
+  LOG("Found valid NSECs covering the requested name and type!"<<endl);
   return true;
 }
index e720343c11c8b96f46ef43b59ce2612332324969..e8e905c2261e91d46ce31ee427f146bbe0a30a71 100644 (file)
@@ -97,6 +97,8 @@ private:
   bool getNSECBefore(time_t now, std::shared_ptr<ZoneEntry>& zoneEntry, const DNSName& name, ZoneEntry::CacheEntry& entry);
   bool getNSEC3(time_t now, std::shared_ptr<ZoneEntry>& zoneEntry, const DNSName& name, ZoneEntry::CacheEntry& entry);
   bool getNSEC3Denial(time_t now, std::shared_ptr<ZoneEntry>& zoneEntry, std::vector<DNSRecord>& soaSet, std::vector<std::shared_ptr<RRSIGRecordContent>>& soaSignatures, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC);
+  bool synthesizeFromNSEC3Wildcard(time_t now, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC, ZoneEntry::CacheEntry& nextCloser, const DNSName& wildcardName);
+  bool synthesizeFromNSECWildcard(time_t now, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC, ZoneEntry::CacheEntry& nsec, const DNSName& wildcardName);
 
   SuffixMatchTree<std::shared_ptr<ZoneEntry>> d_zones;
   ReadWriteLock d_lock;
index da73338926d4ec6490a8cc725504e640df78f6ea..a355b7e7a4b60b031513b60ebc5c20c6f8cac872 100644 (file)
@@ -6,6 +6,374 @@
 
 BOOST_AUTO_TEST_SUITE(aggressive_nsec_cc)
 
+BOOST_AUTO_TEST_CASE(test_aggressive_nsec_nxdomain)
+{
+  std::unique_ptr<SyncRes> sr;
+  initSR(sr, true);
+  g_aggressiveNSECCache = make_unique<AggressiveNSECCache>();
+
+  setDNSSECValidation(sr, DNSSECMode::ValidateAll);
+
+  primeHints();
+  /* we first ask b.powerdns.com., get a NXD, then check that the aggressive
+     NSEC cache will use the NSEC (a -> h) to prove that g.powerdns.com. does not exist
+     either */
+  const DNSName target1("b.powerdns.com.");
+  const DNSName target2("g.powerdns.com.");
+  testkeysset_t keys;
+
+  auto luaconfsCopy = g_luaconfs.getCopy();
+  luaconfsCopy.dsAnchors.clear();
+  generateKeyMaterial(g_rootdnsname, DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys, luaconfsCopy.dsAnchors);
+  generateKeyMaterial(DNSName("powerdns.com."), DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys);
+
+  g_luaconfs.setState(luaconfsCopy);
+
+  size_t queriesCount = 0;
+
+  sr->setAsyncCallback([target1, &queriesCount, keys](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 (type == QType::DS || type == QType::DNSKEY) {
+      if (domain != DNSName("powerdns.com.") && domain.isPartOf(DNSName("powerdns.com."))) {
+        /* no cut, NSEC, name does not exist (the generic version will generate an exact NSEC for the target, which we don't want) */
+        setLWResult(res, RCode::NoError, true, false, true);
+        /* no data */
+        addRecordToLW(res, DNSName("powerdns.com."), QType::SOA, "powerdns.com. powerdns.com. 2017032301 10800 3600 604800 3600", DNSResourceRecord::AUTHORITY, 3600);
+        addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
+        /* no record for this name */
+        addNSECRecordToLW(DNSName("a.powerdns.com."), DNSName("h.powerdns.com."), {QType::A, QType::TXT, QType::RRSIG}, 600, res->d_records);
+        addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
+        /* no wildcard either */
+        addNSECRecordToLW(DNSName(").powerdns.com."), DNSName("a.powerdns.com."), {QType::AAAA, QType::RRSIG}, 600, res->d_records);
+        addRRSIG(keys, res->d_records, DNSName("powerdns.com"), 300);
+        return LWResult::Result::Success;
+      }
+      else if (domain == DNSName("com.")) {
+        /* no cut */
+        return genericDSAndDNSKEYHandler(res, domain, DNSName("."), type, keys, false);
+      }
+      else if (domain == DNSName("powerdns.com.")) {
+        return genericDSAndDNSKEYHandler(res, domain, DNSName("."), type, keys);
+      }
+      else {
+        /* cut */
+        return genericDSAndDNSKEYHandler(res, domain, domain, type, keys);
+      }
+    }
+    else {
+      if (isRootServer(ip)) {
+        setLWResult(res, 0, false, false, true);
+        addRecordToLW(res, "powerdns.com.", QType::NS, "a.gtld-servers.com.", DNSResourceRecord::AUTHORITY, 3600);
+        addDS(DNSName("powerdns.com."), 300, res->d_records, keys);
+        addRRSIG(keys, res->d_records, DNSName("."), 300);
+        addRecordToLW(res, "a.gtld-servers.com.", QType::A, "192.0.2.1", DNSResourceRecord::ADDITIONAL, 3600);
+        return LWResult::Result::Success;
+      }
+      else if (ip == ComboAddress("192.0.2.1:53")) {
+        if (domain == target1) {
+          setLWResult(res, RCode::NXDomain, true, false, true);
+          addRecordToLW(res, DNSName("powerdns.com."), QType::SOA, "powerdns.com. powerdns.com. 2017032301 10800 3600 604800 3600", DNSResourceRecord::AUTHORITY, 3600);
+          addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
+          /* no record for this name */
+          addNSECRecordToLW(DNSName("a.powerdns.com."), DNSName("h.powerdns.com."), {QType::A, QType::TXT, QType::RRSIG}, 600, res->d_records);
+          addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
+          /* no wildcard either */
+          addNSECRecordToLW(DNSName(").powerdns.com."), DNSName("a.powerdns.com."), {QType::AAAA, QType::RRSIG}, 600, res->d_records);
+          addRRSIG(keys, res->d_records, DNSName("powerdns.com"), 300);
+          return LWResult::Result::Success;
+        }
+      }
+    }
+
+    return LWResult::Result::Timeout;
+  });
+
+  vector<DNSRecord> ret;
+  int res = sr->beginResolve(target1, QType(QType::A), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NXDomain);
+  BOOST_CHECK_EQUAL(sr->getValidationState(), vState::Secure);
+  BOOST_REQUIRE_EQUAL(ret.size(), 6U);
+  BOOST_CHECK_EQUAL(queriesCount, 7U);
+
+  ret.clear();
+  res = sr->beginResolve(target2, QType(QType::A), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NXDomain);
+  BOOST_CHECK_EQUAL(sr->getValidationState(), vState::Secure);
+  BOOST_REQUIRE_EQUAL(ret.size(), 6U);
+  BOOST_CHECK_EQUAL(queriesCount, 7U);
+}
+
+BOOST_AUTO_TEST_CASE(test_aggressive_nsec_nodata)
+{
+  std::unique_ptr<SyncRes> sr;
+  initSR(sr, true);
+  g_aggressiveNSECCache = make_unique<AggressiveNSECCache>();
+
+  setDNSSECValidation(sr, DNSSECMode::ValidateAll);
+
+  primeHints();
+  /* we first ask a.powerdns.com. | A, get a NODATA, then check that the aggressive
+     NSEC cache will use the NSEC to prove that the AAAA does not exist either */
+  const DNSName target("a.powerdns.com.");
+  testkeysset_t keys;
+
+  auto luaconfsCopy = g_luaconfs.getCopy();
+  luaconfsCopy.dsAnchors.clear();
+  generateKeyMaterial(g_rootdnsname, DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys, luaconfsCopy.dsAnchors);
+  generateKeyMaterial(DNSName("powerdns.com."), DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys);
+
+  g_luaconfs.setState(luaconfsCopy);
+
+  size_t queriesCount = 0;
+
+  sr->setAsyncCallback([target, &queriesCount, keys](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 (type == QType::DS || type == QType::DNSKEY) {
+      if (domain != DNSName("powerdns.com.") && domain.isPartOf(DNSName("powerdns.com."))) {
+        /* no cut, NSEC */
+        return genericDSAndDNSKEYHandler(res, domain, domain, type, keys, false);
+      }
+      else if (domain == DNSName("com.")) {
+        /* no cut */
+        return genericDSAndDNSKEYHandler(res, domain, DNSName("."), type, keys, false);
+      }
+      else if (domain == DNSName("powerdns.com.")) {
+        return genericDSAndDNSKEYHandler(res, domain, DNSName("."), type, keys);
+      }
+      else {
+        /* cut */
+        return genericDSAndDNSKEYHandler(res, domain, domain, type, keys);
+      }
+    }
+    else {
+      if (isRootServer(ip)) {
+        setLWResult(res, 0, false, false, true);
+        addRecordToLW(res, "powerdns.com.", QType::NS, "a.gtld-servers.com.", DNSResourceRecord::AUTHORITY, 3600);
+        addDS(DNSName("powerdns.com."), 300, res->d_records, keys);
+        addRRSIG(keys, res->d_records, DNSName("."), 300);
+        addRecordToLW(res, "a.gtld-servers.com.", QType::A, "192.0.2.1", DNSResourceRecord::ADDITIONAL, 3600);
+        return LWResult::Result::Success;
+      }
+      else if (ip == ComboAddress("192.0.2.1:53")) {
+        if (domain == target && type == QType::A) {
+          setLWResult(res, RCode::NoError, true, false, true);
+          /* no data */
+          addRecordToLW(res, DNSName("powerdns.com."), QType::SOA, "powerdns.com. powerdns.com. 2017032301 10800 3600 604800 3600", DNSResourceRecord::AUTHORITY, 3600);
+          addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
+          /* no record for this name */
+          /* exact match */
+          addNSECRecordToLW(DNSName("a.powerdns.com."), DNSName("powerdns.com."), {QType::TXT, QType::RRSIG}, 600, res->d_records);
+          addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
+          /* no need for wildcard in that case */
+          return LWResult::Result::Success;
+        }
+      }
+    }
+
+    return LWResult::Result::Timeout;
+  });
+
+  vector<DNSRecord> ret;
+  int res = sr->beginResolve(target, QType(QType::A), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NoError);
+  BOOST_CHECK_EQUAL(sr->getValidationState(), vState::Secure);
+  BOOST_REQUIRE_EQUAL(ret.size(), 4U);
+  BOOST_CHECK_EQUAL(queriesCount, 7U);
+
+  ret.clear();
+  res = sr->beginResolve(target, QType(QType::AAAA), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NoError);
+  BOOST_CHECK_EQUAL(sr->getValidationState(), vState::Secure);
+  BOOST_REQUIRE_EQUAL(ret.size(), 4U);
+  BOOST_CHECK_EQUAL(queriesCount, 7U);
+}
+
+BOOST_AUTO_TEST_CASE(test_aggressive_nsec_nodata_wildcard)
+{
+  std::unique_ptr<SyncRes> sr;
+  initSR(sr, true);
+  g_aggressiveNSECCache = make_unique<AggressiveNSECCache>();
+
+  setDNSSECValidation(sr, DNSSECMode::ValidateAll);
+
+  primeHints();
+  /* we first ask a.powerdns.com. | A, get a NODATA (no exact match but there is a wildcard match),
+     then check that the aggressive NSEC cache will use the NSEC to prove that the AAAA does not exist either */
+  const DNSName target("a.powerdns.com.");
+  testkeysset_t keys;
+
+  auto luaconfsCopy = g_luaconfs.getCopy();
+  luaconfsCopy.dsAnchors.clear();
+  generateKeyMaterial(g_rootdnsname, DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys, luaconfsCopy.dsAnchors);
+  generateKeyMaterial(DNSName("powerdns.com."), DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys);
+
+  g_luaconfs.setState(luaconfsCopy);
+
+  size_t queriesCount = 0;
+
+  sr->setAsyncCallback([target, &queriesCount, keys](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 (type == QType::DS || type == QType::DNSKEY) {
+      if (domain != DNSName("powerdns.com.") && domain.isPartOf(DNSName("powerdns.com."))) {
+        /* no cut, NSEC, name does not exist but there is a wildcard (the generic version will generate an exact NSEC for the target, which we don't want) */
+        setLWResult(res, RCode::NoError, true, false, true);
+        /* no data */
+        addRecordToLW(res, DNSName("powerdns.com."), QType::SOA, "powerdns.com. powerdns.com. 2017032301 10800 3600 604800 3600", DNSResourceRecord::AUTHORITY, 3600);
+        addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
+        /* the name does not exist, a wildcard applies but does not have this type */
+        addNSECRecordToLW(DNSName("*.powerdns.com."), DNSName("z.powerdns.com."), {QType::TXT, QType::RRSIG}, 600, res->d_records);
+        addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300, false, boost::none, DNSName("*.powerdns.com"));
+        return LWResult::Result::Success;
+      }
+      else if (domain == DNSName("com.")) {
+        /* no cut */
+        return genericDSAndDNSKEYHandler(res, domain, DNSName("."), type, keys, false);
+      }
+      else if (domain == DNSName("powerdns.com.")) {
+        return genericDSAndDNSKEYHandler(res, domain, DNSName("."), type, keys);
+      }
+      else {
+        /* cut */
+        return genericDSAndDNSKEYHandler(res, domain, domain, type, keys);
+      }
+    }
+    else {
+      if (isRootServer(ip)) {
+        setLWResult(res, 0, false, false, true);
+        addRecordToLW(res, "powerdns.com.", QType::NS, "a.gtld-servers.com.", DNSResourceRecord::AUTHORITY, 3600);
+        addDS(DNSName("powerdns.com."), 300, res->d_records, keys);
+        addRRSIG(keys, res->d_records, DNSName("."), 300);
+        addRecordToLW(res, "a.gtld-servers.com.", QType::A, "192.0.2.1", DNSResourceRecord::ADDITIONAL, 3600);
+        return LWResult::Result::Success;
+      }
+      else if (ip == ComboAddress("192.0.2.1:53")) {
+        if (domain == target && type == QType::A) {
+          setLWResult(res, RCode::NoError, true, false, true);
+          /* no data */
+          addRecordToLW(res, DNSName("powerdns.com."), QType::SOA, "powerdns.com. powerdns.com. 2017032301 10800 3600 604800 3600", DNSResourceRecord::AUTHORITY, 3600);
+          addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
+          /* the name does not exist, a wildcard applies but does not have this type */
+          addNSECRecordToLW(DNSName("*.powerdns.com."), DNSName("z.powerdns.com."), {QType::TXT, QType::RRSIG}, 600, res->d_records);
+          addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300, false, boost::none, DNSName("*.powerdns.com"));
+          return LWResult::Result::Success;
+        }
+      }
+    }
+
+    return LWResult::Result::Timeout;
+  });
+
+  vector<DNSRecord> ret;
+  int res = sr->beginResolve(target, QType(QType::A), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NoError);
+  BOOST_CHECK_EQUAL(sr->getValidationState(), vState::Secure);
+  BOOST_REQUIRE_EQUAL(ret.size(), 4U);
+  BOOST_CHECK_EQUAL(queriesCount, 7U);
+
+  ret.clear();
+  res = sr->beginResolve(target, QType(QType::AAAA), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NoError);
+  BOOST_CHECK_EQUAL(sr->getValidationState(), vState::Secure);
+  BOOST_REQUIRE_EQUAL(ret.size(), 4U);
+  BOOST_CHECK_EQUAL(queriesCount, 7U);
+}
+
+BOOST_AUTO_TEST_CASE(test_aggressive_nsec_wildcard_synthesis)
+{
+  std::unique_ptr<SyncRes> sr;
+  initSR(sr, true);
+  g_aggressiveNSECCache = make_unique<AggressiveNSECCache>();
+
+  setDNSSECValidation(sr, DNSSECMode::ValidateAll);
+
+  primeHints();
+  /* we first ask a.powerdns.com. | A, get an answer synthesized from the wildcard,
+     then check that the aggressive NSEC cache will use the wildcard to synthesize an answer
+     for b.powerdns.com */
+  const DNSName target("a.powerdns.com.");
+  testkeysset_t keys;
+
+  auto luaconfsCopy = g_luaconfs.getCopy();
+  luaconfsCopy.dsAnchors.clear();
+  generateKeyMaterial(g_rootdnsname, DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys, luaconfsCopy.dsAnchors);
+  generateKeyMaterial(DNSName("powerdns.com."), DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys);
+
+  g_luaconfs.setState(luaconfsCopy);
+
+  size_t queriesCount = 0;
+
+  sr->setAsyncCallback([target, &queriesCount, keys](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 (type == QType::DS || type == QType::DNSKEY) {
+      if (domain != DNSName("powerdns.com.") && domain.isPartOf(DNSName("powerdns.com."))) {
+        /* no cut, NSEC, name does not exist but there is a wildcard (the generic version will generate an exact NSEC for the target, which we don't want) */
+        setLWResult(res, RCode::NoError, true, false, true);
+        /* no data */
+        addRecordToLW(res, DNSName("powerdns.com."), QType::SOA, "powerdns.com. powerdns.com. 2017032301 10800 3600 604800 3600", DNSResourceRecord::AUTHORITY, 3600);
+        addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
+        /* the name does not exist, a wildcard applies and have the requested type but no DS */
+        addNSECRecordToLW(DNSName("*.powerdns.com."), DNSName("z.powerdns.com."), {QType::A, QType::RRSIG}, 600, res->d_records);
+        addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300, false, boost::none, DNSName("*.powerdns.com"));
+        return LWResult::Result::Success;
+      }
+      else if (domain == DNSName("com.")) {
+        /* no cut */
+        return genericDSAndDNSKEYHandler(res, domain, DNSName("."), type, keys, false);
+      }
+      else if (domain == DNSName("powerdns.com.")) {
+        return genericDSAndDNSKEYHandler(res, domain, DNSName("."), type, keys);
+      }
+      else {
+        /* cut */
+        return genericDSAndDNSKEYHandler(res, domain, domain, type, keys);
+      }
+    }
+    else {
+      if (isRootServer(ip)) {
+        setLWResult(res, 0, false, false, true);
+        addRecordToLW(res, "powerdns.com.", QType::NS, "a.gtld-servers.com.", DNSResourceRecord::AUTHORITY, 3600);
+        addDS(DNSName("powerdns.com."), 300, res->d_records, keys);
+        addRRSIG(keys, res->d_records, DNSName("."), 300);
+        addRecordToLW(res, "a.gtld-servers.com.", QType::A, "192.0.2.1", DNSResourceRecord::ADDITIONAL, 3600);
+        return LWResult::Result::Success;
+      }
+      else if (ip == ComboAddress("192.0.2.1:53")) {
+        setLWResult(res, RCode::NoError, true, false, true);
+        addRecordToLW(res, domain, QType::A, "192.0.2.1");
+        addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300, false, boost::none, DNSName("*.powerdns.com"));
+        /* the name does not exist, a wildcard applies and has the requested type */
+        addNSECRecordToLW(DNSName("*.powerdns.com."), DNSName("z.powerdns.com."), {QType::A, QType::RRSIG}, 600, res->d_records);
+        addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300, false, boost::none, DNSName("*.powerdns.com"));
+        return LWResult::Result::Success;
+      }
+    }
+
+    return LWResult::Result::Timeout;
+  });
+
+  vector<DNSRecord> ret;
+  int res = sr->beginResolve(target, QType(QType::A), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NoError);
+  BOOST_CHECK_EQUAL(sr->getValidationState(), vState::Secure);
+  BOOST_REQUIRE_EQUAL(ret.size(), 4U);
+  BOOST_CHECK_EQUAL(ret.at(0).d_name, target);
+  BOOST_CHECK_EQUAL(ret.at(0).d_type, QType::A);  
+  BOOST_CHECK_EQUAL(queriesCount, 7U);
+
+  ret.clear();
+  res = sr->beginResolve(DNSName("b.powerdns.com."), QType(QType::A), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NoError);
+  BOOST_CHECK_EQUAL(sr->getValidationState(), vState::Secure);
+  BOOST_REQUIRE_EQUAL(ret.size(), 4U);
+  BOOST_CHECK_EQUAL(ret.at(0).d_name, DNSName("b.powerdns.com."));
+  BOOST_CHECK_EQUAL(ret.at(0).d_type, QType::A);  
+  BOOST_CHECK_EQUAL(queriesCount, 7U);
+}
+
 BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_nxdomain)
 {
   std::unique_ptr<SyncRes> sr;
@@ -15,7 +383,10 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_nxdomain)
   setDNSSECValidation(sr, DNSSECMode::ValidateAll);
 
   primeHints();
-  /* we are lucky enough that our hashes will cover g.powerdns.com. as well */
+  /* we are lucky enough that our hashes will cover g.powerdns.com. as well,
+     so we first ask b.powerdns.com., get a NXD, then check that the aggressive
+     NSEC cache will use the NSEC3 to prove that g.powerdns.com. does not exist
+     either */
   const DNSName target1("b.powerdns.com.");
   const DNSName target2("g.powerdns.com.");
   testkeysset_t keys;
@@ -61,18 +432,17 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_nxdomain)
       else if (ip == ComboAddress("192.0.2.1:53")) {
         if (domain == target1) {
           setLWResult(res, RCode::NXDomain, true, false, true);
-          /* no data */
           addRecordToLW(res, DNSName("powerdns.com."), QType::SOA, "powerdns.com. powerdns.com. 2017032301 10800 3600 604800 3600", DNSResourceRecord::AUTHORITY, 3600);
           addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
           /* no record for this name */
           /* first the closest encloser */
-          addNSEC3UnhashedRecordToLW(DNSName("powerdns.com."), DNSName("powerdns.com."), "whatever", {QType::A, QType::TXT, QType::RRSIG, QType::NSEC}, 600, res->d_records);
+          addNSEC3UnhashedRecordToLW(DNSName("powerdns.com."), DNSName("powerdns.com."), "whatever", {QType::A, QType::TXT, QType::RRSIG}, 600, res->d_records);
           addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
           /* then the next closer */
-          addNSEC3UnhashedRecordToLW(DNSName("a.powerdns.com."), DNSName("powerdns.com."), "v", {QType::RRSIG, QType::NSEC}, 600, res->d_records);
+          addNSEC3UnhashedRecordToLW(DNSName("a.powerdns.com."), DNSName("powerdns.com."), "v", {QType::RRSIG}, 600, res->d_records);
           addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
           /* no wildcard */
-          addNSEC3NarrowRecordToLW(DNSName("*.powerdns.com."), DNSName("powerdns.com."), {QType::AAAA, QType::NSEC, QType::RRSIG}, 600, res->d_records);
+          addNSEC3NarrowRecordToLW(DNSName("*.powerdns.com."), DNSName("powerdns.com."), {QType::AAAA, QType::RRSIG}, 600, res->d_records);
           addRRSIG(keys, res->d_records, DNSName("powerdns.com"), 300);
           return LWResult::Result::Success;
         }
@@ -97,7 +467,6 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_nxdomain)
   BOOST_CHECK_EQUAL(queriesCount, 7U);
 }
 
-#if 0
 BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_nodata)
 {
   std::unique_ptr<SyncRes> sr;
@@ -107,8 +476,9 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_nodata)
   setDNSSECValidation(sr, DNSSECMode::ValidateAll);
 
   primeHints();
-  const DNSName target1("a.powerdns.com.");
-  const DNSName target2("b.powerdns.com.");
+  /* we first ask a.powerdns.com. | A, get a NODATA, then check that the aggressive
+     NSEC cache will use the NSEC3 to prove that the AAAA does not exist either */
+  const DNSName target("a.powerdns.com.");
   testkeysset_t keys;
 
   auto luaconfsCopy = g_luaconfs.getCopy();
@@ -120,7 +490,7 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_nodata)
 
   size_t queriesCount = 0;
 
-  sr->setAsyncCallback([target1, &queriesCount, keys](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) {
+  sr->setAsyncCallback([target, &queriesCount, keys](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 (type == QType::DS || type == QType::DNSKEY) {
@@ -130,7 +500,10 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_nodata)
       }
       else if (domain == DNSName("com.")) {
         /* no cut */
-        return genericDSAndDNSKEYHandler(res, domain, domain, type, keys, false);
+        return genericDSAndDNSKEYHandler(res, domain, DNSName("."), type, keys, false);
+      }
+      else if (domain == DNSName("powerdns.com.")) {
+        return genericDSAndDNSKEYHandler(res, domain, DNSName("."), type, keys);
       }
       else {
         /* cut */
@@ -147,21 +520,119 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_nodata)
         return LWResult::Result::Success;
       }
       else if (ip == ComboAddress("192.0.2.1:53")) {
-        if (domain == target1) {
-          setLWResult(res, 0, true, false, true);
+        if (domain == target && type == QType::A) {
+          setLWResult(res, RCode::NoError, true, false, true);
           /* no data */
-          addRecordToLW(res, DNSName("com."), QType::SOA, "com. com. 2017032301 10800 3600 604800 3600", DNSResourceRecord::AUTHORITY, 3600);
-          addRRSIG(keys, res->d_records, DNSName("com."), 300);
+          addRecordToLW(res, DNSName("powerdns.com."), QType::SOA, "powerdns.com. powerdns.com. 2017032301 10800 3600 604800 3600", DNSResourceRecord::AUTHORITY, 3600);
+          addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
           /* no record for this name */
+          /* exact match */
+          addNSEC3UnhashedRecordToLW(DNSName("a.powerdns.com."), DNSName("powerdns.com."), "whatever", {QType::TXT, QType::RRSIG}, 600, res->d_records);
+          addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
+          /* no need for next closer or wildcard in that case */
+          return LWResult::Result::Success;
+        }
+      }
+    }
+
+    return LWResult::Result::Timeout;
+  });
+
+  vector<DNSRecord> ret;
+  int res = sr->beginResolve(target, QType(QType::A), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NoError);
+  BOOST_CHECK_EQUAL(sr->getValidationState(), vState::Secure);
+  BOOST_REQUIRE_EQUAL(ret.size(), 4U);
+  BOOST_CHECK_EQUAL(queriesCount, 7U);
+
+  ret.clear();
+  res = sr->beginResolve(target, QType(QType::AAAA), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NoError);
+  BOOST_CHECK_EQUAL(sr->getValidationState(), vState::Secure);
+  BOOST_REQUIRE_EQUAL(ret.size(), 4U);
+  BOOST_CHECK_EQUAL(queriesCount, 7U);
+}
+
+BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_nodata_wildcard)
+{
+  std::unique_ptr<SyncRes> sr;
+  initSR(sr, true);
+  g_aggressiveNSECCache = make_unique<AggressiveNSECCache>();
+
+  setDNSSECValidation(sr, DNSSECMode::ValidateAll);
+
+  primeHints();
+  /* we first ask a.powerdns.com. | A, get a NODATA (no exact match but there is a wildcard match),
+     then check that the aggressive NSEC cache will use the NSEC3 to prove that the AAAA does not exist either */
+  const DNSName target("a.powerdns.com.");
+  testkeysset_t keys;
+
+  auto luaconfsCopy = g_luaconfs.getCopy();
+  luaconfsCopy.dsAnchors.clear();
+  generateKeyMaterial(g_rootdnsname, DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys, luaconfsCopy.dsAnchors);
+  generateKeyMaterial(DNSName("powerdns.com."), DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys);
+
+  g_luaconfs.setState(luaconfsCopy);
+
+  size_t queriesCount = 0;
+
+  sr->setAsyncCallback([target, &queriesCount, keys](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 (type == QType::DS || type == QType::DNSKEY) {
+      if (domain != DNSName("powerdns.com.") && domain.isPartOf(DNSName("powerdns.com."))) {
+        /* no cut, NSEC3, name does not exist but there is a wildcard (the generic version will generate an exact NSEC3 for the target, which we don't want) */
+        setLWResult(res, RCode::NoError, true, false, true);
+        /* no data */
+        addRecordToLW(res, DNSName("powerdns.com."), QType::SOA, "powerdns.com. powerdns.com. 2017032301 10800 3600 604800 3600", DNSResourceRecord::AUTHORITY, 3600);
+        addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
+        /* first the closest encloser */
+        addNSEC3UnhashedRecordToLW(DNSName("powerdns.com."), DNSName("powerdns.com."), "whatever", {QType::A, QType::TXT, QType::RRSIG}, 600, res->d_records);
+        addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
+        /* then the next closer */
+        addNSEC3UnhashedRecordToLW(DNSName("+.powerdns.com."), DNSName("powerdns.com."), "v", {QType::RRSIG}, 600, res->d_records);
+        addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
+        /* a wildcard applies but does not have this type */
+        addNSEC3UnhashedRecordToLW(DNSName("*.powerdns.com."), DNSName("powerdns.com."), "whatever", {QType::TXT, QType::RRSIG}, 600, res->d_records);
+        addRRSIG(keys, res->d_records, DNSName("powerdns.com"), 300, false, boost::none, DNSName("*.powerdns.com"));
+        return LWResult::Result::Success;
+      }
+      else if (domain == DNSName("com.")) {
+        /* no cut */
+        return genericDSAndDNSKEYHandler(res, domain, DNSName("."), type, keys, false);
+      }
+      else if (domain == DNSName("powerdns.com.")) {
+        return genericDSAndDNSKEYHandler(res, domain, DNSName("."), type, keys);
+      }
+      else {
+        /* cut */
+        return genericDSAndDNSKEYHandler(res, domain, domain, type, keys);
+      }
+    }
+    else {
+      if (isRootServer(ip)) {
+        setLWResult(res, 0, false, false, true);
+        addRecordToLW(res, "powerdns.com.", QType::NS, "a.gtld-servers.com.", DNSResourceRecord::AUTHORITY, 3600);
+        addDS(DNSName("powerdns.com."), 300, res->d_records, keys);
+        addRRSIG(keys, res->d_records, DNSName("."), 300);
+        addRecordToLW(res, "a.gtld-servers.com.", QType::A, "192.0.2.1", DNSResourceRecord::ADDITIONAL, 3600);
+        return LWResult::Result::Success;
+      }
+      else if (ip == ComboAddress("192.0.2.1:53")) {
+        if (domain == target && type == QType::A) {
+          setLWResult(res, RCode::NoError, true, false, true);
+          /* no data */
+          addRecordToLW(res, DNSName("powerdns.com."), QType::SOA, "powerdns.com. powerdns.com. 2017032301 10800 3600 604800 3600", DNSResourceRecord::AUTHORITY, 3600);
+          addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
           /* first the closest encloser */
-          addNSEC3UnhashedRecordToLW(DNSName("com."), DNSName("com."), "whatever", {QType::A, QType::TXT, QType::RRSIG, QType::NSEC}, 600, res->d_records);
-          addRRSIG(keys, res->d_records, DNSName("com."), 300);
+          addNSEC3UnhashedRecordToLW(DNSName("powerdns.com."), DNSName("powerdns.com."), "whatever", {QType::A, QType::TXT, QType::RRSIG}, 600, res->d_records);
+          addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
           /* then the next closer */
-          addNSEC3NarrowRecordToLW(domain, DNSName("com."), {QType::RRSIG, QType::NSEC}, 600, res->d_records);
-          addRRSIG(keys, res->d_records, DNSName("com."), 300);
-          /* a wildcard matches but has no record for this type */
-          addNSEC3UnhashedRecordToLW(DNSName("*.com."), DNSName("com."), "whatever", {QType::AAAA, QType::NSEC, QType::RRSIG}, 600, res->d_records);
-          addRRSIG(keys, res->d_records, DNSName("com"), 300, false, boost::none, DNSName("*.com"));
+          addNSEC3UnhashedRecordToLW(DNSName("+.powerdns.com."), DNSName("powerdns.com."), "v", {QType::RRSIG}, 600, res->d_records);
+          addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
+          /* a wildcard applies but does not have this type */
+          addNSEC3UnhashedRecordToLW(DNSName("*.powerdns.com."), DNSName("powerdns.com."), "whatever", {QType::TXT, QType::RRSIG}, 600, res->d_records);
+          addRRSIG(keys, res->d_records, DNSName("powerdns.com"), 300, false, boost::none, DNSName("*.powerdns.com"));
           return LWResult::Result::Success;
         }
       }
@@ -175,16 +646,115 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_nodata)
   BOOST_CHECK_EQUAL(res, RCode::NoError);
   BOOST_CHECK_EQUAL(sr->getValidationState(), vState::Secure);
   BOOST_REQUIRE_EQUAL(ret.size(), 8U);
-  BOOST_CHECK_EQUAL(queriesCount, 6U);
+  BOOST_CHECK_EQUAL(queriesCount, 7U);
 
-  /* again, to test the cache */
   ret.clear();
-  res = sr->beginResolve(target, QType(QType::A), QClass::IN, ret);
+  res = sr->beginResolve(target, QType(QType::AAAA), QClass::IN, ret);
   BOOST_CHECK_EQUAL(res, RCode::NoError);
   BOOST_CHECK_EQUAL(sr->getValidationState(), vState::Secure);
   BOOST_REQUIRE_EQUAL(ret.size(), 8U);
-  BOOST_CHECK_EQUAL(queriesCount, 6U);
+  BOOST_CHECK_EQUAL(queriesCount, 7U);
+}
+
+BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_wildcard_synthesis)
+{
+  std::unique_ptr<SyncRes> sr;
+  initSR(sr, true);
+  g_aggressiveNSECCache = make_unique<AggressiveNSECCache>();
+
+  setDNSSECValidation(sr, DNSSECMode::ValidateAll);
+
+  primeHints();
+  /* we first ask a.powerdns.com. | A, get an answer synthesized from the wildcard,
+     then check that the aggressive NSEC cache will use the wildcard to synthesize an answer
+     for b.powerdns.com */
+  const DNSName target("a.powerdns.com.");
+  testkeysset_t keys;
+
+  auto luaconfsCopy = g_luaconfs.getCopy();
+  luaconfsCopy.dsAnchors.clear();
+  generateKeyMaterial(g_rootdnsname, DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys, luaconfsCopy.dsAnchors);
+  generateKeyMaterial(DNSName("powerdns.com."), DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys);
+
+  g_luaconfs.setState(luaconfsCopy);
+
+  size_t queriesCount = 0;
+
+  sr->setAsyncCallback([target, &queriesCount, keys](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 (type == QType::DS || type == QType::DNSKEY) {
+      if (domain != DNSName("powerdns.com.") && domain.isPartOf(DNSName("powerdns.com."))) {
+        /* no cut, NSEC3, name does not exist but there is a wildcard (the generic version will generate an exact NSEC3 for the target, which we don't want) */
+        setLWResult(res, RCode::NoError, true, false, true);
+        /* no data */
+        addRecordToLW(res, DNSName("powerdns.com."), QType::SOA, "powerdns.com. powerdns.com. 2017032301 10800 3600 604800 3600", DNSResourceRecord::AUTHORITY, 3600);
+        addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
+        /* first the closest encloser */
+        addNSEC3UnhashedRecordToLW(DNSName("powerdns.com."), DNSName("powerdns.com."), "whatever", {QType::A, QType::TXT, QType::RRSIG}, 600, res->d_records);
+        addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
+        /* then the next closer */
+        addNSEC3UnhashedRecordToLW(DNSName("+.powerdns.com."), DNSName("powerdns.com."), "v", {QType::RRSIG}, 600, res->d_records);
+        addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
+        /* a wildcard applies but does not have this type */
+        addNSEC3UnhashedRecordToLW(DNSName("*.powerdns.com."), DNSName("powerdns.com."), "whatever", {QType::A, QType::RRSIG}, 600, res->d_records);
+        addRRSIG(keys, res->d_records, DNSName("powerdns.com"), 300, false, boost::none, DNSName("*.powerdns.com"));
+        return LWResult::Result::Success;
+      }
+      else if (domain == DNSName("com.")) {
+        /* no cut */
+        return genericDSAndDNSKEYHandler(res, domain, DNSName("."), type, keys, false);
+      }
+      else if (domain == DNSName("powerdns.com.")) {
+        return genericDSAndDNSKEYHandler(res, domain, DNSName("."), type, keys);
+      }
+      else {
+        /* cut */
+        return genericDSAndDNSKEYHandler(res, domain, domain, type, keys);
+      }
+    }
+    else {
+      if (isRootServer(ip)) {
+        setLWResult(res, 0, false, false, true);
+        addRecordToLW(res, "powerdns.com.", QType::NS, "a.gtld-servers.com.", DNSResourceRecord::AUTHORITY, 3600);
+        addDS(DNSName("powerdns.com."), 300, res->d_records, keys);
+        addRRSIG(keys, res->d_records, DNSName("."), 300);
+        addRecordToLW(res, "a.gtld-servers.com.", QType::A, "192.0.2.1", DNSResourceRecord::ADDITIONAL, 3600);
+        return LWResult::Result::Success;
+      }
+      else if (ip == ComboAddress("192.0.2.1:53")) {
+        setLWResult(res, RCode::NoError, true, false, true);
+        addRecordToLW(res, domain, QType::A, "192.0.2.1");
+        addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300, false, boost::none, DNSName("*.powerdns.com"));
+        /* no need for the closest encloser since we have a positive answer expanded from a wildcard */
+        /* the next closer */
+        addNSEC3UnhashedRecordToLW(DNSName("+.powerdns.com."), DNSName("powerdns.com."), "v", {QType::RRSIG}, 600, res->d_records);
+        addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
+        /* and of course we don't deny the wildcard itself */
+        return LWResult::Result::Success;
+      }
+    }
+
+    return LWResult::Result::Timeout;
+  });
+
+  vector<DNSRecord> ret;
+  int res = sr->beginResolve(target, QType(QType::A), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NoError);
+  BOOST_CHECK_EQUAL(sr->getValidationState(), vState::Secure);
+  BOOST_REQUIRE_EQUAL(ret.size(), 4U);
+  BOOST_CHECK_EQUAL(ret.at(0).d_name, target);
+  BOOST_CHECK_EQUAL(ret.at(0).d_type, QType::A);  
+  BOOST_CHECK_EQUAL(queriesCount, 7U);
+
+  ret.clear();
+  res = sr->beginResolve(DNSName("b.powerdns.com."), QType(QType::A), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NoError);
+  BOOST_CHECK_EQUAL(sr->getValidationState(), vState::Secure);
+  BOOST_REQUIRE_EQUAL(ret.size(), 4U);
+  BOOST_CHECK_EQUAL(ret.at(0).d_name, DNSName("b.powerdns.com."));
+  BOOST_CHECK_EQUAL(ret.at(0).d_type, QType::A);  
+  BOOST_CHECK_EQUAL(queriesCount, 7U);
 }
-#endif
 
 BOOST_AUTO_TEST_SUITE_END()
index d2c1f2954ed1684d018737f7faad8607e30f1024..1c0c25989542a03669dff6058118ef38bda4ab52 100644 (file)
@@ -287,7 +287,7 @@ void computeRRSIG(const DNSSECPrivateKey& dpk, const DNSName& signer, const DNSN
   const auto& rc = dpk.getKey();
 
   rrc.d_type = signQType;
-  rrc.d_labels = signQName.countLabels() - signQName.isWildcard();
+  rrc.d_labels = signQName.countLabels() - (signQName.isWildcard() ? 1 : 0);
   rrc.d_originalttl = signTTL;
   rrc.d_siginception = inception ? *inception : (*now - 10);
   rrc.d_sigexpire = *now + sigValidity;
index 4052e1f5a3bd36cd2c1b58d79ac5b706628d2adf..d7d43c6ef19624c6fc47563983176db4d0817e6c 100644 (file)
@@ -1945,7 +1945,6 @@ bool SyncRes::doCacheCheck(const DNSName &qname, const DNSName& authname, bool w
 
   /* let's check if we have a NSEC covering that record */
   if (g_aggressiveNSECCache) {
-    cerr<<"calling get denial"<<endl;
     if (g_aggressiveNSECCache->getDenial(d_now.tv_sec, qname, qtype, ret, res, d_cacheRemote, d_routingTag, d_doDNSSEC)) {
       state = vState::Secure;
       return true;
@@ -3352,12 +3351,29 @@ RCode::rcodes_ SyncRes::updateCacheFromRecords(unsigned int depth, LWResult& lwr
 
       if (doCache) {
         g_recCache->replace(d_now.tv_sec, i->first.name, i->first.type, i->second.records, i->second.signatures, authorityRecs, i->first.type == QType::DS ? true : isAA, auth, i->first.place == DNSResourceRecord::ANSWER ? ednsmask : boost::none, d_routingTag, recordState, remoteIP);
+
+        if (needWildcardProof && recordState == vState::Secure && i->first.place == DNSResourceRecord::ANSWER && g_aggressiveNSECCache && i->first.name == qname && !i->second.signatures.empty() && !d_routingTag && !ednsmask) {
+          /* we have an answer synthesized from a wildcard and aggressive NSEC is enabled, we need to store the
+             wildcard in its non-expanded form in the cache to be able to synthesize wildcard answers later */
+          const auto& rrsig = i->second.signatures.at(0);
+          if (isWildcardExpanded(labelCount, rrsig) && !isWildcardExpandedOntoItself(i->first.name, labelCount, rrsig)) {
+            DNSName realOwner = getNSECOwnerName(i->first.name, i->second.signatures);
+            std::vector<DNSRecord> content;
+            content.reserve(i->second.records.size());
+            for (const auto& record : i->second.records) {
+              DNSRecord nonExpandedRecord(record);
+              nonExpandedRecord.d_name = realOwner;
+              content.push_back(std::move(nonExpandedRecord));
+            }
+
+            g_recCache->replace(d_now.tv_sec, realOwner, QType(i->first.type), content, i->second.signatures, /* no additional records in that case */ {}, i->first.type == QType::DS ? true : isAA, auth, boost::none, boost::none, recordState, remoteIP);
+          }
+        }
       }
     }
 
     if ((i->first.type == QType::NSEC || i->first.type == QType::NSEC3) && recordState == vState::Secure && !seenAuth.empty() && g_aggressiveNSECCache) {
-      cerr<<"Good candidate for aggressive NSEC caching"<<endl;
-
+      // Good candidate for NSEC{,3} caching
       g_aggressiveNSECCache->insertNSEC(seenAuth, i->first.name, i->second.records.at(0), i->second.signatures, i->first.type == QType::NSEC3);
     }