From: Remi Gacogne Date: Fri, 17 Apr 2026 15:40:57 +0000 (+0200) Subject: rec: Fix DNAME interaction with aggressive use of NSEC3 X-Git-Url: http://git.ipfire.org/gitweb/?a=commitdiff_plain;h=bf64487523c19df2834c6def15585bd9abeef229;p=thirdparty%2Fpdns.git rec: Fix DNAME interaction with aggressive use of NSEC3 rfc6672 section 5.3.2 "DNAME Bit in NSEC Type Map": In any negative response, the NSEC or NSEC3 [RFC5155] record type bitmap SHOULD be checked to see that there was no DNAME that could have been applied. If the DNAME bit in the type bitmap is set and the query name is a subdomain of the closest encloser that is asserted, then DNAME substitution should have been done, but the substitution has not been done as specified. Signed-off-by: Remi Gacogne --- diff --git a/pdns/recursordist/aggressive_nsec.cc b/pdns/recursordist/aggressive_nsec.cc index aa26d92c1e..19a9c87771 100644 --- a/pdns/recursordist/aggressive_nsec.cc +++ b/pdns/recursordist/aggressive_nsec.cc @@ -651,6 +651,20 @@ bool AggressiveNSECCache::getNSEC3Denial(time_t now, std::shared_ptrisSet(QType::DNAME)) { + /* rfc6672 section 5.3.2: DNAME Bit in NSEC Type Map + + In any negative response, the NSEC or NSEC3 [RFC5155] record type + bitmap SHOULD be checked to see that there was no DNAME that could + have been applied. If the DNAME bit in the type bitmap is set and + the query name is a subdomain of the closest encloser that is + asserted, then DNAME substitution should have been done, but the + substitution has not been done as specified. + */ + VLOG_NO_PREFIX(log, " but this NSEC3 has the DNAME bit set"); + return false; + } + found = true; break; } diff --git a/pdns/recursordist/test-aggressive_nsec_cc.cc b/pdns/recursordist/test-aggressive_nsec_cc.cc index e7bea9a77b..bea8758b5a 100644 --- a/pdns/recursordist/test-aggressive_nsec_cc.cc +++ b/pdns/recursordist/test-aggressive_nsec_cc.cc @@ -2216,4 +2216,166 @@ BOOST_AUTO_TEST_CASE(test_aggressive_max_nsec3_hash_cost) } } +BOOST_AUTO_TEST_CASE(test_aggressive_nsec_dname) +{ + auto cache = make_unique(10000); + g_recCache = std::make_unique(); + + const DNSName zone("powerdns.com"); + time_t now = time(nullptr); + + /* first we need a SOA */ + std::vector records; + time_t ttd = now + 30; + DNSRecord drSOA; + drSOA.d_name = zone; + drSOA.d_type = QType::SOA; + drSOA.d_class = QClass::IN; + drSOA.setContent(std::make_shared("pdns-public-ns1.powerdns.com. pieter\\.lexis.powerdns.com. 2017032301 10800 3600 604800 3600")); + drSOA.d_ttl = static_cast(ttd); // XXX truncation + drSOA.d_place = DNSResourceRecord::ANSWER; + records.push_back(drSOA); + + g_recCache->replace(now, zone, QType(QType::SOA), records, {}, {}, true, zone, std::nullopt, MemRecursorCache::NOTAG, vState::Secure); + BOOST_CHECK_EQUAL(g_recCache->size(), 1U); + + { + cache = make_unique(10000); + DNSName name("powerdns.com"); + DNSRecord rec; + rec.d_name = name; + rec.d_type = QType::NSEC; + rec.d_ttl = now + 10; + + NSECRecordContent nrc; + nrc.d_next = DNSName("z.powerdns.com"); + for (const auto& type : {QType::DNAME}) { + nrc.set(type); + } + + rec.setContent(std::make_shared(nrc)); + auto rrsig = std::make_shared("NSEC 5 3 10 20370101000000 20370101000000 24567 sub.powerdns.com. data"); + cache->insertNSEC(zone, rec.d_name, rec, {rrsig}, false); + + BOOST_CHECK_EQUAL(cache->getEntriesCount(), 1U); + + /* the cache should NOT be able to deny anything below powerdns.com */ + BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, DNSName("www.powerdns.com"), QType::A), false); + } +} + +BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_dname) +{ + auto cache = make_unique(10000); + g_recCache = std::make_unique(); + + const DNSName zone("powerdns.com"); + time_t now = time(nullptr); + + /* first we need a SOA */ + std::vector records; + time_t ttd = now + 30; + DNSRecord drSOA; + drSOA.d_name = zone; + drSOA.d_type = QType::SOA; + drSOA.d_class = QClass::IN; + drSOA.setContent(std::make_shared("pdns-public-ns1.powerdns.com. pieter\\.lexis.powerdns.com. 2017032301 10800 3600 604800 3600")); + drSOA.d_ttl = static_cast(ttd); // XXX truncation + drSOA.d_place = DNSResourceRecord::ANSWER; + records.push_back(drSOA); + + g_recCache->replace(now, zone, QType(QType::SOA), records, {}, {}, true, zone, std::nullopt, MemRecursorCache::NOTAG, vState::Secure); + BOOST_CHECK_EQUAL(g_recCache->size(), 1U); + + const std::string salt("ab"); + const unsigned int iterationsCount = 1; + { + cache = make_unique(10000); + DNSName closestEncloser("powerdns.com."); + DNSName nextCloser("www.powerdns.com."); + DNSName wildcard("*.powerdns.com."); + + { + /* closest encloser */ + std::string hashed = hashQNameWithSalt(salt, iterationsCount, closestEncloser); + DNSRecord rec; + rec.d_name = DNSName(toBase32Hex(hashed)) + zone; + rec.d_type = QType::NSEC3; + rec.d_ttl = now + 10; + + NSEC3RecordContent nrc; + nrc.d_algorithm = 1; + nrc.d_flags = 0; + nrc.d_iterations = iterationsCount; + nrc.d_salt = salt; + nrc.d_nexthash = hashed; + incrementHash(nrc.d_nexthash); + for (const auto& type : {QType::DNAME}) { + nrc.set(type); + } + + rec.setContent(std::make_shared(nrc)); + auto rrsig = std::make_shared("NSEC3 5 3 10 20370101000000 20370101000000 24567 powerdns.com. data"); + cache->insertNSEC(zone, rec.d_name, rec, {rrsig}, true); + } + + { + /* next closer */ + std::string hashed = hashQNameWithSalt(salt, iterationsCount, nextCloser); + auto before = hashed; + decrementHash(before); + DNSRecord rec; + rec.d_name = DNSName(toBase32Hex(before)) + zone; + rec.d_type = QType::NSEC3; + rec.d_ttl = now + 10; + + NSEC3RecordContent nrc; + nrc.d_algorithm = 1; + nrc.d_flags = 0; + nrc.d_iterations = iterationsCount; + nrc.d_salt = salt; + nrc.d_nexthash = hashed; + incrementHash(nrc.d_nexthash); + for (const auto& type : {QType::A}) { + nrc.set(type); + } + + rec.setContent(std::make_shared(nrc)); + auto rrsig = std::make_shared("NSEC3 5 3 10 20370101000000 20370101000000 24567 powerdns.com. data"); + cache->insertNSEC(zone, rec.d_name, rec, {rrsig}, true); + } + + { + /* wildcard */ + std::string hashed = hashQNameWithSalt(salt, iterationsCount, wildcard); + auto before = hashed; + decrementHash(before); + DNSRecord rec; + rec.d_name = DNSName(toBase32Hex(before)) + zone; + rec.d_type = QType::NSEC3; + rec.d_ttl = now + 10; + + NSEC3RecordContent nrc; + nrc.d_algorithm = 1; + nrc.d_flags = 0; + nrc.d_iterations = iterationsCount; + nrc.d_salt = salt; + nrc.d_nexthash = hashed; + incrementHash(nrc.d_nexthash); + for (const auto& type : {QType::A}) { + nrc.set(type); + } + + rec.setContent(std::make_shared(nrc)); + auto rrsig = std::make_shared("NSEC3 5 3 10 20370101000000 20370101000000 24567 powerdns.com. data"); + cache->insertNSEC(zone, rec.d_name, rec, {rrsig}, true); + } + + BOOST_CHECK_EQUAL(cache->getEntriesCount(), 3U); + + /* the cache should NOT be able to deny anything below powerdns.com */ + BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, DNSName("www.powerdns.com."), QType::A), false); + } +} + BOOST_AUTO_TEST_SUITE_END()