From: Peter van Dijk Date: Mon, 7 Apr 2025 13:25:39 +0000 (+0200) Subject: Introduce a Bind-style Views feature. X-Git-Tag: auth-5.0.0-alpha1~1^2~16 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b9afd1d6a54c2bd7a0b1712cee6cafa8e39ea83a;p=thirdparty%2Fpdns.git Introduce a Bind-style Views feature. Add interfaces to the zone cache to get the view name from the originating network address, and to retrieve the appropriate variant name to use to update a ZoneName when necessary. Allow updates from the http api. Add zonecache unit test for views. --- diff --git a/pdns/auth-zonecache.cc b/pdns/auth-zonecache.cc index f1c26f0a9c..c20fc7ddf0 100644 --- a/pdns/auth-zonecache.cc +++ b/pdns/auth-zonecache.cc @@ -23,6 +23,7 @@ #include "config.h" #endif +#include "pdns/misc.hh" #include "auth-zonecache.hh" #include "logger.hh" #include "statbag.hh" @@ -42,7 +43,7 @@ AuthZoneCache::AuthZoneCache(size_t mapsCount) : d_statnumentries = S.getPointer("zone-cache-size"); } -bool AuthZoneCache::getEntry(const ZoneName& zone, int& zoneId) +bool AuthZoneCache::getEntry(const ZoneName& zone, domainid_t& zoneId) { auto& mc = getMap(zone); bool found = false; @@ -64,6 +65,68 @@ bool AuthZoneCache::getEntry(const ZoneName& zone, int& zoneId) return found; } +#if defined(PDNS_AUTH) // [ +std::string AuthZoneCache::getViewFromNetwork(Netmask* net) +{ + string view{}; + + if (net == nullptr || net->empty()) { + return view; + } + + try { + auto nets = d_nets.read_lock(); + const auto* netview = nets->lookup(net->getNetwork()); + if (netview != nullptr) { + // Tell our caller the span of the network being hit... + *net = netview->first; + // ...and which view it covers. + view = netview->second; + } + } + catch (...) { + // this handles the "empty" case, but might hide other errors + } + + // If this network doesn't match a view, then we want to clear the netmask + // information, as our caller might submit it to the packet cache and there + // is no reason to narrow caching for views-agnostic queries. + // TODO: no longer needed once packet cache indexes on views rather than + // netmasks. + if (view.empty()) { + *net = Netmask(); + } + + return view; +} + +std::string AuthZoneCache::getVariantFromView(const ZoneName& zone, const std::string& view) +{ + string variant{}; + + if (!view.empty()) { + auto views = d_views.read_lock(); + if (views->count(view) == 1) { + const auto& viewmap = views->at(view); + if (viewmap.count(zone.operator const DNSName&()) == 1) { + variant = viewmap.at(zone.operator const DNSName&()); + } + } + } + + return variant; +} + +void AuthZoneCache::setZoneVariant(std::unique_ptr& packet) +{ + Netmask net = packet->getRealRemote(); + string view = getViewFromNetwork(&net); + packet->qdomainzone = ZoneName(packet->qdomain); + string variant = getVariantFromView(packet->qdomainzone, view); + packet->qdomainzone.setVariant(variant); +} +#endif // ] PDNS_AUTH + bool AuthZoneCache::isEnabled() const { return d_refreshinterval > 0; @@ -72,6 +135,12 @@ bool AuthZoneCache::isEnabled() const void AuthZoneCache::clear() { purgeLockedCollectionsVector(d_maps); + { + d_nets.write_lock()->clear(); + } + { + d_views.write_lock()->clear(); + } } void AuthZoneCache::replace(const vector>& zone_indices) @@ -133,6 +202,18 @@ void AuthZoneCache::replace(const vector>& zone_indice } } +void AuthZoneCache::replace(NetmaskTree nettree) +{ + auto nets = d_nets.write_lock(); + nets->swap(nettree); +} + +void AuthZoneCache::replace(ViewsMap viewsmap) +{ + auto views = d_views.write_lock(); + views->swap(viewsmap); +} + void AuthZoneCache::add(const ZoneName& zone, const int zoneId) { if (!d_refreshinterval) @@ -196,3 +277,37 @@ void AuthZoneCache::setReplacePending() pending->d_pendingUpdates.clear(); } } + +void AuthZoneCache::addToView(const std::string& view, const ZoneName& zone) +{ + const DNSName& strictZone = zone.operator const DNSName&(); + auto views = d_views.write_lock(); + AuthZoneCache::ViewsMap& map = *views; + map[view][strictZone] = zone.getVariant(); +} + +void AuthZoneCache::removeFromView(const std::string& view, const ZoneName& zone) +{ + const DNSName& strictZone = zone.operator const DNSName&(); + auto views = d_views.write_lock(); + AuthZoneCache::ViewsMap& map = *views; + if (map.count(view) == 0) { + return; // Nothing to do, we did not know about that view + } + auto& innerMap = map.at(view); + if (auto iter = innerMap.find(strictZone); iter != innerMap.end()) { + innerMap.erase(iter); + } + // else nothing to do, we did not know about that zone in that view +} + +void AuthZoneCache::updateNetwork(const Netmask& network, const std::string& view) +{ + auto nets = d_nets.write_lock(); + if (view.empty()) { + nets->erase(network); + } + else { + nets->insert_or_assign(network, view); + } +} diff --git a/pdns/auth-zonecache.hh b/pdns/auth-zonecache.hh index 02db80f9c5..d2632c20ee 100644 --- a/pdns/auth-zonecache.hh +++ b/pdns/auth-zonecache.hh @@ -26,18 +26,39 @@ #include "dnsname.hh" #include "lock.hh" #include "misc.hh" +#include "iputils.hh" class AuthZoneCache : public boost::noncopyable { public: AuthZoneCache(size_t mapsCount = 1024); + using ViewsMap = std::map>; + + // Zone maintainance void replace(const vector>& zone); + void replace(NetmaskTree nettree); + void replace(ViewsMap viewsmap); void add(const ZoneName& zone, const int zoneId); void remove(const ZoneName& zone); void setReplacePending(); //!< call this when data collection for the subsequent replace() call starts. - bool getEntry(const ZoneName& zone, int& zoneId); + // Views maintainance + void addToView(const std::string& view, const ZoneName& zone); + void removeFromView(const std::string& view, const ZoneName& zone); + + // Network maintainance + void updateNetwork(const Netmask& network, const std::string& view); + + // Zone lookup + bool getEntry(const ZoneName& zone, domainid_t& zoneId); + + // View lookup + std::string getViewFromNetwork(Netmask* net); + + // Variant lookup + std::string getVariantFromView(const ZoneName& zone, const std::string& view); + void setZoneVariant(std::unique_ptr& packet); size_t size() { return *d_statnumentries; } //!< number of entries in the cache @@ -57,6 +78,9 @@ public: void clear(); private: + SharedLockGuarded> d_nets; + SharedLockGuarded d_views; + struct CacheValue { int zoneId{-1}; diff --git a/pdns/iputils.hh b/pdns/iputils.hh index 9e07bac2fa..8783bffe1f 100644 --- a/pdns/iputils.hh +++ b/pdns/iputils.hh @@ -896,6 +896,10 @@ public: { return std::tie(d_network, d_bits) == std::tie(rhs.d_network, rhs.d_bits); } + bool operator!=(const Netmask& rhs) const + { + return !operator==(rhs); + } [[nodiscard]] bool empty() const { diff --git a/pdns/packethandler.cc b/pdns/packethandler.cc index fc79911739..4c76288e25 100644 --- a/pdns/packethandler.cc +++ b/pdns/packethandler.cc @@ -1596,7 +1596,7 @@ bool PacketHandler::opcodeQueryInner2(DNSPacket& pkt, queryState &state, bool re return true; } - if(!B.getAuth(ZoneName(state.target), pkt.qtype, &d_sd)) { + if(!B.getAuth(ZoneName(state.target), pkt.qtype, &d_sd, true, &pkt)) { DLOG(g_log<setA(false); // drop AA if we never had a SOA in the first place diff --git a/pdns/test-auth-zonecache_cc.cc b/pdns/test-auth-zonecache_cc.cc index 8466892c7c..56b5f177bd 100644 --- a/pdns/test-auth-zonecache_cc.cc +++ b/pdns/test-auth-zonecache_cc.cc @@ -42,14 +42,15 @@ BOOST_AUTO_TEST_CASE(test_replace) AuthZoneCache cache; cache.setRefreshInterval(3600); - vector> zone_indices{ + vector> zone_indices{ {ZoneName("example.org."), 1}, }; cache.setReplacePending(); cache.replace(zone_indices); - int zoneId = 0; - bool found = cache.getEntry(ZoneName("example.org."), zoneId); + domainid_t zoneId = 0; + ZoneName zone("example.org"); + bool found = cache.getEntry(zone, zoneId); if (!found || zoneId != 1) { BOOST_FAIL("zone added in replace() not found"); } @@ -60,14 +61,15 @@ BOOST_AUTO_TEST_CASE(test_add_while_pending_replace) AuthZoneCache cache; cache.setRefreshInterval(3600); - vector> zone_indices{ + vector> zone_indices{ {ZoneName("powerdns.org."), 1}}; cache.setReplacePending(); cache.add(ZoneName("example.org."), 2); cache.replace(zone_indices); - int zoneId = 0; - bool found = cache.getEntry(ZoneName("example.org."), zoneId); + domainid_t zoneId = 0; + ZoneName zone("example.org"); + bool found = cache.getEntry(zone, zoneId); if (!found || zoneId != 2) { BOOST_FAIL("zone added while replace was pending not found"); } @@ -78,14 +80,15 @@ BOOST_AUTO_TEST_CASE(test_remove_while_pending_replace) AuthZoneCache cache; cache.setRefreshInterval(3600); - vector> zone_indices{ + vector> zone_indices{ {ZoneName("powerdns.org."), 1}}; cache.setReplacePending(); cache.remove(ZoneName("powerdns.org.")); cache.replace(zone_indices); - int zoneId = 0; - bool found = cache.getEntry(ZoneName("example.org."), zoneId); + domainid_t zoneId = 0; + ZoneName zone("example.org"); + bool found = cache.getEntry(zone, zoneId); if (found) { BOOST_FAIL("zone removed while replace was pending is found"); } @@ -97,7 +100,7 @@ BOOST_AUTO_TEST_CASE(test_add_while_pending_replace_duplicate) AuthZoneCache cache; cache.setRefreshInterval(3600); - vector> zone_indices{ + vector> zone_indices{ {ZoneName("powerdns.org."), 1}, {ZoneName("example.org."), 2}, }; @@ -105,8 +108,9 @@ BOOST_AUTO_TEST_CASE(test_add_while_pending_replace_duplicate) cache.add(ZoneName("example.org."), 3); cache.replace(zone_indices); - int zoneId = 0; - bool found = cache.getEntry(ZoneName("example.org."), zoneId); + domainid_t zoneId = 0; + ZoneName zone("example.org"); + bool found = cache.getEntry(zone, zoneId); if (!found || zoneId == 0) { BOOST_FAIL("zone added while replace was pending not found"); } @@ -115,4 +119,132 @@ BOOST_AUTO_TEST_CASE(test_add_while_pending_replace_duplicate) } } +BOOST_AUTO_TEST_CASE(test_netmask) +{ + AuthZoneCache cache; + cache.setRefreshInterval(3600); + + // Declare a few zones + ZoneName bl("bug.less"); // NOLINT(readability-identifier-length) + ZoneName bli("bug.less..inner"); + ZoneName blo("bug.less..outer"); + ZoneName fb("fewer.bugs"); // NOLINT(readability-identifier-length) + ZoneName bp("bad.puns"); // NOLINT(readability-identifier-length) + ZoneName nonexistent("non.existent"); + cache.add(bli, 42); + cache.add(blo, 43); + cache.add(fb, 100); + cache.add(bp, 1000); + + // Declare a few networks + std::string inner{"inner"}; + std::string outer{"outer"}; + std::string disjoint{"disjoint"}; + Netmask innerMask("20.25.4.0/24"); + Netmask outerMask("20.25.0.0/16"); + cache.updateNetwork(outerMask, outer); + cache.updateNetwork(innerMask, inner); + + // Declare a few views + cache.addToView(inner, bli); + cache.addToView(outer, blo); + + domainid_t zoneId{0}; + std::string variant; + std::string view; + ZoneName search{}; + + // Query from no known address + bool found = cache.getEntry(bl, zoneId); + if (found) { + BOOST_FAIL("bug.less lookup should have failed"); + } + + // Query from inner zone + Netmask nm(makeComboAddress("20.25.4.24")); // NOLINT(readability-identifier-length) + view = cache.getViewFromNetwork(&nm); + BOOST_CHECK_EQUAL(view, inner); + variant = cache.getVariantFromView(bl, view); + if (nm != innerMask) { + BOOST_FAIL("bug.less lookup from inner zone reported wrong network " + nm.toString()); + } + found = cache.getEntry(ZoneName(bl.operator const DNSName&(), variant), zoneId); + if (!found) { + BOOST_FAIL("bug.less lookup from inner zone should have succeeded"); + } + if (zoneId != 42) { + BOOST_FAIL("bug.less lookup from inner zone reported wrong id " + std::to_string(zoneId)); + } + variant = cache.getVariantFromView(fb, view); + BOOST_CHECK(variant.empty()); + if (nm != innerMask) { + BOOST_FAIL("fewer.bugs lookup from inner zone reported wrong network " + nm.toString()); + } + found = cache.getEntry(fb, zoneId); + if (!found) { + BOOST_FAIL("fewer.bugs lookup from inner zone should have succeeded"); + } + if (zoneId != 100) { + BOOST_FAIL("fewer.bugs lookup from inner zone reported wrong id " + std::to_string(zoneId)); + } + + // Query from outer zone + nm = makeComboAddress("20.25.20.25"); + view = cache.getViewFromNetwork(&nm); + BOOST_CHECK_EQUAL(view, outer); + variant = cache.getVariantFromView(bl, view); + if (nm != outerMask) { + BOOST_FAIL("bug.less lookup from outer zone reported wrong network " + nm.toString()); + } + found = cache.getEntry(ZoneName(bl.operator const DNSName&(), variant), zoneId); + if (!found) { + BOOST_FAIL("bug.less lookup from outer zone should have succeeded"); + } + if (zoneId != 43) { + BOOST_FAIL("bug.less lookup from outer zone reported wrong id " + std::to_string(zoneId)); + } + variant = cache.getVariantFromView(bp, view); + BOOST_CHECK(variant.empty()); + if (nm != outerMask) { + BOOST_FAIL("bad.puns lookup from outer zone reported wrong network " + nm.toString()); + } + found = cache.getEntry(bp, zoneId); + if (!found) { + BOOST_FAIL("bad.puns lookup from outer zone should have succeeded"); + } + if (zoneId != 1000) { + BOOST_FAIL("bad.puns lookup from outer zone reported wrong id " + std::to_string(zoneId)); + } + + // Query from no particular zone, should clear netmask + nm = makeComboAddress("1.2.3.4"); + view = cache.getViewFromNetwork(&nm); + BOOST_CHECK_EQUAL(view, ""); + variant = cache.getVariantFromView(nonexistent, view); + BOOST_CHECK(variant.empty()); + found = cache.getEntry(nonexistent, zoneId); + if (found) { + BOOST_FAIL("non.existent lookup from the internet should have failed"); + } + view = cache.getViewFromNetwork(&nm); + BOOST_CHECK_EQUAL(view, ""); + variant = cache.getVariantFromView(bl, view); + BOOST_CHECK(variant.empty()); + found = cache.getEntry(bl, zoneId); + if (found) { + BOOST_FAIL("bug.less lookup from the internet should have failed"); + } + view = cache.getViewFromNetwork(&nm); + BOOST_CHECK_EQUAL(view, ""); + variant = cache.getVariantFromView(bp, view); + BOOST_CHECK(variant.empty()); + found = cache.getEntry(bp, zoneId); + if (!found) { + BOOST_FAIL("bad.puns lookup from the internet should have succeeded"); + } + if (zoneId != 1000) { + BOOST_FAIL("bad.puns lookup from the internet reported wrong id " + std::to_string(zoneId)); + } +} + BOOST_AUTO_TEST_SUITE_END(); diff --git a/pdns/ueberbackend.cc b/pdns/ueberbackend.cc index 655e7a5936..8916ccbdf4 100644 --- a/pdns/ueberbackend.cc +++ b/pdns/ueberbackend.cc @@ -341,6 +341,32 @@ void UeberBackend::updateZoneCache() } } g_zoneCache.replace(zone_indices); + + NetmaskTree nettree; + for (auto& backend : backends) { + vector> nettag; + backend->networkList(nettag); + for (auto& [net, tag] : nettag) { + nettree.insert_or_assign(net, tag); + } + } + g_zoneCache.replace(nettree); // FIXME: this needs some smart pending stuff too + + AuthZoneCache::ViewsMap viewsmap; + for (auto& backend : backends) { + vector views; + backend->viewList(views); + for (auto& view : views) { + vector zones; + backend->viewListZones(view, zones); + for (ZoneName& zone : zones) { + auto zonename = DNSName(zone); + auto variant = zone.getVariant(); + viewsmap[view][zonename] = variant; + } + } + } + g_zoneCache.replace(viewsmap); } void UeberBackend::rediscover(string* status) @@ -493,7 +519,7 @@ static bool foundTarget(const ZoneName& target, const ZoneName& shorter, const Q return false; } -bool UeberBackend::getAuth(const ZoneName& target, const QType& qtype, SOAData* soaData, bool cachedOk) +bool UeberBackend::getAuth(const ZoneName& target, const QType& qtype, SOAData* soaData, bool cachedOk, DNSPacket* pkt_p) { // A backend can respond to our authority request with the 'best' match it // has. For example, when asked for a.b.c.example.com. it might respond with @@ -506,6 +532,20 @@ bool UeberBackend::getAuth(const ZoneName& target, const QType& qtype, SOAData* ZoneName shorter(target); vector> bestMatches(backends.size(), pair(target.operator const DNSName&().wirelength() + 1, SOAData())); + Netmask remote; + if (pkt_p != nullptr) { + remote = pkt_p->getRealRemote(); + } + std::string view{}; + if (g_zoneCache.isEnabled()) { + Netmask _remote(remote); + view = g_zoneCache.getViewFromNetwork(&_remote); + // Remember the view netmask, if applicable, for ECS responses. + if (!view.empty() && pkt_p != nullptr) { + pkt_p->d_span = _remote; + } + } + bool first = true; while (first || shorter.chopOff()) { first = false; @@ -513,8 +553,13 @@ bool UeberBackend::getAuth(const ZoneName& target, const QType& qtype, SOAData* int zoneId{-1}; if (cachedOk && g_zoneCache.isEnabled()) { - if (g_zoneCache.getEntry(shorter, zoneId)) { - if (fillSOAFromZoneRecord(shorter, zoneId, soaData)) { + std::string variant = g_zoneCache.getVariantFromView(shorter, view); + ZoneName _shorter(shorter.operator const DNSName&(), variant); + if (g_zoneCache.getEntry(_shorter, zoneId)) { + if (fillSOAFromZoneRecord(_shorter, zoneId, soaData)) { + // Need to invoke foundTarget() with the same variant part in the + // first two arguments, since they are compared as ZoneName, hence + // the use of `shorter' rather than `_shorter' here. if (foundTarget(target, shorter, qtype, soaData, found)) { return true; } diff --git a/pdns/ueberbackend.hh b/pdns/ueberbackend.hh index ff1762e1b1..f727e8d7cc 100644 --- a/pdns/ueberbackend.hh +++ b/pdns/ueberbackend.hh @@ -100,7 +100,7 @@ public: void lookupEnd(); /** Determines if we are authoritative for a zone, and at what level */ - bool getAuth(const ZoneName& target, const QType& qtype, SOAData* soaData, bool cachedOk = true); + bool getAuth(const ZoneName& target, const QType& qtype, SOAData* soaData, bool cachedOk = true, DNSPacket* pkt_p = nullptr); /** Load SOA info from backends, ignoring the cache.*/ bool getSOAUncached(const ZoneName& domain, SOAData& soaData); void getAllDomains(vector* domains, bool getSerial, bool include_disabled); diff --git a/pdns/ws-auth.cc b/pdns/ws-auth.cc index 5f110f6e23..64a45758ee 100644 --- a/pdns/ws-auth.cc +++ b/pdns/ws-auth.cc @@ -2714,6 +2714,10 @@ static void apiServerViewsPOST(HttpRequest* req, HttpResponse* resp) if (!domainInfo.backend->viewAddZone(view, zonename)) { throw ApiException("Failed to add " + zonename.toString() + " to view " + view); } + // Notify zone cache of the new association + if (g_zoneCache.isEnabled()) { + g_zoneCache.addToView(view, zonename); + } resp->body = ""; resp->status = 204; @@ -2728,6 +2732,10 @@ static void apiServerViewsDELETE(HttpRequest* req, HttpResponse* resp) if (!zoneData.domainInfo.backend->viewDelZone(view, zoneData.zoneName)) { throw ApiException("Failed to remove " + zoneData.zoneName.toString() + " from view " + view); } + // Notify zone cache of the removed association + if (g_zoneCache.isEnabled()) { + g_zoneCache.removeFromView(view, zoneData.zoneName); + } resp->body = ""; resp->status = 204; @@ -2784,6 +2792,10 @@ static void apiServerNetworksPUT(HttpRequest* req, HttpResponse* resp) if (!backend.networkSet(network, view)) { throw ApiException("Failed to setup view " + view + " for network " + network.toString()); } + // Notify zone cache of the new association + if (g_zoneCache.isEnabled()) { + g_zoneCache.updateNetwork(network, view); + } resp->body = ""; resp->status = 204;