]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Implement an optional RRSet TTL check.
authorMiod Vallat <miod.vallat@powerdns.com>
Wed, 21 Jan 2026 13:56:06 +0000 (14:56 +0100)
committerMiod Vallat <miod.vallat@powerdns.com>
Wed, 21 Jan 2026 14:43:16 +0000 (15:43 +0100)
Perform such checks in pdnsutil and the HTTP API when performing RRSet
modifications.

Signed-off-by: Miod Vallat <miod.vallat@powerdns.com>
pdns/check-zone.cc
pdns/check-zone.hh
pdns/pdnsutil.cc
pdns/ws-auth.cc

index 64d854029ee75ede78967dba9559492ea462fcfb..4add9bf07e1a9e9d1d6724306e27ef66017ec2a2 100644 (file)
@@ -78,6 +78,18 @@ void checkRRSet(const vector<DNSResourceRecord>& oldrrs, vector<DNSResourceRecor
         if (previous.content == rec.content) {
           errors.emplace_back(std::make_pair(rec, std::string{"duplicate record with content \""} + rec.content + "\""));
         }
+        // Enforce identical TTLs for all records with the same name and type,
+        // if required. This is optional because some callers are able to
+        // compute allrrs in a way which already enforces this, and therefore
+        // it is useless to check a second time.
+        if ((flags & RRSET_CHECK_TTL) != 0) {
+          if (rec.ttl != previous.ttl) {
+            // This error message may be misleading if a TTL discrepancy already
+            // exists in the RRset, as it might blame an existing record rather
+            // than those being added. ¯\_(ツ)_/¯
+            errors.emplace_back(std::make_pair(rec, std::string{"uses a different TTL value than the remainder of the RRset"}));
+          }
+        }
       }
       else {
         if (QType::exclusiveEntryTypes.count(rec.qtype.getCode()) != 0
index 9e63d87f7c21c422f0ec279b60c3500053f1c57f..2ddfc979f253d860d9d3bf270b1cc78c154ec96d 100644 (file)
@@ -37,6 +37,7 @@ bool validateViewName(std::string_view name, std::string& error);
 enum RRSetFlags : unsigned int
 {
   RRSET_ALLOW_UNDERSCORES = 1 << 0, // Allow underscore in names
+  RRSET_CHECK_TTL = 1 << 1, // Check the TTL of the RRset
 };
 
 // Returns the list of errors found for new records which violate RRset
index 5361d29eb25f25557434e26024bcc109918c17d6..2bdd03444721a7423b16fe57ef4381825fde2dbd 100644 (file)
@@ -2770,9 +2770,9 @@ static int addOrReplaceRecord(bool isAdd, const vector<string>& cmds)
   }
 
   std::vector<std::pair<DNSResourceRecord, string>> errors;
-  Check::RRSetFlags flags{0};
+  Check::RRSetFlags flags{Check::RRSET_CHECK_TTL};
   if (allowUnderscores) {
-    flags = Check::RRSET_ALLOW_UNDERSCORES;
+    flags = static_cast<Check::RRSetFlags>(flags | Check::RRSET_ALLOW_UNDERSCORES);
   }
   Check::checkRRSet(oldrrs, newrrs, zone, flags, errors);
   oldrrs.clear(); // no longer needed
index 1ab78613febb5736e6979864e77fde17dfebf4e2..6ed1ecb6b6ed6ed5286a92ec35ca6e32c59d970e 100644 (file)
@@ -1685,14 +1685,10 @@ static bool areUnderscoresAllowed(const ZoneName& zonename, DNSBackend& backend)
 
 // Wrapper around checkRRSet; returns true if all checks successful, false if
 // not, in which case the response body and status have been filled up.
-static bool checkNewRecords(HttpResponse* resp, vector<DNSResourceRecord>& records, const ZoneName& zone, bool allowUnderscores)
+static bool checkNewRecords(HttpResponse* resp, vector<DNSResourceRecord>& records, const ZoneName& zone, Check::RRSetFlags flags)
 {
   std::vector<std::pair<DNSResourceRecord, string>> errors;
 
-  Check::RRSetFlags flags{0};
-  if (allowUnderscores) {
-    flags = Check::RRSET_ALLOW_UNDERSCORES;
-  }
   Check::checkRRSet({}, records, zone, flags, errors);
   if (errors.empty()) {
     return true;
@@ -2067,7 +2063,9 @@ static void apiServerZonesPOST(HttpRequest* req, HttpResponse* resp)
     }
   }
 
-  if (!checkNewRecords(resp, new_records, zonename, false)) { // no RFC1123-CONFORMANCE metadata on new zones
+  // Flags = 0, as new zones do not have RFC1123-CONFORMANCE metadata yet, and
+  // all records use the same default ttl value.
+  if (!checkNewRecords(resp, new_records, zonename, static_cast<Check::RRSetFlags>(0))) {
     return;
   }
 
@@ -2244,7 +2242,11 @@ static void apiServerZoneDetailPUT(HttpRequest* req, HttpResponse* resp)
     }
 
     bool allowUnderscores = areUnderscoresAllowed(zoneData.zoneName, *zoneData.domainInfo.backend);
-    if (!checkNewRecords(resp, new_records, zoneData.zoneName, allowUnderscores)) {
+    Check::RRSetFlags flags{Check::RRSET_CHECK_TTL};
+    if (allowUnderscores) {
+      flags = static_cast<Check::RRSetFlags>(flags | Check::RRSET_ALLOW_UNDERSCORES);
+    }
+    if (!checkNewRecords(resp, new_records, zoneData.zoneName, flags)) {
       return;
     }
 
@@ -2545,7 +2547,12 @@ static applyResult applyReplace(const DomainInfo& domainInfo, const ZoneName& zo
           soa.edit_done = increaseSOARecord(resourceRecord, soa.edit_api_kind, soa.edit_kind, zonename);
         }
       }
-      if (!checkNewRecords(resp, new_records, zonename, allowUnderscores)) {
+      // All records use the same TTL, no need to check for discrepancy.
+      Check::RRSetFlags flags{0};
+      if (allowUnderscores) {
+        flags = Check::RRSET_ALLOW_UNDERSCORES;
+      }
+      if (!checkNewRecords(resp, new_records, zonename, flags)) {
         // Proper error response has been set up, no need to do anything further.
         return ABORT;
       }
@@ -2578,6 +2585,7 @@ static applyResult applyReplace(const DomainInfo& domainInfo, const ZoneName& zo
   return SUCCESS;
 }
 
+// Apply a PRUNE or EXTEND changetype.
 static applyResult applyPruneOrExtend(const DomainInfo& domainInfo, const ZoneName& zonename, const Json& container, DNSName& qname, QType& qtype, bool allowUnderscores, soaEditSettings& soa, HttpResponse* resp, changeType operationType, std::vector<DNSResourceRecord>& rrset)
 {
   if (!container["records"].is_array()) {
@@ -2598,7 +2606,11 @@ static applyResult applyPruneOrExtend(const DomainInfo& domainInfo, const ZoneNa
       soa.edit_done = increaseSOARecord(new_record, soa.edit_api_kind, soa.edit_kind, zonename);
     }
 
-    if (!checkNewRecords(resp, new_records, zonename, allowUnderscores)) {
+    Check::RRSetFlags flags{Check::RRSET_CHECK_TTL};
+    if (allowUnderscores) {
+      flags = static_cast<Check::RRSetFlags>(flags | Check::RRSET_ALLOW_UNDERSCORES);
+    }
+    if (!checkNewRecords(resp, new_records, zonename, flags)) {
       // Proper error response has been set up, no need to do anything further.
       return ABORT;
     }