]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Preliminary views supports for LMDB.
authorPeter van Dijk <peter.van.dijk@powerdns.com>
Mon, 7 Apr 2025 13:24:50 +0000 (15:24 +0200)
committerMiod Vallat <miod.vallat@powerdns.com>
Mon, 26 May 2025 11:49:12 +0000 (13:49 +0200)
modules/lmdbbackend/lmdbbackend.cc
modules/lmdbbackend/lmdbbackend.hh
pdns/iputils.hh

index a9b7e25f82e8152441fefb3adffa1c8d304704a1..a436965798606e5f672642f5cb6d103c6ada839e 100644 (file)
 #include "pdns/base32.hh"
 #include "pdns/dns.hh"
 #include "pdns/dnsbackend.hh"
+#include "pdns/dnsname.hh"
 #include "pdns/dnspacket.hh"
 #include "pdns/dnssecinfra.hh"
 #include "pdns/logger.hh"
+#include "pdns/misc.hh"
 #include "pdns/pdnsexception.hh"
 #include "pdns/uuid-utils.hh"
 #include <boost/archive/binary_iarchive.hpp>
@@ -56,7 +58,8 @@
 
 // List the class version here. Default is 0
 BOOST_CLASS_VERSION(LMDBBackend::KeyDataDB, 1)
-BOOST_CLASS_VERSION(DomainInfo, 1)
+BOOST_CLASS_VERSION(ZoneName, 1)
+BOOST_CLASS_VERSION(DomainInfo, 2)
 
 static bool s_first = true;
 static uint32_t s_shards = 0;
@@ -667,6 +670,8 @@ LMDBBackend::LMDBBackend(const std::string& suffix)
     throw std::runtime_error("LMDB backend does not support multiple instances");
   }
 
+  d_views = ::arg().mustDo("views"); // This is a global setting
+
   setArgPrefix("lmdb" + suffix);
 
   string syncMode = toLower(getArg("sync-mode"));
@@ -742,6 +747,8 @@ LMDBBackend::LMDBBackend(const std::string& suffix)
       d_tmeta = std::make_shared<tmeta_t>(d_tdomains->getEnv(), "metadata_v5");
       d_tkdb = std::make_shared<tkdb_t>(d_tdomains->getEnv(), "keydata_v5");
       d_ttsig = std::make_shared<ttsig_t>(d_tdomains->getEnv(), "tsig_v5");
+      d_tnetworks = d_tdomains->getEnv()->openDB("networks_v5", MDB_CREATE);
+      d_tviews = d_tdomains->getEnv()->openDB("views_v5", MDB_CREATE);
 
       auto pdnsdbi = d_tdomains->getEnv()->openDB("pdns", MDB_CREATE);
 
@@ -804,6 +811,8 @@ LMDBBackend::LMDBBackend(const std::string& suffix)
     d_tmeta = std::make_shared<tmeta_t>(d_tdomains->getEnv(), "metadata_v5");
     d_tkdb = std::make_shared<tkdb_t>(d_tdomains->getEnv(), "keydata_v5");
     d_ttsig = std::make_shared<ttsig_t>(d_tdomains->getEnv(), "tsig_v5");
+    d_tnetworks = d_tdomains->getEnv()->openDB("networks_v5", MDB_CREATE);
+    d_tviews = d_tdomains->getEnv()->openDB("views_v5", MDB_CREATE);
   }
   d_trecords.resize(s_shards);
   d_dolog = ::arg().mustDo("query-logging");
@@ -826,6 +835,15 @@ LMDBBackend::~LMDBBackend()
   }
 }
 
+unsigned int LMDBBackend::getCapabilities()
+{
+  unsigned int caps = CAP_DNSSEC | CAP_DIRECT | CAP_LIST | CAP_CREATE;
+  if (d_views) {
+    caps |= CAP_VIEWS;
+  }
+  return caps;
+}
+
 namespace boost
 {
 namespace serialization
@@ -858,39 +876,29 @@ namespace serialization
   template <class Archive>
   void save(Archive& arc, const ZoneName& zone, const unsigned int /* version */)
   {
-    // Because ZoneName is an object containing a single DNSName field,
-    // we can't naively write
-    //   arc & zone.operator const DNSName&();
-    // because the serialization actually writes the class version number in
-    // front of our provided serialization, thus causing it to be larger than
-    // the DNSName serialization. In order to remain interoperable with
-    // existing serializations, we skip the DNSName's own version number and
-    // directly serialize its contents.
-    const DNSName& name = zone.operator const DNSName&();
-    if (name.empty()) {
-      arc& std::string(); // it's arc.operator& but clang-format is confused here
-    }
-    else {
-      arc & name.toDNSStringLC();
-    }
+    arc & zone.operator const DNSName&();
+    arc & zone.getVariant();
   }
 
   template <class Archive>
-  void load(Archive& arc, ZoneName& zone, const unsigned int /* version */)
+  void load(Archive& arc, ZoneName& zone, const unsigned int version)
   {
-    // Similarly to save() above, we can't write
-    //   DNSName tmp;
-    //   arc & tmp;
-    //   zone = ZoneName(tmp);
-    // but have to reconstruct a DNSName from a string.
-    string tmp;
-    arc & tmp;
-    if (tmp.empty()) {
-      zone = ZoneName();
-    }
-    else {
-      zone = ZoneName(DNSName(tmp.c_str(), tmp.size(), 0, false));
+    if (version == 0) { // for schemas up to 5, ZoneName serialized as DNSName
+      std::string tmp{};
+      arc & tmp;
+      if (tmp.empty()) {
+        zone = ZoneName();
+      }
+      else {
+        zone = ZoneName(DNSName(tmp.c_str(), tmp.size(), 0, false));
+      }
+      return;
     }
+    DNSName tmp;
+    std::string variant{};
+    arc & tmp;
+    arc & variant;
+    zone = ZoneName(tmp, variant);
   }
 
   template <class Archive>
@@ -910,7 +918,14 @@ namespace serialization
   template <class Archive>
   void load(Archive& ar, DomainInfo& g, const unsigned int version)
   {
-    ar & g.zone;
+    if (version >= 2) {
+      ar & g.zone;
+    }
+    else {
+      DNSName tmp;
+      ar & tmp;
+      new (&g.zone) ZoneName(tmp);
+    }
     ar & g.last_check;
     ar & g.account;
     ar & g.primaries;
@@ -919,13 +934,26 @@ namespace serialization
     g.id = static_cast<domainid_t>(domainId);
     ar & g.notified_serial;
     ar & g.kind;
-    if (version >= 1) {
-      ar & g.options;
-      ar & g.catalog;
-    }
-    else {
+    switch (version) {
+    case 0:
+      // These fields did not exist.
       g.options.clear();
       g.catalog.clear();
+      break;
+    case 1:
+      // These fields did exist, but catalog as DNSName only.
+      ar & g.options;
+      {
+        DNSName tmp;
+        ar & tmp;
+        g.catalog = ZoneName(tmp);
+      }
+      break;
+    default:
+      // These fields exist, with catalog as ZoneName.
+      ar & g.options;
+      ar & g.catalog;
+      break;
     }
   }
 
@@ -1295,6 +1323,181 @@ bool LMDBBackend::replaceComments([[maybe_unused]] domainid_t domain_id, [[maybe
   return comments.empty();
 }
 
+// FIXME: this is not very efficient
+static DNSName keyUnconv(std::string& instr)
+{
+  // instr is now com0example0
+  vector<string> labels;
+  boost::split(labels, instr, [](char chr) { return chr == '\0'; });
+
+  // we get a spurious empty label at the end, drop it
+  labels.resize(labels.size() - 1);
+
+  if (labels.size() == 1 && labels[0].empty()) {
+    // this is the root
+    return g_rootdnsname;
+  }
+
+  DNSName tmp;
+
+  for (auto const& label : labels) {
+    tmp.appendRawLabel(label);
+  }
+  return tmp.labelReverse();
+}
+
+static std::string makeBadDataExceptionMessage(const std::string& where, std::exception& exc, MDBOutVal& key, MDBOutVal& val)
+{
+  ostringstream msg;
+  msg << "during " << where << ", got exception (" << exc.what() << "), ";
+  msg << "key: " << makeHexDump(key.getNoStripHeader<string>()) << ", ";
+  msg << "value: " << makeHexDump(val.get<string>());
+
+  return msg.str();
+}
+
+void LMDBBackend::viewList(vector<string>& result)
+{
+  auto txn = d_tdomains->getEnv()->getROTransaction();
+
+  auto cursor = txn->getROCursor(d_tviews);
+
+  MDBOutVal key{}; // <view, dnsname>
+  MDBOutVal val{}; // <variant>
+
+  auto ret = cursor.first(key, val);
+
+  if (ret == MDB_NOTFOUND) {
+    return;
+  }
+
+  do {
+    string view;
+    string zone;
+    try {
+      std::tie(view, zone) = splitField(key.getNoStripHeader<string>(), '\x0');
+      auto variant = val.get<string>();
+      result.push_back(view);
+    }
+    catch (std::exception& e) {
+      throw PDNSException(makeBadDataExceptionMessage("viewList", e, key, val));
+    }
+
+    string inkey{view + string(1, (char)1)};
+    MDBInVal bound{inkey};
+    ret = cursor.lower_bound(bound, key, val); // this should use some lower bound thing to skip to the next view, also avoiding duplicates in `result`
+  } while (ret != MDB_NOTFOUND);
+}
+
+void LMDBBackend::viewListZones(const string& inview, vector<ZoneName>& result)
+{
+  result.clear();
+
+  auto txn = d_tdomains->getEnv()->getROTransaction();
+
+  auto cursor = txn->getROCursor(d_tviews);
+
+  string inkey{inview + string(1, (char)0)};
+  MDBInVal prefix{inkey};
+  MDBOutVal key{}; // <view, dnsname>
+  MDBOutVal val{}; // <variant>
+
+  auto ret = cursor.prefix(prefix, key, val);
+
+  if (ret == MDB_NOTFOUND) {
+    return;
+  }
+
+  do {
+    try {
+      auto [view, _zone] = splitField(key.getNoStripHeader<string>(), '\x0');
+      auto variant = val.get<string>();
+      auto zone = keyUnconv(_zone);
+      result.emplace_back(ZoneName(zone, variant));
+    }
+    catch (std::exception& e) {
+      throw PDNSException(makeBadDataExceptionMessage("viewListZones", e, key, val));
+    }
+
+    ret = cursor.next(key, val);
+  } while (ret != MDB_NOTFOUND);
+}
+
+// TODO: make this add-or-del to reduce code duplication?
+bool LMDBBackend::viewAddZone(const string& view, const ZoneName& zone)
+{
+  auto txn = d_tdomains->getEnv()->getRWTransaction();
+
+  string key = view + string(1, (char)0) + keyConv(zone.operator const DNSName&());
+  string val = zone.getVariant(); // variant goes here
+
+  txn->put(d_tviews, key, val);
+  txn->commit();
+
+  return true;
+}
+
+bool LMDBBackend::viewDelZone(const string& view, const ZoneName& zone)
+{
+  auto txn = d_tdomains->getEnv()->getRWTransaction();
+
+  string key = view + string(1, (char)0) + keyConv(zone.operator const DNSName&());
+  // string val = "foo"; // variant goes here
+
+  txn->del(d_tviews, key);
+  txn->commit();
+
+  return true;
+}
+
+bool LMDBBackend::networkSet(const Netmask& net, std::string& view)
+{
+  auto txn = d_tdomains->getEnv()->getRWTransaction();
+
+  if (view.empty()) {
+    txn->del(d_tnetworks, net.toByteString());
+  }
+  else {
+    txn->put(d_tnetworks, net.toByteString(), view);
+  }
+  txn->commit();
+
+  return true;
+}
+
+bool LMDBBackend::networkList(vector<pair<Netmask, string>>& networks)
+{
+  networks.clear();
+
+  auto txn = d_tdomains->getEnv()->getROTransaction();
+
+  auto cursor = txn->getROCursor(d_tnetworks);
+
+  MDBOutVal netval{};
+  MDBOutVal viewval{};
+
+  auto ret = cursor.first(netval, viewval);
+
+  if (ret == MDB_NOTFOUND) {
+    return true;
+  }
+
+  do {
+    try {
+      auto net = Netmask(netval.getNoStripHeader<string>(), Netmask::byteString);
+      auto view = viewval.get<string>();
+      networks.emplace_back(std::make_pair(net, view));
+    }
+    catch (std::exception& e) {
+      throw PDNSException(makeBadDataExceptionMessage("networkList", e, netval, viewval));
+    }
+
+    ret = cursor.next(netval, viewval);
+  } while (ret != MDB_NOTFOUND);
+
+  return true;
+}
+
 // tempting to templatize these two functions but the pain is not worth it
 // NOLINTNEXTLINE(readability-identifier-length)
 std::shared_ptr<LMDBBackend::RecordsRWTransaction> LMDBBackend::getRecordsRWTransaction(domainid_t id)
@@ -1622,6 +1825,12 @@ bool LMDBBackend::getSerial(DomainInfo& di)
 
 bool LMDBBackend::getDomainInfo(const ZoneName& domain, DomainInfo& info, bool getserial)
 {
+  // If caller asks about a zone with variant, but views are not enabled,
+  // punt.
+  if (domain.hasVariant() && !d_views) {
+    return false;
+  }
+
   {
     auto txn = d_tdomains->getROTransaction();
     // auto range = txn.prefix_range<0>(domain);
@@ -1784,6 +1993,11 @@ void LMDBBackend::getAllDomains(vector<DomainInfo>* domains, bool /* doSerial */
       return false;
     }
 
+    // Skip domains with variants if views are disabled.
+    if (di.zone.hasVariant() && !d_views) {
+      return false;
+    }
+
     return true;
   });
 }
index df598aefa4237d58f592f94c5932b3623228a891..47b28376468f386086aa8eb3c1b6bacbb0e9a2b3 100644 (file)
@@ -59,7 +59,12 @@ std::string keyConv(const T& t)
 template <class T, typename std::enable_if<std::is_same<T, ZoneName>::value, T>::type* = nullptr>
 std::string keyConv(const T& t)
 {
-  return keyConv(t.operator const DNSName&());
+  if (t.hasVariant()) {
+    return keyConv(t.operator const DNSName&()) + string(1, (char)0) + keyConv(t.getVariant());
+  }
+  else {
+    return keyConv(t.operator const DNSName&());
+  }
 }
 
 class LMDBBackend : public DNSBackend
@@ -68,7 +73,7 @@ public:
   explicit LMDBBackend(const string& suffix = "");
   ~LMDBBackend();
 
-  unsigned int getCapabilities() override { return CAP_DNSSEC | CAP_DIRECT | CAP_LIST | CAP_CREATE; }
+  unsigned int getCapabilities() override;
   bool list(const ZoneName& target, domainid_t domainId, bool include_disabled) override;
 
   bool getDomainInfo(const ZoneName& domain, DomainInfo& info, bool getserial = true) override;
@@ -83,6 +88,14 @@ public:
   bool replaceRRSet(domainid_t domain_id, const DNSName& qname, const QType& qt, const vector<DNSResourceRecord>& rrset) override;
   bool replaceComments(domainid_t domain_id, const DNSName& qname, const QType& qt, const vector<Comment>& comments) override;
 
+  void viewList(vector<string>& /* result */) override;
+  void viewListZones(const string& /* view */, vector<ZoneName>& /* result */) override;
+  bool viewAddZone(const string& /* view */, const ZoneName& /* zone */) override;
+  bool viewDelZone(const string& /* view */, const ZoneName& /* zone */) override;
+
+  bool networkSet(const Netmask& net, std::string& view) override;
+  bool networkList(vector<pair<Netmask, string>>& networks) override;
+
   void getAllDomains(vector<DomainInfo>* domains, bool doSerial, bool include_disabled) override;
   void lookup(const QType& type, const DNSName& qdomain, domainid_t zoneId, DNSPacket* p = nullptr) override { lookupInternal(type, qdomain, zoneId, p, false); }
   void APILookup(const QType& type, const DNSName& qdomain, domainid_t zoneId, bool include_disabled = false) override { lookupInternal(type, qdomain, zoneId, nullptr, include_disabled); }
@@ -306,6 +319,8 @@ private:
   shared_ptr<tmeta_t> d_tmeta;
   shared_ptr<tkdb_t> d_tkdb;
   shared_ptr<ttsig_t> d_ttsig;
+  MDBDbi d_tnetworks;
+  MDBDbi d_tviews;
 
   shared_ptr<RecordsROTransaction> d_rotxn; // for lookup and list
   shared_ptr<RecordsRWTransaction> d_rwtxn; // for feedrecord within begin/aborttransaction
@@ -337,5 +352,6 @@ private:
   bool d_dolog;
   bool d_random_ids;
   bool d_handle_dups;
+  bool d_views;
   DTime d_dtime; // used only for logging
 };
index 425027de514a84192f6809133123e14d3ff47b1b..9e07bac2fad9d128787ce948e6f6ceb6f1d24656 100644 (file)
@@ -337,6 +337,9 @@ union ComboAddress
         return host.data();
       }
     }
+    else {
+      return "invalid";
+    }
     return "invalid " + stringerror();
   }
 
@@ -736,20 +739,36 @@ public:
     }
   }
 
+  enum stringType
+  {
+    humanString,
+    byteString,
+  };
   //! Constructor supplies the mask, which cannot be changed
-  Netmask(const string& mask)
+  Netmask(const string& mask, stringType type = humanString)
   {
-    pair<string, string> split = splitField(mask, '/');
-    d_network = makeComboAddress(split.first);
+    if (type == byteString) {
+      uint8_t afi = mask.at(0);
+      size_t len = afi == 4 ? 4 : 16;
+      uint8_t bits = mask.at(len + 1);
 
-    if (!split.second.empty()) {
-      setBits(pdns::checked_stoi<uint8_t>(split.second));
-    }
-    else if (d_network.sin4.sin_family == AF_INET) {
-      setBits(32);
+      d_network = makeComboAddressFromRaw(afi, mask.substr(1, len));
+
+      setBits(bits);
     }
     else {
-      setBits(128);
+      pair<string, string> split = splitField(mask, '/');
+      d_network = makeComboAddress(split.first);
+
+      if (!split.second.empty()) {
+        setBits(pdns::checked_stoi<uint8_t>(split.second));
+      }
+      else if (d_network.sin4.sin_family == AF_INET) {
+        setBits(32);
+      }
+      else {
+        setBits(128);
+      }
     }
   }
 
@@ -814,6 +833,17 @@ public:
     return d_network.toStringNoInterface();
   }
 
+  [[nodiscard]] string toByteString() const
+  {
+    ostringstream tmp;
+
+    tmp << (d_network.isIPv4() ? "\x04" : "\x06")
+        << d_network.toByteString()
+        << getBits();
+
+    return tmp.str();
+  }
+
   [[nodiscard]] const ComboAddress& getNetwork() const
   {
     return d_network;