From: Miod Vallat Date: Wed, 6 Aug 2025 10:01:14 +0000 (+0200) Subject: Underscores may appear in hostnames if RFC112-CONFORMANCE metadata is set to 0. X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=3fbe9dcb4e6a82e1335b63201cfb84e9d1fd73ac;p=thirdparty%2Fpdns.git Underscores may appear in hostnames if RFC112-CONFORMANCE metadata is set to 0. Signed-off-by: Miod Vallat --- diff --git a/docs/domainmetadata.rst b/docs/domainmetadata.rst index 090c2fbcc..9aa5ea5b6 100644 --- a/docs/domainmetadata.rst +++ b/docs/domainmetadata.rst @@ -214,6 +214,14 @@ Global defaults for these values can be set via :ref:`setting-default-publish-cd .. _metadata-signaling-zone: +RFC1123-CONFORMANCE +------------------- +.. versionadded:: 5.1.0 + +If set to 0, hostnames within the zone are allowed to deviate from :rfc:`1123` +by allowing underscore (``_``) characters to appear anywhere a letter or a +digit is allowed. + SIGNALING-ZONE -------------- .. versionadded:: 5.0.0 diff --git a/pdns/check-zone.cc b/pdns/check-zone.cc index 431da983e..9044cf546 100644 --- a/pdns/check-zone.cc +++ b/pdns/check-zone.cc @@ -53,7 +53,7 @@ bool validateViewName(std::string_view name, std::string& error) return true; } -void checkRRSet(const vector& oldrrs, vector& allrrs, const ZoneName& zone, vector>& errors) +void checkRRSet(const vector& oldrrs, vector& allrrs, const ZoneName& zone, bool allowUnderscores, vector>& errors) { // QTypes that MUST NOT have multiple records of the same type in a given RRset. static const std::set onlyOneEntryTypes = {QType::CNAME, QType::DNAME, QType::SOA}; @@ -109,7 +109,7 @@ void checkRRSet(const vector& oldrrs, vector& oldrrs, vector& allrrs, const ZoneName& zone, vector>& errors); +void checkRRSet(const vector& oldrrs, vector& allrrs, const ZoneName& zone, bool allowUnderscores, vector>& errors); } // namespace Check diff --git a/pdns/dnsname.cc b/pdns/dnsname.cc index 97c0c30c3..b66f75c86 100644 --- a/pdns/dnsname.cc +++ b/pdns/dnsname.cc @@ -638,9 +638,17 @@ bool DNSName::isWildcard() const /* * Returns true if the DNSName is a valid RFC 1123 hostname, this function uses * a regex on the string, so it is probably best not used when speed is essential. + * + * If allowUnderscore is set, underscore characters (`_') are allowed anywhere + * a letter or a digit would have been. In particular, leading underscores are + * allowed. */ -bool DNSName::isHostname() const +bool DNSName::isHostname(bool allowUnderscore) const { + if (allowUnderscore) { + static Regex hostNameRegexWithUnderscore = Regex("^(([A-Za-z0-9_]([A-Za-z0-9-_]*[A-Za-z0-9_])?)\\.)+$"); + return hostNameRegexWithUnderscore.match(this->toString()); + } static Regex hostNameRegex = Regex("^(([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)\\.)+$"); return hostNameRegex.match(this->toString()); } diff --git a/pdns/dnsname.hh b/pdns/dnsname.hh index 9756b5fae..e5fde7877 100644 --- a/pdns/dnsname.hh +++ b/pdns/dnsname.hh @@ -149,7 +149,7 @@ public: DNSName getCommonLabels(const DNSName& other) const; //!< Return the list of common labels from the top, for example 'c.d' for 'a.b.c.d' and 'x.y.c.d' DNSName labelReverse() const; bool isWildcard() const; - bool isHostname() const; + bool isHostname(bool allowUnderscore = false) const; unsigned int countLabels() const; size_t wirelength() const; //!< Number of total bytes in the name bool empty() const { return d_storage.empty(); } diff --git a/pdns/dnsrecords.cc b/pdns/dnsrecords.cc index cb8172e89..f55804686 100644 --- a/pdns/dnsrecords.cc +++ b/pdns/dnsrecords.cc @@ -1043,7 +1043,7 @@ ComboAddress getAddr(const DNSRecord& dr, uint16_t defport) /** * Check if the DNSNames that should be hostnames, are hostnames */ -void checkHostnameCorrectness(const DNSResourceRecord& rr) +void checkHostnameCorrectness(const DNSResourceRecord& rr, bool allowUnderscore) // NOLINT(readability-identifier-length) { if (rr.qtype.getCode() == QType::NS || rr.qtype.getCode() == QType::MX || rr.qtype.getCode() == QType::SRV) { DNSName toCheck; @@ -1064,7 +1064,7 @@ void checkHostnameCorrectness(const DNSResourceRecord& rr) } else if ((rr.qtype.getCode() == QType::MX || rr.qtype.getCode() == QType::SRV) && toCheck == g_rootdnsname) { // allow null MX/SRV - } else if(!toCheck.isHostname()) { + } else if(!toCheck.isHostname(allowUnderscore)) { throw std::runtime_error(boost::str(boost::format("non-hostname content %s") % toCheck.toString())); } } diff --git a/pdns/dnsrecords.hh b/pdns/dnsrecords.hh index 2677cd6b1..f1bd7a961 100644 --- a/pdns/dnsrecords.hh +++ b/pdns/dnsrecords.hh @@ -1336,4 +1336,4 @@ class MOADNSParser; bool getEDNSOpts(const MOADNSParser& mdp, EDNSOpts* eo); void reportAllTypes(); ComboAddress getAddr(const DNSRecord& dr, uint16_t defport=0); -void checkHostnameCorrectness(const DNSResourceRecord& rr); +void checkHostnameCorrectness(const DNSResourceRecord& rr, bool allowUnderscore = false); diff --git a/pdns/pdnsutil.cc b/pdns/pdnsutil.cc index 4721fe441..76b89ef27 100644 --- a/pdns/pdnsutil.cc +++ b/pdns/pdnsutil.cc @@ -870,6 +870,14 @@ static std::string terminalSafe(const std::string& input) return output; } +static bool areUnderscoresAllowed(const ZoneName& zonename, DomainInfo& info) +{ + string underscores{}; + info.backend->getDomainMetadataOne(zonename, "RFC1123-CONFORMANCE", underscores); + // Metadata absent implies strict conformance + return underscores == "0"; +} + static int checkZone(DNSSECKeeper &dk, UeberBackend &B, const ZoneName& zone, const vector* suppliedrecords=nullptr) // NOLINT(readability-function-cognitive-complexity,readability-identifier-length) { int numerrors=0; @@ -1029,6 +1037,8 @@ static int checkZone(DNSSECKeeper &dk, UeberBackend &B, const ZoneName& zone, co else records=*suppliedrecords; + bool allowUnderscores = areUnderscoresAllowed(zone, di); + for(auto &rr : records) { // we modify this if(rr.qtype.getCode() == QType::TLSA) tlsas.insert(rr.qname); @@ -1240,7 +1250,7 @@ static int checkZone(DNSSECKeeper &dk, UeberBackend &B, const ZoneName& zone, co // Check if the DNSNames that should be hostnames, are hostnames try { - checkHostnameCorrectness(rr); + checkHostnameCorrectness(rr, allowUnderscores); } catch (const std::exception& e) { cout << "[Warning] " << rr.qtype.toString() << " record in zone '" << zone << ": " << e.what() << endl; numwarnings++; @@ -2683,6 +2693,8 @@ static int addOrReplaceRecord(bool isAdd, const vector& cmds) } } + bool allowUnderscores = areUnderscoresAllowed(zone, di); + di.backend->startTransaction(zone, UnknownDomainID); DNSResourceRecord oldrr; @@ -2704,7 +2716,7 @@ static int addOrReplaceRecord(bool isAdd, const vector& cmds) } std::vector> errors; - Check::checkRRSet(oldrrs, newrrs, zone, errors); + Check::checkRRSet(oldrrs, newrrs, zone, allowUnderscores, errors); oldrrs.clear(); // no longer needed if (!errors.empty()) { for (const auto& error : errors) { diff --git a/pdns/ws-auth.cc b/pdns/ws-auth.cc index 07736a134..73332f2b5 100644 --- a/pdns/ws-auth.cc +++ b/pdns/ws-auth.cc @@ -1034,6 +1034,7 @@ static bool isValidMetadataKind(const string& kind, bool readonly) {"PRESIGNED", true}, {"PUBLISH-CDNSKEY", false}, {"PUBLISH-CDS", false}, + {"RFC1123-CONFORMANCE", false}, {"SIGNALING-ZONE", false}, {"SLAVE-RENOTIFY", false}, {"SOA-EDIT", true}, @@ -1674,13 +1675,21 @@ static void gatherRecordsFromZone(const std::string& zonestring, vector& records, const ZoneName& zone) +static bool checkNewRecords(HttpResponse* resp, vector& records, const ZoneName& zone, bool allowUnderscores) { std::vector> errors; - Check::checkRRSet({}, records, zone, errors); + Check::checkRRSet({}, records, zone, allowUnderscores, errors); if (errors.empty()) { return true; } @@ -2054,7 +2063,7 @@ static void apiServerZonesPOST(HttpRequest* req, HttpResponse* resp) } } - if (!checkNewRecords(resp, new_records, zonename)) { + if (!checkNewRecords(resp, new_records, zonename, false)) { // no RFC1123-CONFORMANCE metadata on new zones return; } @@ -2230,7 +2239,8 @@ static void apiServerZoneDetailPUT(HttpRequest* req, HttpResponse* resp) throw ApiException("Modifying RRsets in Consumer zones is unsupported"); } - if (!checkNewRecords(resp, new_records, zoneData.zoneName)) { + bool allowUnderscores = areUnderscoresAllowed(zoneData.zoneName, *zoneData.domainInfo.backend); + if (!checkNewRecords(resp, new_records, zoneData.zoneName, allowUnderscores)) { return; } @@ -2460,6 +2470,7 @@ static void patchZone(UeberBackend& backend, const ZoneName& zonename, DomainInf domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind); domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit_kind); bool soa_edit_done = false; + bool allowUnderscores = areUnderscoresAllowed(zonename, *domainInfo.backend); vector new_records; vector new_comments; @@ -2518,7 +2529,7 @@ static void patchZone(UeberBackend& backend, const ZoneName& zonename, DomainInf soa_edit_done = increaseSOARecord(resourceRecord, soa_edit_api_kind, soa_edit_kind, zonename); } } - if (!checkNewRecords(resp, new_records, zonename)) { + if (!checkNewRecords(resp, new_records, zonename, allowUnderscores)) { return; } } diff --git a/regression-tests.api/test_Zones.py b/regression-tests.api/test_Zones.py index bf28bb147..bfcc978cb 100644 --- a/regression-tests.api/test_Zones.py +++ b/regression-tests.api/test_Zones.py @@ -2686,6 +2686,37 @@ $NAME$ 1D IN SOA ns1.example.org. hostmaster.example.org. ( self.assertEqual(r.status_code, 422) self.assertIn('Data field in DNS should end on a quote', r.json()['error']) + def test_underscore_names(self): + name = unique_zone_name() + self.create_zone(name=name, kind='Native') + + payload_metadata = {"type": "Metadata", "kind": "RFC1123-CONFORMANCE", "metadata": ["0"]} + r = self.session.post(self.url("/api/v1/servers/localhost/zones/" + name + "/metadata"), + data=json.dumps(payload_metadata)) + rdata = r.json() + self.assertEqual(r.status_code, 201) + self.assertEqual(rdata["metadata"], payload_metadata["metadata"]) + + rrset = { + 'changetype': 'replace', + 'name': "_underscores_r_us_."+name, + 'type': "A", + 'ttl': 3600, + 'records': [{ + "content": "42.42.42.42", + "disabled": False, + }], + } + payload = {'rrsets': [rrset]} + r = self.session.patch( + self.url("/api/v1/servers/localhost/zones/" + name), + data=json.dumps(payload), + headers={'content-type': 'application/json'}) + self.assert_success(r) + data = self.get_zone(name) + # check our record has appeared + self.assertEqual(get_rrset(data, rrset['name'], 'A')['records'], rrset['records']) + @unittest.skipIf(not is_auth(), "Not applicable") class AuthRootZone(ZonesApiTestCase, AuthZonesHelperMixin):