123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799 |
- # Copyright (C) 2012 Internet Systems Consortium.
- #
- # Permission to use, copy, modify, and distribute this software for any
- # purpose with or without fee is hereby granted, provided that the above
- # copyright notice and this permission notice appear in all copies.
- #
- # THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
- # DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
- # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
- # INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
- # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
- # FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
- # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
- # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- from isc.dns import *
- import isc.ddns.zone_config
- from isc.log import *
- from isc.ddns.logger import logger, ClientFormatter, ZoneFormatter,\
- RRsetFormatter
- from isc.log_messages.libddns_messages import *
- from isc.datasrc import ZoneFinder
- import isc.xfrin.diff
- import copy
- # Result codes for UpdateSession.handle()
- UPDATE_SUCCESS = 0
- UPDATE_ERROR = 1
- UPDATE_DROP = 2
- # Convenient aliases of update-specific section names
- SECTION_ZONE = Message.SECTION_QUESTION
- SECTION_PREREQUISITE = Message.SECTION_ANSWER
- SECTION_UPDATE = Message.SECTION_AUTHORITY
- # Shortcut
- DBGLVL_TRACE_BASIC = logger.DBGLVL_TRACE_BASIC
- class UpdateError(Exception):
- '''Exception for general error in update request handling.
- This exception is intended to be used internally within this module.
- When UpdateSession.handle() encounters an error in handling an update
- request it can raise this exception to terminate the handling.
- This class is constructed with some information that may be useful for
- subsequent possible logging:
- - msg (string) A string explaining the error.
- - zname (isc.dns.Name) The zone name. Can be None when not identified.
- - zclass (isc.dns.RRClass) The zone class. Like zname, can be None.
- - rcode (isc.dns.RCode) The RCODE to be set in the response message.
- - nolog (bool) If True, it indicates there's no more need for logging.
- '''
- def __init__(self, msg, zname, zclass, rcode, nolog=False):
- Exception.__init__(self, msg)
- self.zname = zname
- self.zclass = zclass
- self.rcode = rcode
- self.nolog = nolog
- def foreach_rr_in_rrset(rrset, method, *kwargs):
- '''Helper function. For DDNS, in a number of cases, we need to
- treat the various RRs in a single RRset separately.
- Our libdns++ has no concept of RRs, so in that case,
- what we do is create a temporary 1-RR RRset for each Rdata
- in the RRset object.
- This method then calls the given method with the given args
- for each of the temporary rrsets (the rrset in *wargs is
- replaced by the temporary one)
- Note: if this method is useful in more places, we may want
- to move it out of ddns.
- Example:
- Say you have a method that prints a prexif string and an
- rrset, def my_print(prefix, rrset)
- Given an rrset my_rrset, you'd print the entire rrset
- with my_print("foo", rrset)
- And with this helper function, to print each rr invidually,
- you'd call
- foreach_rr_in_rrsetet(rrset, my_print, "foo", rrset)
- Note the rrset is needed twice, the first to identify it,
- the second as the 'real' argument to my_print (which is replaced
- by this function.
- '''
- result = None
- for rdata in rrset.get_rdata():
- tmp_rrset = isc.dns.RRset(rrset.get_name(),
- rrset.get_class(),
- rrset.get_type(),
- rrset.get_ttl())
- tmp_rrset.add_rdata(rdata)
- # Replace the rrset in the original arguments by our rrset
- args = [arg if arg != rrset else tmp_rrset for arg in kwargs]
- result = method(*args)
- return result
- def convert_rrset_class(rrset, rrclass):
- '''Returns a (new) rrset with the data from the given rrset,
- but of the given class. Useful to convert from NONE and ANY to
- a real class.
- Note that the caller should be careful what to convert;
- and DNS error that could happen during wire-format reading
- could technically occur here, and is not caught by this helper.
- '''
- new_rrset = isc.dns.RRset(rrset.get_name(), rrclass,
- rrset.get_type(), rrset.get_ttl())
- for rdata in rrset.get_rdata():
- # Rdata class is nof modifiable, and must match rrset's
- # class, so we need to to some ugly conversion here.
- # And we cannot use to_text() (since the class may be unknown)
- wire = rdata.to_wire(bytes())
- new_rrset.add_rdata(isc.dns.Rdata(rrset.get_type(), rrclass, wire))
- return new_rrset
- class DDNS_SOA:
- '''Class to handle the SOA in the DNS update '''
- def __get_serial_internal(self, origin_soa):
- '''Get serial number from soa'''
- return Serial(int(origin_soa.get_rdata()[0].to_text().split()[2]))
- def __write_soa_internal(self, origin_soa, soa_num):
- '''Write back serial number to soa'''
- new_soa = RRset(origin_soa.get_name(), origin_soa.get_class(),
- RRType.SOA(), origin_soa.get_ttl())
- soa_rdata_parts = origin_soa.get_rdata()[0].to_text().split()
- soa_rdata_parts[2] = str(soa_num.get_value())
- new_soa.add_rdata(Rdata(origin_soa.get_type(), origin_soa.get_class(),
- " ".join(soa_rdata_parts)))
- return new_soa
- def soa_update_check(self, origin_soa, new_soa):
- '''Check whether the new soa is valid. If the serial number is bigger
- than the old one, it is valid, then return True, otherwise, return
- False. Make sure the origin_soa and new_soa parameters are not none
- before invoke soa_update_check.
- Parameters:
- origin_soa, old SOA resource record.
- new_soa, new SOA resource record.
- Output:
- if the serial number of new soa is bigger than the old one, return
- True, otherwise return False.
- '''
- old_serial = self.__get_serial_internal(origin_soa)
- new_serial = self.__get_serial_internal(new_soa)
- if(new_serial > old_serial):
- return True
- else:
- return False
- def update_soa(self, origin_soa, inc_number = 1):
- ''' Update the soa number incrementally as RFC 2136. Please make sure
- that the origin_soa exists and not none before invoke this function.
- Parameters:
- origin_soa, the soa resource record which will be updated.
- inc_number, the number which will be added into the serial number of
- origin_soa, the default value is one.
- Output:
- The new origin soa whoes serial number has been updated.
- '''
- soa_num = self.__get_serial_internal(origin_soa)
- soa_num = soa_num + inc_number
- if soa_num.get_value() == 0:
- soa_num = soa_num + 1
- return self.__write_soa_internal(origin_soa, soa_num)
- class UpdateSession:
- '''Protocol handling for a single dynamic update request.
- This class is instantiated with a request message and some other
- information that will be used for handling the request. Its main
- method, handle(), will process the request, and normally build
- a response message according to the result. The application of this
- class can use the message to send a response to the client.
- '''
- def __init__(self, req_message, req_data, client_addr, zone_config):
- '''Constructor.
- Note: req_data is not really used as of #1512 but is listed since
- it's quite likely we need it in a subsequent task soon. We'll
- also need to get other parameters such as ACLs, for which, it's less
- clear in which form we want to get the information, so it's left
- open for now.
- Parameters:
- - req_message (isc.dns.Message) The request message. This must be
- in the PARSE mode.
- - req_data (binary) Wire format data of the request message.
- It will be used for TSIG verification if necessary.
- - client_addr (socket address) The address/port of the update client
- in the form of Python socket address object. This is mainly for
- logging and access control.
- - zone_config (ZoneConfig) A tentative container that encapsulates
- the server's zone configuration. See zone_config.py.
- (It'll soon need to be passed ACL in some way, too)
- '''
- self.__message = req_message
- self.__client_addr = client_addr
- self.__zone_config = zone_config
- self.__added_soa = None
- def get_message(self):
- '''Return the update message.
- After handle() is called, it's generally transformed to the response
- to be returned to the client; otherwise it would be identical to
- the request message passed on construction.
- '''
- return self.__message
- def handle(self):
- '''Handle the update request according to RFC2136.
- This method returns a tuple of the following three elements that
- indicate the result of the request.
- - Result code of the request processing, which are:
- UPDATE_SUCCESS Update request granted and succeeded.
- UPDATE_ERROR Some error happened to be reported in the response.
- UPDATE_DROP Error happened and no response should be sent.
- Except the case of UPDATE_DROP, the UpdateSession object will have
- created a response that is to be returned to the request client,
- which can be retrieved by get_message().
- - The name of the updated zone (isc.dns.Name object) in case of
- UPDATE_SUCCESS; otherwise None.
- - The RR class of the updated zone (isc.dns.RRClass object) in case
- of UPDATE_SUCCESS; otherwise None.
- '''
- try:
- self.__get_update_zone()
- # conceptual code that would follow
- prereq_result = self.__check_prerequisites()
- if prereq_result != Rcode.NOERROR():
- self.__make_response(prereq_result)
- return UPDATE_ERROR, self.__zname, self.__zclass
- # self.__check_update_acl()
- update_result = self.__do_update()
- if update_result != Rcode.NOERROR():
- self.__make_response(update_result)
- return UPDATE_ERROR, self.__zname, self.__zclass
- self.__make_response(Rcode.NOERROR())
- return UPDATE_SUCCESS, self.__zname, self.__zclass
- except UpdateError as e:
- if not e.nolog:
- logger.debug(logger.DBGLVL_TRACE_BASIC, LIBDDNS_UPDATE_ERROR,
- ClientFormatter(self.__client_addr),
- ZoneFormatter(e.zname, e.zclass), e)
- self.__make_response(e.rcode)
- return UPDATE_ERROR, None, None
- def __get_update_zone(self):
- '''Parse the zone section and find the zone to be updated.
- If the zone section is valid and the specified zone is found in
- the configuration, sets private member variables for this session:
- __datasrc_client: A matching data source that contains the specified
- zone
- __zname: The zone name as a Name object
- __zclass: The zone class as an RRClass object
- __finder: A ZoneFinder for this zone
- If this method raises an exception, these members are not set
- '''
- # Validation: the zone section must contain exactly one question,
- # and it must be of type SOA.
- n_zones = self.__message.get_rr_count(SECTION_ZONE)
- if n_zones != 1:
- raise UpdateError('Invalid number of records in zone section: ' +
- str(n_zones), None, None, Rcode.FORMERR())
- zrecord = self.__message.get_question()[0]
- if zrecord.get_type() != RRType.SOA():
- raise UpdateError('update zone section contains non-SOA',
- None, None, Rcode.FORMERR())
- # See if we're serving a primary zone specified in the zone section.
- zname = zrecord.get_name()
- zclass = zrecord.get_class()
- zone_type, datasrc_client = self.__zone_config.find_zone(zname, zclass)
- if zone_type == isc.ddns.zone_config.ZONE_PRIMARY:
- self.__zname = zname
- self.__zclass = zclass
- self.__datasrc_client = datasrc_client
- _, self.__finder = datasrc_client.find_zone(zname)
- return
- elif zone_type == isc.ddns.zone_config.ZONE_SECONDARY:
- # We are a secondary server; since we don't yet support update
- # forwarding, we return 'not implemented'.
- logger.debug(DBGLVL_TRACE_BASIC, LIBDDNS_UPDATE_FORWARD_FAIL,
- ClientFormatter(self.__client_addr),
- ZoneFormatter(zname, zclass))
- raise UpdateError('forward', zname, zclass, Rcode.NOTIMP(), True)
- # zone wasn't found
- logger.debug(DBGLVL_TRACE_BASIC, LIBDDNS_UPDATE_NOTAUTH,
- ClientFormatter(self.__client_addr),
- ZoneFormatter(zname, zclass))
- raise UpdateError('notauth', zname, zclass, Rcode.NOTAUTH(), True)
- def __make_response(self, rcode):
- '''Transform the internal message to the update response.
- According RFC2136 Section 3.8, the zone section will be cleared
- as well as other sections. The response Rcode will be set to the
- given value.
- '''
- self.__message.make_response()
- self.__message.clear_section(SECTION_ZONE)
- self.__message.set_rcode(rcode)
- def __prereq_rrset_exists(self, rrset):
- '''Check whether an rrset with the given name and type exists. Class,
- TTL, and Rdata (if any) of the given RRset are ignored.
- RFC2136 Section 2.4.1.
- Returns True if the prerequisite is satisfied, False otherwise.
- Note: the only thing used in the call to find() here is the
- result status. The actual data is immediately dropped. As
- a future optimization, we may want to add a find() option to
- only return what the result code would be (and not read/copy
- any actual data).
- '''
- result, _, _ = self.__finder.find(rrset.get_name(), rrset.get_type(),
- ZoneFinder.NO_WILDCARD |
- ZoneFinder.FIND_GLUE_OK)
- return result == ZoneFinder.SUCCESS
- def __prereq_rrset_exists_value(self, rrset):
- '''Check whether an rrset that matches name, type, and rdata(s) of the
- given rrset exists.
- RFC2136 Section 2.4.2
- Returns True if the prerequisite is satisfied, False otherwise.
- '''
- result, found_rrset, _ = self.__finder.find(rrset.get_name(),
- rrset.get_type(),
- ZoneFinder.NO_WILDCARD |
- ZoneFinder.FIND_GLUE_OK)
- if result == ZoneFinder.SUCCESS and\
- rrset.get_name() == found_rrset.get_name() and\
- rrset.get_type() == found_rrset.get_type():
- # We need to match all actual RRs, unfortunately there is no
- # direct order-independent comparison for rrsets, so this
- # a slightly inefficient way to handle that.
- # shallow copy of the rdata list, so we are sure that this
- # loop does not mess with actual data.
- found_rdata = copy.copy(found_rrset.get_rdata())
- for rdata in rrset.get_rdata():
- if rdata in found_rdata:
- found_rdata.remove(rdata)
- else:
- return False
- return len(found_rdata) == 0
- return False
- def __prereq_rrset_does_not_exist(self, rrset):
- '''Check whether no rrsets with the same name and type as the given
- rrset exist.
- RFC2136 Section 2.4.3.
- Returns True if the prerequisite is satisfied, False otherwise.
- '''
- return not self.__prereq_rrset_exists(rrset)
- def __prereq_name_in_use(self, rrset):
- '''Check whether the name of the given RRset is in use (i.e. has
- 1 or more RRs).
- RFC2136 Section 2.4.4
- Returns True if the prerequisite is satisfied, False otherwise.
- Note: the only thing used in the call to find_all() here is
- the result status. The actual data is immediately dropped. As
- a future optimization, we may want to add a find_all() option
- to only return what the result code would be (and not read/copy
- any actual data).
- '''
- result, rrsets, flags = self.__finder.find_all(rrset.get_name(),
- ZoneFinder.NO_WILDCARD |
- ZoneFinder.FIND_GLUE_OK)
- if result == ZoneFinder.SUCCESS and\
- (flags & ZoneFinder.RESULT_WILDCARD == 0):
- return True
- return False
- def __prereq_name_not_in_use(self, rrset):
- '''Check whether the name of the given RRset is not in use (i.e. does
- not exist at all, or is an empty nonterminal.
- RFC2136 Section 2.4.5.
- Returns True if the prerequisite is satisfied, False otherwise.
- '''
- return not self.__prereq_name_in_use(rrset)
- def __check_in_zone(self, rrset):
- '''Returns true if the name of the given rrset is equal to
- or a subdomain of the zname from the Zone Section.'''
- relation = rrset.get_name().compare(self.__zname).get_relation()
- return relation == NameComparisonResult.SUBDOMAIN or\
- relation == NameComparisonResult.EQUAL
- def __check_prerequisites(self):
- '''Check the prerequisites section of the UPDATE Message.
- RFC2136 Section 2.4.
- Returns a dns Rcode signaling either no error (Rcode.NOERROR())
- or that one of the prerequisites failed (any other Rcode).
- '''
- for rrset in self.__message.get_section(SECTION_PREREQUISITE):
- # First check if the name is in the zone
- if not self.__check_in_zone(rrset):
- logger.info(LIBDDNS_PREREQ_NOTZONE,
- ClientFormatter(self.__client_addr),
- ZoneFormatter(self.__zname, self.__zclass),
- RRsetFormatter(rrset))
- return Rcode.NOTZONE()
- # Algorithm taken from RFC2136 Section 3.2
- if rrset.get_class() == RRClass.ANY():
- if rrset.get_ttl().get_value() != 0 or\
- rrset.get_rdata_count() != 0:
- logger.info(LIBDDNS_PREREQ_FORMERR_ANY,
- ClientFormatter(self.__client_addr),
- ZoneFormatter(self.__zname, self.__zclass),
- RRsetFormatter(rrset))
- return Rcode.FORMERR()
- elif rrset.get_type() == RRType.ANY():
- if not self.__prereq_name_in_use(rrset):
- rcode = Rcode.NXDOMAIN()
- logger.info(LIBDDNS_PREREQ_NAME_IN_USE_FAILED,
- ClientFormatter(self.__client_addr),
- ZoneFormatter(self.__zname, self.__zclass),
- RRsetFormatter(rrset), rcode)
- return rcode
- else:
- if not self.__prereq_rrset_exists(rrset):
- rcode = Rcode.NXRRSET()
- logger.info(LIBDDNS_PREREQ_RRSET_EXISTS_FAILED,
- ClientFormatter(self.__client_addr),
- ZoneFormatter(self.__zname, self.__zclass),
- RRsetFormatter(rrset), rcode)
- return rcode
- elif rrset.get_class() == RRClass.NONE():
- if rrset.get_ttl().get_value() != 0 or\
- rrset.get_rdata_count() != 0:
- logger.info(LIBDDNS_PREREQ_FORMERR_NONE,
- ClientFormatter(self.__client_addr),
- ZoneFormatter(self.__zname, self.__zclass),
- RRsetFormatter(rrset))
- return Rcode.FORMERR()
- elif rrset.get_type() == RRType.ANY():
- if not self.__prereq_name_not_in_use(rrset):
- rcode = Rcode.YXDOMAIN()
- logger.info(LIBDDNS_PREREQ_NAME_NOT_IN_USE_FAILED,
- ClientFormatter(self.__client_addr),
- ZoneFormatter(self.__zname, self.__zclass),
- RRsetFormatter(rrset), rcode)
- return rcode
- else:
- if not self.__prereq_rrset_does_not_exist(rrset):
- rcode = Rcode.YXRRSET()
- logger.info(LIBDDNS_PREREQ_RRSET_DOES_NOT_EXIST_FAILED,
- ClientFormatter(self.__client_addr),
- ZoneFormatter(self.__zname, self.__zclass),
- RRsetFormatter(rrset), rcode)
- return rcode
- elif rrset.get_class() == self.__zclass:
- if rrset.get_ttl().get_value() != 0:
- logger.info(LIBDDNS_PREREQ_FORMERR,
- ClientFormatter(self.__client_addr),
- ZoneFormatter(self.__zname, self.__zclass),
- RRsetFormatter(rrset))
- return Rcode.FORMERR()
- else:
- if not self.__prereq_rrset_exists_value(rrset):
- rcode = Rcode.NXRRSET()
- logger.info(LIBDDNS_PREREQ_RRSET_EXISTS_VAL_FAILED,
- ClientFormatter(self.__client_addr),
- ZoneFormatter(self.__zname, self.__zclass),
- RRsetFormatter(rrset), rcode)
- return rcode
- else:
- logger.info(LIBDDNS_PREREQ_FORMERR_CLASS,
- ClientFormatter(self.__client_addr),
- ZoneFormatter(self.__zname, self.__zclass),
- RRsetFormatter(rrset))
- return Rcode.FORMERR()
- # All prerequisites are satisfied
- return Rcode.NOERROR()
- def __set_soa_rrset(self, rrset):
- '''Sets the given rrset to the member __added_soa (which
- is used by __do_update for updating the SOA record'''
- self.__added_soa = rrset
- def __do_prescan(self):
- '''Perform the prescan as defined in RFC2136 section 3.4.1.
- This method has a side-effect; it sets self._new_soa if
- it encounters the addition of a SOA record in the update
- list (so serial can be checked by update later, etc.).
- It puts the added SOA in self.__added_soa.
- '''
- for rrset in self.__message.get_section(SECTION_UPDATE):
- if not self.__check_in_zone(rrset):
- logger.info(LIBDDNS_UPDATE_NOTZONE,
- ClientFormatter(self.__client_addr),
- ZoneFormatter(self.__zname, self.__zclass),
- RRsetFormatter(rrset))
- return Rcode.NOTZONE()
- if rrset.get_class() == self.__zclass:
- # In fact, all metatypes are in a specific range,
- # so one check can test TKEY to ANY
- # (some value check is needed anyway, since we do
- # not have defined RRtypes for MAILA and MAILB)
- if rrset.get_type().get_code() >= 249:
- logger.info(LIBDDNS_UPDATE_ADD_BAD_TYPE,
- ClientFormatter(self.__client_addr),
- ZoneFormatter(self.__zname, self.__zclass),
- RRsetFormatter(rrset))
- return Rcode.FORMERR()
- if rrset.get_type() == RRType.SOA():
- # In case there's multiple soa records in the update
- # somehow, just take the last
- foreach_rr_in_rrset(rrset, self.__set_soa_rrset, rrset)
- elif rrset.get_class() == RRClass.ANY():
- if rrset.get_ttl().get_value() != 0:
- logger.info(LIBDDNS_UPDATE_DELETE_NONZERO_TTL,
- ClientFormatter(self.__client_addr),
- ZoneFormatter(self.__zname, self.__zclass),
- RRsetFormatter(rrset))
- return Rcode.FORMERR()
- if rrset.get_rdata_count() > 0:
- logger.info(LIBDDNS_UPDATE_DELETE_RRSET_NOT_EMPTY,
- ClientFormatter(self.__client_addr),
- ZoneFormatter(self.__zname, self.__zclass),
- RRsetFormatter(rrset))
- return Rcode.FORMERR()
- if rrset.get_type().get_code() >= 249 and\
- rrset.get_type().get_code() <= 254:
- logger.info(LIBDDNS_UPDATE_DELETE_BAD_TYPE,
- ClientFormatter(self.__client_addr),
- ZoneFormatter(self.__zname, self.__zclass),
- RRsetFormatter(rrset))
- return Rcode.FORMERR()
- elif rrset.get_class() == RRClass.NONE():
- if rrset.get_ttl().get_value() != 0:
- logger.info(LIBDDNS_UPDATE_DELETE_RR_NONZERO_TTL,
- ClientFormatter(self.__client_addr),
- ZoneFormatter(self.__zname, self.__zclass),
- RRsetFormatter(rrset))
- return Rcode.FORMERR()
- if rrset.get_type().get_code() >= 249:
- logger.info(LIBDDNS_UPDATE_DELETE_RR_BAD_TYPE,
- ClientFormatter(self.__client_addr),
- ZoneFormatter(self.__zname, self.__zclass),
- RRsetFormatter(rrset))
- return Rcode.FORMERR()
- else:
- logger.info(LIBDDNS_UPDATE_BAD_CLASS,
- ClientFormatter(self.__client_addr),
- ZoneFormatter(self.__zname, self.__zclass),
- RRsetFormatter(rrset))
- return Rcode.FORMERR()
- return Rcode.NOERROR()
- def __do_update_add_single_rr(self, diff, rr, existing_rrset):
- '''Helper for __do_update_add_rrs_to_rrset: only add the
- rr if it is not present yet
- (note that rr here should already be a single-rr rrset)
- '''
- if existing_rrset is None:
- diff.add_data(rr)
- else:
- rr_rdata = rr.get_rdata()[0]
- if not rr_rdata in existing_rrset.get_rdata():
- diff.add_data(rr)
- def __do_update_add_rrs_to_rrset(self, diff, rrset):
- '''Add the rrs from the given rrset to the diff.
- There is handling for a number of special cases mentioned
- in RFC2136;
- - If the addition is a CNAME, but existing data at its
- name is not, the addition is ignored, and vice versa.
- - If it is a CNAME, and existing data is too, it is
- replaced (existing data is deleted)
- An additional restriction is that SOA data is ignored as
- well (it is handled separately by the __do_update method).
- Note that in the (near) future, this method may have
- addition special-cases processing.
- '''
- # For a number of cases, we may need to remove data in the zone
- # (note; SOA is handled separately by __do_update, so that one
- # is not explicitely ignored here)
- if rrset.get_type() == RRType.SOA():
- return
- result, orig_rrset, _ = self.__finder.find(rrset.get_name(),
- rrset.get_type(),
- ZoneFinder.NO_WILDCARD |
- ZoneFinder.FIND_GLUE_OK)
- if result == self.__finder.CNAME:
- # Ignore non-cname rrs that try to update CNAME records
- # (if rrset itself is a CNAME, the finder result would be
- # SUCCESS, see next case)
- return
- elif result == ZoneFinder.SUCCESS:
- # if update is cname, and zone rr is not, ignore
- if rrset.get_type() == RRType.CNAME():
- # Remove original CNAME record (the new one
- # is added below)
- diff.delete_data(orig_rrset)
- # We do not have WKS support at this time, but if there
- # are special Update equality rules such as for WKS, and
- # we do have support for the type, this is where the check
- # (and potential delete) would go.
- elif result == ZoneFinder.NXRRSET:
- # There is data present, but not for this type.
- # If this type is CNAME, ignore the update
- if rrset.get_type() == RRType.CNAME():
- return
- foreach_rr_in_rrset(rrset, self.__do_update_add_single_rr, diff, rrset, orig_rrset)
- def __do_update_delete_rrset(self, diff, rrset):
- '''Deletes the rrset with the name and type of the given
- rrset from the zone data (by putting all existing data
- in the given diff as delete statements).
- Special cases: if the delete statement is for the
- zone's apex, and the type is either SOA or NS, it
- is ignored.'''
- result, to_delete, _ = self.__finder.find(rrset.get_name(),
- rrset.get_type(),
- ZoneFinder.NO_WILDCARD |
- ZoneFinder.FIND_GLUE_OK)
- if to_delete.get_name() == self.__zname and\
- (to_delete.get_type() == RRType.SOA() or\
- to_delete.get_type() == RRType.NS()):
- # ignore
- return
- foreach_rr_in_rrset(to_delete, diff.delete_data, to_delete)
- def __ns_deleter_helper(self, diff, rrset):
- '''Special case helper for deleting NS resource records
- at the zone apex. In that scenario, the last NS record
- may never be removed (and any action that would do so
- should be ignored).
- '''
- result, orig_rrset, _ = self.__finder.find(rrset.get_name(),
- rrset.get_type(),
- ZoneFinder.NO_WILDCARD |
- ZoneFinder.FIND_GLUE_OK)
- # Even a real rrset comparison wouldn't help here...
- # The goal is to make sure that after deletion of the
- # given rrset, at least 1 NS record is left (at the apex).
- # So we make a (shallow) copy of the existing rrset,
- # and for each rdata in the to_delete set, we check if it wouldn't
- # delete the last one. If it would, that specific one is ignored.
- # If it would not, the rdata is removed from the temporary list
- orig_rrset_rdata = copy.copy(orig_rrset.get_rdata())
- for rdata in rrset.get_rdata():
- if len(orig_rrset_rdata) == 1 and rdata == orig_rrset_rdata[0]:
- # ignore
- continue
- else:
- # create an individual RRset for deletion
- to_delete = isc.dns.RRset(rrset.get_name(),
- rrset.get_class(),
- rrset.get_type(),
- rrset.get_ttl())
- to_delete.add_rdata(rdata)
- orig_rrset_rdata.remove(rdata)
- diff.delete_data(to_delete)
- def __do_update_delete_name(self, diff, rrset):
- '''Delete all data at the name of the given rrset,
- by adding all data found by find_all as delete statements
- to the given diff.
- Special case: if the name is the zone's apex, SOA and
- NS records are kept.
- '''
- result, rrsets, flags = self.__finder.find_all(rrset.get_name(),
- ZoneFinder.NO_WILDCARD |
- ZoneFinder.FIND_GLUE_OK)
- if result == ZoneFinder.SUCCESS and\
- (flags & ZoneFinder.RESULT_WILDCARD == 0):
- for to_delete in rrsets:
- # if name == self.__zname and type is soa or ns, don't delete!
- if to_delete.get_name() == self.__zname and\
- (to_delete.get_type() == RRType.SOA() or
- to_delete.get_type() == RRType.NS()):
- continue
- else:
- foreach_rr_in_rrset(to_delete, diff.delete_data, to_delete)
- def __do_update_delete_rrs_from_rrset(self, diff, rrset):
- '''Deletes all resource records in the given rrset from the
- zone. Resource records that do not exist are ignored.
- If the rrset if of type SOA, it is ignored.
- Uses the __ns_deleter_helper if the rrset's name is the
- zone's apex, and the type is NS.
- '''
- # Delete all rrs in the rrset, except if name=self.__zname and type=soa, or
- # type = ns and there is only one left (...)
- # The delete does not want class NONE, we would not have gotten here
- # if it wasn't, but now is a good time to change it to the zclass.
- to_delete = convert_rrset_class(rrset, self.__zclass)
- if rrset.get_name() == self.__zname:
- if rrset.get_type() == RRType.SOA():
- # ignore
- return
- elif rrset.get_type() == RRType.NS():
- # hmm. okay. annoying. There must be at least one left,
- # delegate to helper method
- self.__ns_deleter_helper(diff, to_delete)
- return
- foreach_rr_in_rrset(to_delete, diff.delete_data, to_delete)
- def __update_soa(self, diff):
- '''Checks the member value __added_soa, and depending on
- whether it has been set and what its value is, creates
- a new SOA if necessary.
- Then removes the original SOA and adds the new one,
- by adding the needed operations to the given diff.'''
- # Get the existing SOA
- # if a new soa was specified, add that one, otherwise, do the
- # serial magic and add the newly created one
- # get it from DS and to increment and stuff
- result, old_soa, _ = self.__finder.find(self.__zname, RRType.SOA(),
- ZoneFinder.NO_WILDCARD |
- ZoneFinder.FIND_GLUE_OK)
- # We may implement recovering from missing SOA data at some point, but
- # for now servfail on such a broken state
- if result != ZoneFinder.SUCCESS:
- raise UpdateError("Error finding SOA record in datasource.",
- self.__zname, self.__zclass, Rcode.SERVFAIL())
- serial_operation = DDNS_SOA()
- if self.__added_soa is not None and\
- serial_operation.soa_update_check(old_soa, self.__added_soa):
- new_soa = self.__added_soa
- else:
- # increment goes here
- new_soa = serial_operation.update_soa(old_soa)
- diff.delete_data(old_soa)
- diff.add_data(new_soa)
- def __do_update(self):
- '''Scan, check, and execute the Update section in the
- DDNS Update message.
- Returns an Rcode to signal the result (NOERROR upon success,
- any error result otherwise).
- '''
- # prescan
- prescan_result = self.__do_prescan()
- if prescan_result != Rcode.NOERROR():
- return prescan_result
- # update
- try:
- # create an ixfr-out-friendly diff structure to work on
- diff = isc.xfrin.diff.Diff(self.__datasrc_client, self.__zname,
- journaling=True, single_update_mode=True)
- # Do special handling for SOA first
- self.__update_soa(diff)
- # Algorithm from RFC2136 Section 3.4
- # Note that this works on full rrsets, not individual RRs.
- # Some checks might be easier with individual RRs, but only if we
- # would use the ZoneUpdater directly (so we can query the
- # 'zone-as-it-would-be-so-far'. However, due to the current use
- # of the Diff class, this is not the case, and therefore it
- # is easier to work with full rrsets for the most parts
- # (less lookups needed; conversion to individual rrs is
- # the same offort whether it is done here or in the several
- # do_update statements)
- for rrset in self.__message.get_section(SECTION_UPDATE):
- if rrset.get_class() == self.__zclass:
- self.__do_update_add_rrs_to_rrset(diff, rrset)
- elif rrset.get_class() == RRClass.ANY():
- if rrset.get_type() == RRType.ANY():
- self.__do_update_delete_name(diff, rrset)
- else:
- self.__do_update_delete_rrset(diff, rrset)
- elif rrset.get_class() == RRClass.NONE():
- self.__do_update_delete_rrs_from_rrset(diff, rrset)
- diff.commit()
- return Rcode.NOERROR()
- except isc.datasrc.Error as dse:
- logger.info(LIBDDNS_UPDATE_DATASRC_ERROR, dse)
- return Rcode.SERVFAIL()
- except Exception as uce:
- logger.error(LIBDDNS_UPDATE_UNCAUGHT_EXCEPTION,
- ClientFormatter(self.__client_addr),
- ZoneFormatter(self.__zname, self.__zclass),
- uce)
- return Rcode.SERVFAIL()
|