.. _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
return true;
}
-void checkRRSet(const vector<DNSResourceRecord>& oldrrs, vector<DNSResourceRecord>& allrrs, const ZoneName& zone, vector<pair<DNSResourceRecord, string>>& errors)
+void checkRRSet(const vector<DNSResourceRecord>& oldrrs, vector<DNSResourceRecord>& allrrs, const ZoneName& zone, bool allowUnderscores, vector<pair<DNSResourceRecord, string>>& errors)
{
// QTypes that MUST NOT have multiple records of the same type in a given RRset.
static const std::set<uint16_t> onlyOneEntryTypes = {QType::CNAME, QType::DNAME, QType::SOA};
// Check if the DNSNames that should be hostnames, are hostnames
try {
- checkHostnameCorrectness(rec);
+ checkHostnameCorrectness(rec, allowUnderscores);
}
catch (const std::exception& e) {
errors.emplace_back(std::make_pair(rec, e.what()));
// *) no exact duplicates
// *) no duplicates for QTypes that can only be present once per RRset
// *) hostnames are hostnames
-void checkRRSet(const vector<DNSResourceRecord>& oldrrs, vector<DNSResourceRecord>& allrrs, const ZoneName& zone, vector<pair<DNSResourceRecord, string>>& errors);
+void checkRRSet(const vector<DNSResourceRecord>& oldrrs, vector<DNSResourceRecord>& allrrs, const ZoneName& zone, bool allowUnderscores, vector<pair<DNSResourceRecord, string>>& errors);
} // namespace Check
/*
* 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());
}
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(); }
/**
* 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;
}
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()));
}
}
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);
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<DNSResourceRecord>* suppliedrecords=nullptr) // NOLINT(readability-function-cognitive-complexity,readability-identifier-length)
{
int numerrors=0;
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);
// 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++;
}
}
+ bool allowUnderscores = areUnderscoresAllowed(zone, di);
+
di.backend->startTransaction(zone, UnknownDomainID);
DNSResourceRecord oldrr;
}
std::vector<std::pair<DNSResourceRecord, string>> 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) {
{"PRESIGNED", true},
{"PUBLISH-CDNSKEY", false},
{"PUBLISH-CDS", false},
+ {"RFC1123-CONFORMANCE", false},
{"SIGNALING-ZONE", false},
{"SLAVE-RENOTIFY", false},
{"SOA-EDIT", true},
}
}
+static bool areUnderscoresAllowed(const ZoneName& zonename, DNSBackend& backend)
+{
+ string underscores{};
+ backend.getDomainMetadataOne(zonename, "RFC1123-CONFORMANCE", underscores);
+ // Metadata absent implies strict conformance
+ return underscores == "0";
+}
+
// 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)
+static bool checkNewRecords(HttpResponse* resp, vector<DNSResourceRecord>& records, const ZoneName& zone, bool allowUnderscores)
{
std::vector<std::pair<DNSResourceRecord, string>> errors;
- Check::checkRRSet({}, records, zone, errors);
+ Check::checkRRSet({}, records, zone, allowUnderscores, errors);
if (errors.empty()) {
return true;
}
}
}
- if (!checkNewRecords(resp, new_records, zonename)) {
+ if (!checkNewRecords(resp, new_records, zonename, false)) { // no RFC1123-CONFORMANCE metadata on new zones
return;
}
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;
}
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<DNSResourceRecord> new_records;
vector<Comment> new_comments;
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;
}
}
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):