]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Introduce a Bind-style Views feature.
authorPeter van Dijk <peter.van.dijk@powerdns.com>
Mon, 7 Apr 2025 13:25:39 +0000 (15:25 +0200)
committerMiod Vallat <miod.vallat@powerdns.com>
Mon, 26 May 2025 11:49:12 +0000 (13:49 +0200)
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.

pdns/auth-zonecache.cc
pdns/auth-zonecache.hh
pdns/iputils.hh
pdns/packethandler.cc
pdns/test-auth-zonecache_cc.cc
pdns/ueberbackend.cc
pdns/ueberbackend.hh
pdns/ws-auth.cc

index f1c26f0a9cae3324fc221e9c837f47bc9b599c5a..c20fc7ddf01734fb5fa6d7f8561f2e584126f236 100644 (file)
@@ -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<DNSPacket>& 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<std::tuple<ZoneName, int>>& zone_indices)
@@ -133,6 +202,18 @@ void AuthZoneCache::replace(const vector<std::tuple<ZoneName, int>>& zone_indice
   }
 }
 
+void AuthZoneCache::replace(NetmaskTree<string> 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);
+  }
+}
index 02db80f9c5205f309ae971231e1479a238474490..d2632c20ee7d71482dc91fe665d51bdbf246f94f 100644 (file)
 #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<std::string, std::map<DNSName, std::string>>;
+
+  // Zone maintainance
   void replace(const vector<std::tuple<ZoneName, int>>& zone);
+  void replace(NetmaskTree<string> 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<DNSPacket>& packet);
 
   size_t size() { return *d_statnumentries; } //!< number of entries in the cache
 
@@ -57,6 +78,9 @@ public:
   void clear();
 
 private:
+  SharedLockGuarded<NetmaskTree<string>> d_nets;
+  SharedLockGuarded<ViewsMap> d_views;
+
   struct CacheValue
   {
     int zoneId{-1};
index 9e07bac2fad9d128787ce948e6f6ceb6f1d24656..8783bffe1f032b574c1c58ef87f67954610ddb95 100644 (file)
@@ -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
   {
index fc799117396ef88c5b2387a5515b31c396587b14..4c76288e25a805aed631219bf2088da41f1e7d04 100644 (file)
@@ -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<<Logger::Error<<"We have no authority over zone '"<<state.target<<"'"<<endl);
     if (!retargeted) {
       state.r->setA(false); // drop AA if we never had a SOA in the first place
index 8466892c7cf09ce45150256084e29a7ce3009771..56b5f177bd0238ccb7c28ef492d9709dbddd9533 100644 (file)
@@ -42,14 +42,15 @@ BOOST_AUTO_TEST_CASE(test_replace)
   AuthZoneCache cache;
   cache.setRefreshInterval(3600);
 
-  vector<std::tuple<ZoneName, int>> zone_indices{
+  vector<std::tuple<ZoneName, domainid_t>> 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<std::tuple<ZoneName, int>> zone_indices{
+  vector<std::tuple<ZoneName, domainid_t>> 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<std::tuple<ZoneName, int>> zone_indices{
+  vector<std::tuple<ZoneName, domainid_t>> 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<std::tuple<ZoneName, int>> zone_indices{
+  vector<std::tuple<ZoneName, domainid_t>> 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();
index 655e7a59360768ae79924ff74fc4d4d72a118a92..8916ccbdf410e2e98e40e75f79d60ea3177c4185 100644 (file)
@@ -341,6 +341,32 @@ void UeberBackend::updateZoneCache()
     }
   }
   g_zoneCache.replace(zone_indices);
+
+  NetmaskTree<string> nettree;
+  for (auto& backend : backends) {
+    vector<pair<Netmask, string>> 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<string> views;
+    backend->viewList(views);
+    for (auto& view : views) {
+      vector<ZoneName> 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<pair<size_t, SOAData>> 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;
           }
index ff1762e1b1e7e3efb87ddc150df8ee6d9ac64050..f727e8d7cc82bf2b435e86888dfef12d344ef361 100644 (file)
@@ -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<DomainInfo>* domains, bool getSerial, bool include_disabled);
index 5f110f6e23771769d1437b79c4bdc87ec69dee03..64a45758ee97892657434f3ef25f7e7ce072dba9 100644 (file)
@@ -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;