From: Ruben d'Arco Date: Mon, 3 Dec 2012 05:55:02 +0000 (+0100) Subject: Add rfc2136handler to handle rfc2136 operation X-Git-Tag: rec-3.6.0-rc1~556^2~3^2~96 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f1b59a55bc7085fc34238cc116fb98e3a2cd0989;p=thirdparty%2Fpdns.git Add rfc2136handler to handle rfc2136 operation --- diff --git a/pdns/rfc2136handler.cc b/pdns/rfc2136handler.cc new file mode 100644 index 0000000000..b7e39f3e5d --- /dev/null +++ b/pdns/rfc2136handler.cc @@ -0,0 +1,691 @@ +#include "packethandler.hh" +#include "qtype.hh" +#include "dnspacket.hh" +#include "packetcache.hh" +#include "dnsseckeeper.hh" +#include "base64.hh" +#include "base32.hh" +#include "misc.hh" +#include "arguments.hh" + +extern PacketCache PC; + +// Implement section 3.2.1 and 3.2.2 of RFC2136 +int PacketHandler::checkUpdatePrerequisites(const DNSRecord *rr, DomainInfo *di) { + if (rr->d_ttl != 0) + return RCode::FormErr; + + // 3.2.1 and 3.2.2 check content length. + if ( (rr->d_class == QClass::NONE || rr->d_class == QClass::ANY) && rr->d_clen != 0) + return RCode::FormErr; + + string rLabel = stripDot(rr->d_label); + + bool foundRecord=false; + DNSResourceRecord rec; + di->backend->lookup(QType(QType::ANY), rLabel); + while(di->backend->get(rec)) { + if (!rec.qtype.getCode()) + continue; + if ((rr->d_type != QType::ANY && rec.qtype == rr->d_type) || rr->d_type == QType::ANY) + foundRecord=true; + } + + // Section 3.2.1 + if (rr->d_class == QClass::ANY && !foundRecord) { + if (rr->d_type == QType::ANY) + return RCode::NXDomain; + if (rr->d_type != QType::ANY) + return RCode::NXRRSet; + } + + // Section 3.2.2 + if (rr->d_class == QClass::NONE && foundRecord) { + if (rr->d_type == QType::ANY) + return RCode::YXDomain; + if (rr->d_type != QType::ANY) + return RCode::YXRRSet; + } + + return RCode::NoError; +} + + +// Method implements section 3.4.1 of RFC2136 +int PacketHandler::checkUpdatePrescan(const DNSRecord *rr) { + // The RFC stats that d_class != ZCLASS, but we only support the IN class. + if (rr->d_class != QClass::IN && rr->d_class != QClass::NONE && rr->d_class != QClass::ANY) + return RCode::FormErr; + + QType qtype = QType(rr->d_type); + + if (! qtype.isSupportedType()) + return RCode::FormErr; + + if ((rr->d_class == QClass::NONE || rr->d_class == QClass::ANY) && rr->d_ttl != 0) + return RCode::FormErr; + + if (rr->d_class == QClass::ANY && rr->d_clen != 0) + return RCode::FormErr; + + if (qtype.isMetadataType()) + return RCode::FormErr; + + if (rr->d_class != QClass::ANY && qtype.getCode() == QType::ANY) + return RCode::FormErr; + + return RCode::NoError; +} + +// Implements section 3.4.2 of RFC2136 +uint16_t PacketHandler::performUpdate(const string &msgPrefix, const DNSRecord *rr, DomainInfo *di, bool narrow, bool haveNSEC3, const NSEC3PARAMRecordContent *ns3pr, bool *updatedSerial) { + /*DNSResourceRecord rec; + uint16_t updatedRecords = 0, deletedRecords = 0, insertedRecords = 0; + + string rLabel = stripDot(rr->d_label); + + if (rr->d_class == QClass::IN) { // 3.4.2.2, Add/update records. + DLOG(L< delnonterm; + vector > recordsToUpdate; + di->backend->lookup(QType(QType::ANY), rLabel); + while (di->backend->get(rec)) { + if (!rec.qtype.getCode()) + delnonterm.insert(rec.qname); // we're inserting a record which is a ENT, so we must delete that ENT + if (rr->d_type == QType::SOA && rec.qtype == QType::SOA) { + foundRecord = true; + DNSResourceRecord newRec = rec; + newRec.setContent(rr->d_content->getZoneRepresentation()); + newRec.ttl = rr->d_ttl; + SOAData sdOld, sdUpdate; + fillSOAData(rec.content, sdOld); + fillSOAData(newRec.content, sdUpdate); + if (rfc1982LessThan(sdOld.serial, sdUpdate.serial)) { + recordsToUpdate.push_back(make_pair(rec, newRec)); + *updatedSerial = true; + } + else + L<d_type == QType::CNAME && rec.qtype == QType::CNAME) { // If the update record is a cname, we update that cname. + foundRecord = true; + DNSResourceRecord newRec = rec; + newRec.ttl = rr->d_ttl; + newRec.setContent(rr->d_content->getZoneRepresentation()); + recordsToUpdate.push_back(make_pair(rec, newRec)); + } else if (rec.qtype == rr->d_type) { + string content = rr->d_content->getZoneRepresentation(); + if (rec.getZoneRepresentation() == content) { + foundRecord=true; + DNSResourceRecord newRec = rec; + newRec.ttl = rr->d_ttl; // If content matches, we can only update the TTL. + recordsToUpdate.push_back(make_pair(rec, newRec)); + } + } + } + // Update the records + for(vector >::const_iterator i=recordsToUpdate.begin(); i!=recordsToUpdate.end(); ++i){ + di->backend->updateRecord(i->first, i->second); + L<first.qname<<"|"<first.qtype.getName()<id; + L<backend->feedRecord(newRec); + insertedRecords++; + } + + // The next section will fix order and Auth fields and insert ENT's + if (insertedRecords > 0) { + string shorter(rLabel); + bool auth=true; + + set insnonterm; + if (shorter != di->zone && rr->d_type != QType::DS) { + do { + if (shorter == di->zone) + break; + + bool foundShorter = false; + di->backend->lookup(QType(QType::ANY), shorter); + while (di->backend->get(rec)) { + if (rec.qname != rLabel) + foundShorter = true; + if (rec.qtype == QType::NS) + auth=false; + } + if (!foundShorter && shorter != rLabel && shorter != di->zone) + insnonterm.insert(shorter); + + } while(chopOff(shorter)); + } + + + if(haveNSEC3) + { + string hashed; + if(!narrow) + hashed=toLower(toBase32Hex(hashQNameWithSalt(ns3pr->d_iterations, ns3pr->d_salt, rLabel))); + + di->backend->updateDNSSECOrderAndAuthAbsolute(di->id, rLabel, hashed, auth); + if(!auth || rr->d_type == QType::DS) + { + di->backend->nullifyDNSSECOrderNameAndAuth(di->id, rLabel, "NS"); + di->backend->nullifyDNSSECOrderNameAndAuth(di->id, rLabel, "A"); + di->backend->nullifyDNSSECOrderNameAndAuth(di->id, rLabel, "AAAA"); + } + } + else // NSEC + { + di->backend->updateDNSSECOrderAndAuth(di->id, di->zone, rLabel, auth); + if(!auth || rr->d_type == QType::DS) + { + di->backend->nullifyDNSSECOrderNameAndAuth(di->id, rLabel, "A"); + di->backend->nullifyDNSSECOrderNameAndAuth(di->id, rLabel, "AAAA"); + } + } + // If we insert an NS, all the records below it become non auth - so, we're inserting a delegate. + // Auth can only be false when the rLabel is not the zone + if (auth == false && rr->d_type == QType::NS) { + DLOG(L< qnames; + di->backend->listSubZone(rLabel, di->id); + while(di->backend->get(rec)) { + if (rec.qtype.getCode() && rec.qtype.getCode() != QType::DS) // Skip ENT and DS records. + qnames.push_back(rec.qname); + } + for(vector::const_iterator qname=qnames.begin(); qname != qnames.end(); ++qname) { + if(haveNSEC3) { + string hashed; + if(!narrow) + hashed=toLower(toBase32Hex(hashQNameWithSalt(ns3pr->d_iterations, ns3pr->d_salt, *qname))); + + di->backend->updateDNSSECOrderAndAuthAbsolute(di->id, *qname, hashed, auth); + di->backend->nullifyDNSSECOrderNameAndAuth(di->id, *qname, "NS"); + } + else // NSEC + di->backend->updateDNSSECOrderAndAuth(di->id, di->zone, *qname, auth); + + di->backend->nullifyDNSSECOrderNameAndAuth(di->id, *qname, "AAAA"); + di->backend->nullifyDNSSECOrderNameAndAuth(di->id, *qname, "A"); + } + } + + //Insert and delete ENT's + if (insnonterm.size() > 0 || delnonterm.size() > 0) { + DLOG(L<backend->updateEmptyNonTerminals(di->id, di->zone, insnonterm, delnonterm, false); + for (set::const_iterator i=insnonterm.begin(); i!=insnonterm.end(); i++) { + string hashed; + if(haveNSEC3) + { + string hashed; + if(!narrow) + hashed=toLower(toBase32Hex(hashQNameWithSalt(ns3pr->d_iterations, ns3pr->d_salt, *i))); + di->backend->updateDNSSECOrderAndAuthAbsolute(di->id, *i, hashed, false); + } + } + } + } + } // rr->d_class == QClass::IN + + + + // The following section deals with the removal of records. When the class is ANY, all records of + // that name (and/or type) are deleted. When the type is NONE, the RDATA must match as well. + // There are special cases for SOA and NS records to ensure the zone will remain operational. + //Section 3.4.2.3: Delete RRs based on name and (if provided) type, but never delete NS or SOA at the zone apex. + vector recordsToDelete; + if (rr->d_class == QClass::ANY) { + DLOG(L<zone && (rr->d_type == QType::SOA || rr->d_type == QType::NS) ) ) { + di->backend->lookup(QType(QType::ANY), rLabel); + while (di->backend->get(rec)) { + if (rec.qtype.getCode() && (rr->d_type == QType::ANY || rr->d_type == rec.qtype.getCode())) + recordsToDelete.push_back(rec); + } + } + } + + // Section 3.4.2.4, Delete a specific record that matches name, type and rdata + // There are special conditions for SOA (never delete them). There is also a special condition for NS records, + // but that's filtered out by not calling this method in those cases - There's a check to make sure we don't delete the + // last NS. + if (rr->d_class == QClass::NONE && rr->d_type != QType::SOA) { // never remove SOA. + DLOG(L<backend->lookup(QType(QType::ANY), rLabel); + while(di->backend->get(rec)) { + if (rec.qtype.getCode() && rec.qtype == rr->d_type && rec.getZoneRepresentation() == rr->d_content->getZoneRepresentation()) + recordsToDelete.push_back(rec); + } + } + + if (recordsToDelete.size()) { + // Perform removes on the backend and fix auth/ordername + for(vector::const_iterator recToDelete=recordsToDelete.begin(); recToDelete!=recordsToDelete.end(); ++recToDelete){ + L<qname<<"|"<qtype.getName()<backend->removeRecord(*recToDelete); + deletedRecords++; + + if (recToDelete->qtype.getCode() == QType::NS && recToDelete->qname != di->zone) { + vector changeAuth; + di->backend->listSubZone(recToDelete->qname, di->id); + while (di->backend->get(rec)) { + if (rec.qtype.getCode()) // skip ENT records + changeAuth.push_back(rec.qname); + } + for (vector::const_iterator changeRec=changeAuth.begin(); changeRec!=changeAuth.end(); ++changeRec) { + if(haveNSEC3) { + string hashed; + if(!narrow) + hashed=toLower(toBase32Hex(hashQNameWithSalt(ns3pr->d_iterations, ns3pr->d_salt, *changeRec))); + + di->backend->updateDNSSECOrderAndAuthAbsolute(di->id, *changeRec, hashed, true); + } + else // NSEC + di->backend->updateDNSSECOrderAndAuth(di->id, di->zone, *changeRec, true); + } + } + } + + // Fix ENT records. + // We must check if we have a record below the current level and if we removed the 'last' record + // on that level. If so, we must insert an ENT record. + // We take extra care here to not 'include' the record that we just deleted. Some backends will still return it. + set insnonterm, delnonterm; + bool foundDeeper = false, foundOther = false; + di->backend->listSubZone(rLabel, di->id); + while (di->backend->get(rec)) { + if (rec.qname == rLabel && !count(recordsToDelete.begin(), recordsToDelete.end(), rec)) + foundOther = true; + if (rec.qname != rLabel) + foundDeeper = true; + } + + if (foundDeeper && !foundOther) { + insnonterm.insert(rLabel); + } else if (!foundOther) { + // If we didn't have to insert an ENT, we might have deleted a record at very deep level + // and we must then clean up the ENT's above the deleted record. + string shorter(rLabel); + do { + bool foundRealRR=false; + if (shorter == di->zone) + break; + // The reason for a listSubZone here is because might go up the tree and find the root ENT of another branch + // consider these non ENT-records: + // a.b.c.d.e.test.com + // a.b.d.e.test.com + // if we delete a.b.c.d.e.test.com, we go up to d.e.test.com and then find a.b.d.e.test.com + // At that point we can stop deleting ENT's because the tree is in tact again. + di->backend->listSubZone(shorter, di->id); + while (di->backend->get(rec)) { + if (rec.qtype.getCode()) + foundRealRR=true; + } + if (!foundRealRR) + delnonterm.insert(shorter); + else + break; // we found a real record - tree is ok again. + }while(chopOff(shorter)); + } + + if (insnonterm.size() > 0 || delnonterm.size() > 0) { + DLOG(L<backend->updateEmptyNonTerminals(di->id, di->zone, insnonterm, delnonterm, false); + for (set::const_iterator i=insnonterm.begin(); i!=insnonterm.end(); i++) { + string hashed; + if(haveNSEC3) + { + string hashed; + if(!narrow) + hashed=toLower(toBase32Hex(hashQNameWithSalt(ns3pr->d_iterations, ns3pr->d_salt, *i))); + di->backend->updateDNSSECOrderAndAuthAbsolute(di->id, *i, hashed, true); + } + } + } + } + + L<getRemote() + " for " + p->qdomain + ": "; + L< allowedRanges; + B.getDomainMetadata(p->qdomain, "ALLOW-2136-FROM", allowedRanges); + if (! ::arg()["allow-2136-from"].empty()) + stringtok(allowedRanges, ::arg()["allow-2136-from"], ", \t" ); + + NetmaskGroup ng; + for(vector::const_iterator i=allowedRanges.begin(); i != allowedRanges.end(); i++) + ng.addMask(*i); + + if ( ! ng.match(&p->d_remote)) { + L< tsigKeys; + B.getDomainMetadata(p->qdomain, "TSIG-ALLOW-2136", tsigKeys); + if (tsigKeys.size() > 0) { + bool validKey = false; + + TSIGRecordContent trc; + string inputkey, message; + if (! p->getTSIGDetails(&trc, &inputkey, &message)) { + L<::const_iterator key=tsigKeys.begin(); key != tsigKeys.end(); key++) { + if (inputkey == *key) // because checkForCorrectTSIG has already been performed earlier on, if the names of the ky match with the domain given. THis is valid. + validKey=true; + } + + if (!validKey) { + L<d_havetsig) + L<getString()); + if (mdp.d_header.qdcount != 1) { + L<qtype.getCode() != QType::SOA) { // RFC2136 2.3 - ZTYPE must be SOA + L<qclass != QClass::IN) { + L<qdomain, di) || !di.backend) { + L<qdomain<<"' (or backend does not support RFC2136 operation)"<first; + // Skip this check for other field types (like the TSIG - which is in the additional section) + // For a TSIG, the label is the dnskey. + if (! (rr->d_place == DNSRecord::Answer || rr->d_place == DNSRecord::Nameserver)) + continue; + + string label = stripDot(rr->d_label); + + if (!endsOn(label, di.zone)) { + L<startTransaction(p->qdomain, -1)) { // Not giving the domain_id means that we do not delete the records. + L<qdomain<<" does not support transaction. Can't do Update packet."<first; + if (rr->d_place == DNSRecord::Answer) { + int res = checkUpdatePrerequisites(rr, &di); + if (res>0) { + L<abortTransaction(); + return res; + } + } + } + + // 3.2.3 - Prerequisite check - this is outside of updatePrequisitesCheck because we check an RRSet and not the RR. + typedef pair rrSetKey_t; + typedef vector rrVector_t; + typedef std::map RRsetMap_t; + RRsetMap_t preReqRRsets; + for(MOADNSParser::answers_t::const_iterator i=mdp.d_answers.begin(); i != mdp.d_answers.end(); ++i) { + const DNSRecord *rr = &i->first; + if (rr->d_place == DNSRecord::Answer) { + // Last line of 3.2.3 + if (rr->d_class != QClass::IN && rr->d_class != QClass::NONE && rr->d_class != QClass::ANY) + return RCode::FormErr; + + if (rr->d_class == QClass::IN) { + rrSetKey_t key = make_pair(stripDot(rr->d_label), rr->d_type); + rrVector_t *vec = &preReqRRsets[key]; + vec->push_back(DNSResourceRecord(*rr)); + } + } + } + + if (preReqRRsets.size() > 0) { + RRsetMap_t zoneRRsets; + for (RRsetMap_t::iterator preRRSet = preReqRRsets.begin(); preRRSet != preReqRRsets.end(); ++preRRSet) { + rrSetKey_t rrSet=preRRSet->first; + rrVector_t *vec = &preRRSet->second; + + DNSResourceRecord rec; + di.backend->lookup(QType(QType::ANY), rrSet.first); + uint16_t foundRR=0, matchRR=0; + while (di.backend->get(rec)) { + if (rec.qtype == rrSet.second) { + foundRR++; + for(rrVector_t::iterator rrItem=vec->begin(); rrItem != vec->end(); ++rrItem) { + rrItem->ttl = rec.ttl; // The compare one line below also compares TTL, so we make them equal because TTL is not user within prerequisite checks. + if (*rrItem == rec) + matchRR++; + } + } + } + if (matchRR != foundRR || foundRR != vec->size()) { + L<abortTransaction(); + return RCode::NXRRSet; + } + } + } + + + + // 3.4 - Prescan & Add/Update/Delete records + uint16_t changedRecords = 0; + try { + + // 3.4.1 - Prescan section + for(MOADNSParser::answers_t::const_iterator i=mdp.d_answers.begin(); i != mdp.d_answers.end(); ++i) { + const DNSRecord *rr = &i->first; + if (rr->d_place == DNSRecord::Nameserver) { + int res = checkUpdatePrescan(rr); + if (res>0) { + L<abortTransaction(); + return res; + } + } + } + + bool updatedSerial=false; + NSEC3PARAMRecordContent ns3pr; + bool narrow; + bool haveNSEC3 = d_dk.getNSEC3PARAM(di.zone, &ns3pr, &narrow); + + // We get all the before/after fields before doing anything to the db. + // We can't do this inside performUpdate() because when we remove a delegate, the before/after result is different to what it should be + // to purge the cache correctly - One update/delete might cause a before/after to be created which is before/after the original before/after. + vector< pair > beforeAfterSet; + if (!haveNSEC3) { + for(MOADNSParser::answers_t::const_iterator i=mdp.d_answers.begin(); i != mdp.d_answers.end(); ++i) { + const DNSRecord *rr = &i->first; + if (rr->d_place == DNSRecord::Nameserver) { + string before, after; +// di.backend->getBeforeAndAfterNames(di.id, di.zone, stripDot(rr->d_label), before, after, (rr->d_class != QClass::IN)); + beforeAfterSet.push_back(make_pair(before, after)); + } + } + } + + // 3.4.2 - Perform the updates. + // There's a special condition where deleting the last NS record at zone apex is never deleted (3.4.2.4) + // This means we must do it outside the normal performUpdate() because that focusses only on a seperate RR. + vector nsRRtoDelete; + for(MOADNSParser::answers_t::const_iterator i=mdp.d_answers.begin(); i != mdp.d_answers.end(); ++i) { + const DNSRecord *rr = &i->first; + if (rr->d_place == DNSRecord::Nameserver) { + if (rr->d_class == QClass::NONE && rr->d_type == QType::NS && stripDot(rr->d_label) == di.zone) + nsRRtoDelete.push_back(rr); + else + changedRecords += performUpdate(msgPrefix, rr, &di, narrow, haveNSEC3, &ns3pr, &updatedSerial); + } + } + if (nsRRtoDelete.size()) { + vector nsRRInZone; + DNSResourceRecord rec; + di.backend->lookup(QType(QType::NS), di.zone); + while (di.backend->get(rec)) { + nsRRInZone.push_back(rec); + } + if (nsRRInZone.size() > nsRRtoDelete.size()) { // only delete if the NS's we delete are less then what we have in the zone (3.4.2.4) + for (vector::iterator inZone=nsRRInZone.begin(); inZone != nsRRInZone.end(); inZone++) { + for (vector::iterator rr=nsRRtoDelete.begin(); rr != nsRRtoDelete.end(); rr++) { + if (inZone->getZoneRepresentation() == (*rr)->d_content->getZoneRepresentation()) + changedRecords += performUpdate(msgPrefix, *rr, &di, narrow, haveNSEC3, &ns3pr, &updatedSerial); + } + } + } + } + + + // Purge the records! + if (changedRecords > 0) { + if (haveNSEC3) { + string zone(di.zone); + zone.append("$"); + PC.purge(zone); // For NSEC3, nuke the complete zone. + } else { + //for(vector< pair >::const_iterator i=beforeAfterSet.begin(); i != beforeAfterSet.end(); i++) + //PC.purgeRange(i->first, i->second, di.zone); + } + } + + // Section 3.6 - Update the SOA serial - outside of performUpdate because we do a SOA update for the complete update message + if (changedRecords > 0 && !updatedSerial) + increaseSerial(msgPrefix, di); + } + catch (AhuException &e) { + L<abortTransaction(); + return RCode::ServFail; + } + catch (...) { + L<abortTransaction(); + return RCode::ServFail; + } + + if (!di.backend->commitTransaction()) { + L<lookup(QType(QType::SOA), di.zone); + bool foundSOA=false; + while (di.backend->get(rec)) { + newRec = rec; + foundSOA=true; + } + if (!foundSOA) { + throw AhuException("SOA-Serial update failed because there was no SOA. Wowie."); + } + SOAData soa2Update; + fillSOAData(rec.content, soa2Update); + + vector soaEdit2136Setting; + B.getDomainMetadata(di.zone, "SOA-EDIT-2136", soaEdit2136Setting); + string soaEdit2136 = "DEFAULT"; + string soaEdit; + if (!soaEdit2136Setting.empty()) { + soaEdit2136 = soaEdit2136Setting[0]; + if (pdns_iequals(soaEdit2136, "SOA-EDIT") || pdns_iequals(soaEdit2136,"SOA-EDIT-INCREASE") ){ + vector soaEditSetting; + B.getDomainMetadata(di.zone, "SOA-EDIT", soaEditSetting); + if (soaEditSetting.empty()) { + L<updateRecord(rec, newRec); + PC.purge(newRec.qname); +} \ No newline at end of file