def __do_send(self): while True: data = self.__send_buffer.get_data(self.__send_marker) if data is None: # send complete; clear the internal buffer for next possible # send. logger.debug(logger.DBGLVL_TRACE_DETAIL, PYSERVER_COMMON_DNS_TCP_SEND_DONE, ClientFormatter(self.__remote_addr), self.__send_marker) self.__send_buffer = None self.__send_marker = 0 return self.SEND_DONE try: cc = self.__sock.send(data) except socket.error as ex: total_len = self.__send_buffer.get_total_len() if ex.errno == errno.EAGAIN: logger.debug(logger.DBGLVL_TRACE_DETAIL, PYSERVER_COMMON_DNS_TCP_SEND_PENDING, ClientFormatter(self.__remote_addr), self.__send_marker, total_len) return self.SENDING logger.warn(PYSERVER_COMMON_DNS_TCP_SEND_FAILED, ClientFormatter(self.__remote_addr), self.__send_marker, total_len, ex) self.__sock.close() self.__sock = None return self.CLOSED self.__send_marker += cc
def handle(self): '''Handle the update request according to RFC2136. This method returns a tuple of the following 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(). If it's UPDATE_DROP, subsequent call to get_message() returns None. - The name of the updated zone (bundy.dns.Name object) in case of UPDATE_SUCCESS; otherwise None. - The RR class of the updated zone (bundy.dns.RRClass object) in case of UPDATE_SUCCESS; otherwise None. - The name of the used data source associated with DataSourceClient in case of UPDATE_SUCCESS; otherwise None. ''' try: self._get_update_zone() # Contrary to what RFC2136 specifies, we do ACL checks before # prerequisites. It's now generally considered to be a bad # idea, and actually does harm such as information # leak. It should make more sense to prevent any security issues # by performing ACL check as early as possible. self.__check_update_acl(self.__zname, self.__zclass) self._create_diff() prereq_result = self.__check_prerequisites() if prereq_result != Rcode.NOERROR: self.__make_response(prereq_result) return UPDATE_ERROR, self.__zname, self.__zclass, None update_result = self.__do_update() if update_result != Rcode.NOERROR: self.__make_response(update_result) return UPDATE_ERROR, self.__zname, self.__zclass, None self.__make_response(Rcode.NOERROR) datasrc_name = self.__datasrc_client.get_datasource_name() return UPDATE_SUCCESS, self.__zname, self.__zclass, datasrc_name except UpdateError as e: if not e.nolog: logger.debug(logger.DBGLVL_TRACE_BASIC, LIBDDNS_UPDATE_PROCESSING_FAILED, ClientFormatter(self.__client_addr, self.__tsig), ZoneFormatter(e.zname, e.zclass), e) # If RCODE is specified, create a corresponding resonse and return # ERROR; otherwise clear the message and return DROP. if e.rcode is not None: self.__make_response(e.rcode) return UPDATE_ERROR, None, None, None self.__message = None return UPDATE_DROP, None, None, None except bundy.datasrc.Error as e: logger.error(LIBDDNS_DATASRC_ERROR, ClientFormatter(self.__client_addr, self.__tsig), e) self.__make_response(Rcode.SERVFAIL) return UPDATE_ERROR, None, 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 If this method raises an exception, these members are not set. Note: This method is protected for ease of use in tests, where methods are tested that need the setup done here without calling the full handle() method. ''' # 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 == bundy.ddns.zone_config.ZONE_PRIMARY: self.__datasrc_client = datasrc_client self.__zname = zname self.__zclass = zclass return elif zone_type == bundy.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, self.__tsig), 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, self.__tsig), ZoneFormatter(zname, zclass)) raise UpdateError('notauth', zname, zclass, Rcode.NOTAUTH, True)
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: # Do special handling for SOA first self.__update_soa() # 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 effort 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(rrset) elif rrset.get_class() == RRClass.ANY: if rrset.get_type() == RRType.ANY: self.__do_update_delete_name(rrset) else: self.__do_update_delete_rrset(rrset) elif rrset.get_class() == RRClass.NONE: self.__do_update_delete_rrs_from_rrset(rrset) if not check_zone( self.__zname, self.__zclass, self.__diff.get_rrset_collection(), (self.__validate_error, self.__validate_warning)): raise UpdateError('Validation of the new zone failed', self.__zname, self.__zclass, Rcode.REFUSED) self.__diff.commit() return Rcode.NOERROR except UpdateError: # Propagate UpdateError exceptions (don't catch them in the # blocks below) raise except bundy.datasrc.Error as dse: logger.info(LIBDDNS_UPDATE_DATASRC_COMMIT_FAILED, 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
def __check_update_acl(self, zname, zclass): '''Apply update ACL for the zone to be updated.''' acl = self.__zone_config.get_update_acl(zname, zclass) action = acl.execute( bundy.acl.dns.RequestContext( (self.__client_addr[0], self.__client_addr[1]), self.__tsig)) if action == REJECT: logger.info(LIBDDNS_UPDATE_DENIED, ClientFormatter(self.__client_addr, self.__tsig), ZoneFormatter(zname, zclass)) raise UpdateError('rejected', zname, zclass, Rcode.REFUSED, True) if action == DROP: logger.info(LIBDDNS_UPDATE_DROPPED, ClientFormatter(self.__client_addr, self.__tsig), ZoneFormatter(zname, zclass)) raise UpdateError('dropped', zname, zclass, None, True) logger.debug(logger.DBGLVL_TRACE_BASIC, LIBDDNS_UPDATE_APPROVED, ClientFormatter(self.__client_addr, self.__tsig), ZoneFormatter(zname, zclass))
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 for rr in foreach_rr(rrset): self.__set_soa_rrset(rr) 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 __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). ''' # Temporary array to store exact-match RRsets exact_match_rrsets = [] 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: collect_rrsets(exact_match_rrsets, rrset) else: logger.info(LIBDDNS_PREREQ_FORMERR_CLASS, ClientFormatter(self.__client_addr), ZoneFormatter(self.__zname, self.__zclass), RRsetFormatter(rrset)) return Rcode.FORMERR for collected_rrset in exact_match_rrsets: if not self.__prereq_rrset_exists_value(collected_rrset): rcode = Rcode.NXRRSET logger.info(LIBDDNS_PREREQ_RRSET_EXISTS_VAL_FAILED, ClientFormatter(self.__client_addr), ZoneFormatter(self.__zname, self.__zclass), RRsetFormatter(collected_rrset), rcode) return rcode # All prerequisites are satisfied return Rcode.NOERROR