|
@@ -28,7 +28,9 @@ from optparse import OptionParser, OptionValueError
|
|
|
from isc.config.ccsession import *
|
|
|
from isc.notify import notify_out
|
|
|
import isc.util.process
|
|
|
+from isc.datasrc import DataSourceClient, ZoneFinder
|
|
|
import isc.net.parse
|
|
|
+from isc.xfrin.diff import Diff
|
|
|
from isc.log_messages.xfrin_messages import *
|
|
|
|
|
|
isc.log.init("b10-xfrin")
|
|
@@ -62,6 +64,9 @@ ZONE_MANAGER_MODULE_NAME = 'Zonemgr'
|
|
|
REFRESH_FROM_ZONEMGR = 'refresh_from_zonemgr'
|
|
|
ZONE_XFRIN_FAILED = 'zone_xfrin_failed'
|
|
|
|
|
|
+# Constants for debug levels, to be removed when we have #1074.
|
|
|
+DBG_XFRIN_TRACE = 3
|
|
|
+
|
|
|
# These two default are currently hard-coded. For config this isn't
|
|
|
# necessary, but we need these defaults for optional command arguments
|
|
|
# (TODO: have similar support to get default values for command
|
|
@@ -77,6 +82,11 @@ XFRIN_FAIL = 1
|
|
|
class XfrinException(Exception):
|
|
|
pass
|
|
|
|
|
|
+class XfrinProtocolError(Exception):
|
|
|
+ '''An exception raised for errors encountered in xfrin protocol handling.
|
|
|
+ '''
|
|
|
+ pass
|
|
|
+
|
|
|
class XfrinZoneInfoException(Exception):
|
|
|
"""This exception is raised if there is an error in the given
|
|
|
configuration (part), or when a command does not have a required
|
|
@@ -112,29 +122,358 @@ def _check_zone_class(zone_class_str):
|
|
|
except InvalidRRClass as irce:
|
|
|
raise XfrinZoneInfoException("bad zone class: " + zone_class_str + " (" + str(irce) + ")")
|
|
|
|
|
|
+def get_soa_serial(soa_rdata):
|
|
|
+ '''Extract the serial field of an SOA RDATA and returns it as an intger.
|
|
|
+
|
|
|
+ We don't have to be very efficient here, so we first dump the entire RDATA
|
|
|
+ as a string and convert the first corresponding field. This should be
|
|
|
+ sufficient in practice, but may not always work when the MNAME or RNAME
|
|
|
+ contains an (escaped) space character in their labels. Ideally there
|
|
|
+ should be a more direct and convenient way to get access to the SOA
|
|
|
+ fields.
|
|
|
+ '''
|
|
|
+ return int(soa_rdata.to_text().split()[2])
|
|
|
+
|
|
|
+class XfrinState:
|
|
|
+ '''
|
|
|
+ The states of the incomding *XFR state machine.
|
|
|
+
|
|
|
+ We (will) handle both IXFR and AXFR with a single integrated state
|
|
|
+ machine because they cannot be distinguished immediately - an AXFR
|
|
|
+ response to an IXFR request can only be detected when the first two (2)
|
|
|
+ response RRs have already been received.
|
|
|
+
|
|
|
+ The following diagram summarizes the state transition. After sending
|
|
|
+ the query, xfrin starts the process with the InitialSOA state (all
|
|
|
+ IXFR/AXFR response begins with an SOA). When it reaches IXFREnd
|
|
|
+ or AXFREnd, the process successfully completes.
|
|
|
+
|
|
|
+ (AXFR or
|
|
|
+ (recv SOA) AXFR-style IXFR) (SOA, add)
|
|
|
+ InitialSOA------->FirstData------------->AXFR--------->AXFREnd
|
|
|
+ | | ^ (post xfr
|
|
|
+ | | | checks, then
|
|
|
+ | +--+ commit)
|
|
|
+ | (non SOA, add)
|
|
|
+ |
|
|
|
+ | (non SOA, delete)
|
|
|
+ (pure IXFR,| +-------+
|
|
|
+ keep handling)| (Delete SOA) V |
|
|
|
+ + ->IXFRDeleteSOA------>IXFRDelete--+
|
|
|
+ ^ |
|
|
|
+ (see SOA, not end, | (see SOA)|
|
|
|
+ commit, keep handling) | |
|
|
|
+ | V
|
|
|
+ +---------IXFRAdd<----------+IXFRAddSOA
|
|
|
+ (non SOA, add)| ^ | (Add SOA)
|
|
|
+ ----------+ |
|
|
|
+ |(see SOA w/ end serial, commit changes)
|
|
|
+ V
|
|
|
+ IXFREnd
|
|
|
+
|
|
|
+ Note that changes are committed for every "difference sequence"
|
|
|
+ (i.e. changes for one SOA update). This means when an IXFR response
|
|
|
+ contains multiple difference sequences and something goes wrong
|
|
|
+ after several commits, these changes have been published and visible
|
|
|
+ to clients even if the IXFR session is subsequently aborted.
|
|
|
+ It is not clear if this is valid in terms of the protocol specification.
|
|
|
+ Section 4 of RFC 1995 states:
|
|
|
+
|
|
|
+ An IXFR client, should only replace an older version with a newer
|
|
|
+ version after all the differences have been successfully processed.
|
|
|
+
|
|
|
+ If this "replacement" is for the changes of one difference sequence
|
|
|
+ and "all the differences" mean the changes for that sequence, this
|
|
|
+ implementation strictly follows what RFC states. If this is for
|
|
|
+ the entire IXFR response (that may contain multiple sequences),
|
|
|
+ we should implement it with one big transaction and one final commit
|
|
|
+ at the very end.
|
|
|
+
|
|
|
+ For now, we implement it with multiple smaller commits for two
|
|
|
+ reasons. First, this is what BIND 9 does, and we generally port
|
|
|
+ the implementation logic here. BIND 9 has been supporting IXFR
|
|
|
+ for many years, so the fact that it still behaves this way
|
|
|
+ probably means it at least doesn't cause a severe operational
|
|
|
+ problem in practice. Second, especially because BIND 10 would
|
|
|
+ often uses a database backend, a larger transaction could cause an
|
|
|
+ undesirable effects, e.g. suspending normal lookups for a longer
|
|
|
+ period depending on the characteristics of the database. Even if
|
|
|
+ we find something wrong in a later sequeunce and abort the
|
|
|
+ session, we can start another incremental update from what has
|
|
|
+ been validated, or we can switch to AXFR to replace the zone
|
|
|
+ completely.
|
|
|
+
|
|
|
+ This implementation uses the state design pattern, where each state
|
|
|
+ is represented as a subclass of the base XfrinState class. Each concrete
|
|
|
+ subclass of XfrinState is assumed to define two methods: handle_rr() and
|
|
|
+ finish_message(). These methods handle specific part of XFR protocols
|
|
|
+ and (if necessary) perform the state transition.
|
|
|
+
|
|
|
+ Conceptually, XfrinState and its subclasses are a "friend" of
|
|
|
+ XfrinConnection and are assumed to be allowed to access its internal
|
|
|
+ information (even though Python does not have a strict access control
|
|
|
+ between different classes).
|
|
|
+
|
|
|
+ The XfrinState and its subclasses are designed to be stateless, and
|
|
|
+ can be used as singleton objects. For now, however, we always instantiate
|
|
|
+ a new object for every state transition, partly because the introduction
|
|
|
+ of singleton will make a code bit complicated, and partly because
|
|
|
+ the overhead of object instantiotion wouldn't be significant for xfrin.
|
|
|
+
|
|
|
+ '''
|
|
|
+ def set_xfrstate(self, conn, new_state):
|
|
|
+ '''Set the XfrConnection to a given new state.
|
|
|
+
|
|
|
+ As a "friend" class, this method intentionally gets access to the
|
|
|
+ connection's "private" method.
|
|
|
+
|
|
|
+ '''
|
|
|
+ conn._XfrinConnection__set_xfrstate(new_state)
|
|
|
+
|
|
|
+ def handle_rr(self, conn):
|
|
|
+ '''Handle one RR of an XFR response message.
|
|
|
+
|
|
|
+ Depending on the state, the RR is generally added or deleted in the
|
|
|
+ corresponding data source, or in some special cases indicates
|
|
|
+ a specifi transition, such as starting a new IXFR difference
|
|
|
+ sequence or completing the session.
|
|
|
+
|
|
|
+ All subclass has their specific behaviors for this method, so
|
|
|
+ there is no default definition. If the base class version
|
|
|
+ is called, it's a bug of the caller, and it's notified via
|
|
|
+ an XfrinException exception.
|
|
|
+
|
|
|
+ This method returns a boolean value: True if the given RR was
|
|
|
+ fully handled and the caller should go to the next RR; False
|
|
|
+ if the caller needs to call this method with the (possibly) new
|
|
|
+ state for the same RR again.
|
|
|
+
|
|
|
+ '''
|
|
|
+ raise XfrinException("Internal bug: " +
|
|
|
+ "XfrinState.handle_rr() called directly")
|
|
|
+
|
|
|
+ def finish_message(self, conn):
|
|
|
+ '''Perform any final processing after handling all RRs of a response.
|
|
|
+
|
|
|
+ This method then returns a boolean indicating whether to continue
|
|
|
+ receiving the message. Unless it's in the end of the entire XFR
|
|
|
+ session, we should continue, so this default method simply returns
|
|
|
+ True.
|
|
|
+
|
|
|
+ '''
|
|
|
+ return True
|
|
|
+
|
|
|
+class XfrinInitialSOA(XfrinState):
|
|
|
+ def handle_rr(self, conn, rr):
|
|
|
+ if rr.get_type() != RRType.SOA():
|
|
|
+ raise XfrinProtocolError('First RR in zone transfer must be SOA ('
|
|
|
+ + rr.get_type().to_text() + ' received)')
|
|
|
+ conn._end_serial = get_soa_serial(rr.get_rdata()[0])
|
|
|
+
|
|
|
+ # FIXME: we need to check the serial is actually greater than ours.
|
|
|
+ # To do so, however, we need to implement serial number arithmetic.
|
|
|
+ # Although it wouldn't be a big task, we'll leave it for a separate
|
|
|
+ # task for now. (Always performing xfr could be inefficient, but
|
|
|
+ # shouldn't do any harm otherwise)
|
|
|
+
|
|
|
+ self.set_xfrstate(conn, XfrinFirstData())
|
|
|
+ return True
|
|
|
+
|
|
|
+class XfrinFirstData(XfrinState):
|
|
|
+ def handle_rr(self, conn, rr):
|
|
|
+ '''Handle the first RR after initial SOA in an XFR session.
|
|
|
+
|
|
|
+ This state happens exactly once in an XFR session, where
|
|
|
+ we decide whether it's incremental update ("real" IXFR) or
|
|
|
+ non incremental update (AXFR or AXFR-style IXFR).
|
|
|
+ If we initiated IXFR and the transfer begins with two SOAs
|
|
|
+ (the serial of the second one being equal to our serial),
|
|
|
+ it's incremental; otherwise it's non incremental.
|
|
|
+
|
|
|
+ This method always return False (unlike many other handle_rr()
|
|
|
+ methods) because this first RR must be examined again in the
|
|
|
+ determined update context.
|
|
|
+
|
|
|
+ Note that in the non incremental case the RR should normally be
|
|
|
+ something other SOA, but it's still possible it's an SOA with a
|
|
|
+ different serial than ours. The only possible interpretation at
|
|
|
+ this point is that it's non incremental update that only consists
|
|
|
+ of the SOA RR. It will result in broken zone (for example, it
|
|
|
+ wouldn't even contain an apex NS) and should be rejected at post
|
|
|
+ XFR processing, but in terms of the XFR session processing we
|
|
|
+ accept it and move forward.
|
|
|
+
|
|
|
+ Note further that, in the half-broken SOA-only transfer case,
|
|
|
+ these two SOAs are supposed to be the same as stated in Section 2.2
|
|
|
+ of RFC 5936. We don't check that condition here, either; we'll
|
|
|
+ leave whether and how to deal with that situation to the end of
|
|
|
+ the processing of non incremental update. See also a related
|
|
|
+ discussion at the IETF dnsext wg:
|
|
|
+ http://www.ietf.org/mail-archive/web/dnsext/current/msg07908.html
|
|
|
+
|
|
|
+ '''
|
|
|
+ if conn._request_type == RRType.IXFR() and \
|
|
|
+ rr.get_type() == RRType.SOA() and \
|
|
|
+ conn._request_serial == get_soa_serial(rr.get_rdata()[0]):
|
|
|
+ logger.debug(DBG_XFRIN_TRACE, XFRIN_GOT_INCREMENTAL_RESP,
|
|
|
+ conn.zone_str())
|
|
|
+ self.set_xfrstate(conn, XfrinIXFRDeleteSOA())
|
|
|
+ else:
|
|
|
+ logger.debug(DBG_XFRIN_TRACE, XFRIN_GOT_NONINCREMENTAL_RESP,
|
|
|
+ conn.zone_str())
|
|
|
+ # We are now going to add RRs to the new zone. We need create
|
|
|
+ # a Diff object. It will be used throughtout the XFR session.
|
|
|
+ conn._diff = Diff(conn._datasrc_client, conn._zone_name, True)
|
|
|
+ self.set_xfrstate(conn, XfrinAXFR())
|
|
|
+ return False
|
|
|
+
|
|
|
+class XfrinIXFRDeleteSOA(XfrinState):
|
|
|
+ def handle_rr(self, conn, rr):
|
|
|
+ if rr.get_type() != RRType.SOA():
|
|
|
+ # this shouldn't happen; should this occur it means an internal
|
|
|
+ # bug.
|
|
|
+ raise XfrinException(rr.get_type().to_text() +
|
|
|
+ ' RR is given in IXFRDeleteSOA state')
|
|
|
+ # This is the beginning state of one difference sequence (changes
|
|
|
+ # for one SOA update). We need to create a new Diff object now.
|
|
|
+ conn._diff = Diff(conn._datasrc_client, conn._zone_name)
|
|
|
+ conn._diff.delete_data(rr)
|
|
|
+ self.set_xfrstate(conn, XfrinIXFRDelete())
|
|
|
+ return True
|
|
|
+
|
|
|
+class XfrinIXFRDelete(XfrinState):
|
|
|
+ def handle_rr(self, conn, rr):
|
|
|
+ if rr.get_type() == RRType.SOA():
|
|
|
+ # This is the only place where current_serial is set
|
|
|
+ conn._current_serial = get_soa_serial(rr.get_rdata()[0])
|
|
|
+ self.set_xfrstate(conn, XfrinIXFRAddSOA())
|
|
|
+ return False
|
|
|
+ conn._diff.delete_data(rr)
|
|
|
+ return True
|
|
|
+
|
|
|
+class XfrinIXFRAddSOA(XfrinState):
|
|
|
+ def handle_rr(self, conn, rr):
|
|
|
+ if rr.get_type() != RRType.SOA():
|
|
|
+ # this shouldn't happen; should this occur it means an internal
|
|
|
+ # bug.
|
|
|
+ raise XfrinException(rr.get_type().to_text() +
|
|
|
+ ' RR is given in IXFRAddSOA state')
|
|
|
+ conn._diff.add_data(rr)
|
|
|
+ self.set_xfrstate(conn, XfrinIXFRAdd())
|
|
|
+ return True
|
|
|
+
|
|
|
+class XfrinIXFRAdd(XfrinState):
|
|
|
+ def handle_rr(self, conn, rr):
|
|
|
+ if rr.get_type() == RRType.SOA():
|
|
|
+ soa_serial = get_soa_serial(rr.get_rdata()[0])
|
|
|
+ if soa_serial == conn._end_serial:
|
|
|
+ conn._diff.commit()
|
|
|
+ self.set_xfrstate(conn, XfrinIXFREnd())
|
|
|
+ return True
|
|
|
+ elif soa_serial != conn._current_serial:
|
|
|
+ raise XfrinProtocolError('IXFR out of sync: expected ' +
|
|
|
+ 'serial ' +
|
|
|
+ str(conn._current_serial) +
|
|
|
+ ', got ' + str(soa_serial))
|
|
|
+ else:
|
|
|
+ conn._diff.commit()
|
|
|
+ self.set_xfrstate(conn, XfrinIXFRDeleteSOA())
|
|
|
+ return False
|
|
|
+ conn._diff.add_data(rr)
|
|
|
+ return True
|
|
|
+
|
|
|
+class XfrinIXFREnd(XfrinState):
|
|
|
+ def handle_rr(self, conn, rr):
|
|
|
+ raise XfrinProtocolError('Extra data after the end of IXFR diffs: ' +
|
|
|
+ rr.to_text())
|
|
|
+
|
|
|
+ def finish_message(self, conn):
|
|
|
+ '''Final processing after processing an entire IXFR session.
|
|
|
+
|
|
|
+ There will be more actions here, but for now we simply return False,
|
|
|
+ indicating there will be no more message to receive.
|
|
|
+
|
|
|
+ '''
|
|
|
+ return False
|
|
|
+
|
|
|
+class XfrinAXFR(XfrinState):
|
|
|
+ def handle_rr(self, conn, rr):
|
|
|
+ """
|
|
|
+ Handle the RR by putting it into the zone.
|
|
|
+ """
|
|
|
+ conn._diff.add_data(rr)
|
|
|
+ if rr.get_type() == RRType.SOA():
|
|
|
+ # SOA means end. Don't commit it yet - we need to perform
|
|
|
+ # post-transfer checks
|
|
|
+
|
|
|
+ soa_serial = get_soa_serial(rr.get_rdata()[0])
|
|
|
+ if conn._end_serial != soa_serial:
|
|
|
+ logger.warn(XFRIN_AXFR_INCONSISTENT_SOA, conn.zone_str(),
|
|
|
+ conn._end_serial, soa_serial)
|
|
|
+
|
|
|
+ self.set_xfrstate(conn, XfrinAXFREnd())
|
|
|
+ # Yes, we've eaten this RR.
|
|
|
+ return True
|
|
|
+
|
|
|
+class XfrinAXFREnd(XfrinState):
|
|
|
+ def handle_rr(self, conn, rr):
|
|
|
+ raise XfrinProtocolError('Extra data after the end of AXFR: ' +
|
|
|
+ rr.to_text())
|
|
|
+
|
|
|
+ def finish_message(self, conn):
|
|
|
+ """
|
|
|
+ Final processing after processing an entire AXFR session.
|
|
|
+
|
|
|
+ In this process all the AXFR changes are committed to the
|
|
|
+ data source.
|
|
|
+
|
|
|
+ There might be more actions here, but for now we simply return False,
|
|
|
+ indicating there will be no more message to receive.
|
|
|
+
|
|
|
+ """
|
|
|
+ conn._diff.commit()
|
|
|
+ return False
|
|
|
+
|
|
|
class XfrinConnection(asyncore.dispatcher):
|
|
|
'''Do xfrin in this class. '''
|
|
|
|
|
|
def __init__(self,
|
|
|
- sock_map, zone_name, rrclass, db_file, shutdown_event,
|
|
|
- master_addrinfo, tsig_key = None, verbose = False,
|
|
|
- idle_timeout = 60):
|
|
|
- ''' idle_timeout: max idle time for read data from socket.
|
|
|
- db_file: specify the data source file.
|
|
|
- check_soa: when it's true, check soa first before sending xfr query
|
|
|
+ sock_map, zone_name, rrclass, datasrc_client,
|
|
|
+ shutdown_event, master_addrinfo, tsig_key=None,
|
|
|
+ idle_timeout=60):
|
|
|
+ '''Constructor of the XfirnConnection class.
|
|
|
+
|
|
|
+ idle_timeout: max idle time for read data from socket.
|
|
|
+ datasrc_client: the data source client object used for the XFR session.
|
|
|
+ This will eventually replace db_file completely.
|
|
|
+
|
|
|
'''
|
|
|
|
|
|
asyncore.dispatcher.__init__(self, map=sock_map)
|
|
|
- self.create_socket(master_addrinfo[0], master_addrinfo[1])
|
|
|
+
|
|
|
+ # The XFR state. Conceptually this is purely private, so we emphasize
|
|
|
+ # the fact by the double underscore. Other classes are assumed to
|
|
|
+ # get access to this via get_xfrstate(), and only XfrinState classes
|
|
|
+ # are assumed to be allowed to modify it via __set_xfrstate().
|
|
|
+ self.__state = None
|
|
|
+
|
|
|
+ # Requested transfer type (RRType.AXFR or RRType.IXFR). The actual
|
|
|
+ # transfer type may differ due to IXFR->AXFR fallback:
|
|
|
+ self._request_type = None
|
|
|
+
|
|
|
+ # Zone parameters
|
|
|
self._zone_name = zone_name
|
|
|
- self._sock_map = sock_map
|
|
|
self._rrclass = rrclass
|
|
|
- self._db_file = db_file
|
|
|
+
|
|
|
+ # Data source handler
|
|
|
+ self._datasrc_client = datasrc_client
|
|
|
+
|
|
|
+ self.create_socket(master_addrinfo[0], master_addrinfo[1])
|
|
|
+ self._sock_map = sock_map
|
|
|
self._soa_rr_count = 0
|
|
|
self._idle_timeout = idle_timeout
|
|
|
self.setblocking(1)
|
|
|
self._shutdown_event = shutdown_event
|
|
|
- self._verbose = verbose
|
|
|
self._master_address = master_addrinfo[2]
|
|
|
self._tsig_key = tsig_key
|
|
|
self._tsig_ctx = None
|
|
@@ -145,6 +484,16 @@ class XfrinConnection(asyncore.dispatcher):
|
|
|
def __create_tsig_ctx(self, key):
|
|
|
return TSIGContext(key)
|
|
|
|
|
|
+ def __set_xfrstate(self, new_state):
|
|
|
+ self.__state = new_state
|
|
|
+
|
|
|
+ def get_xfrstate(self):
|
|
|
+ return self.__state
|
|
|
+
|
|
|
+ def zone_str(self):
|
|
|
+ '''A convenient function for logging to include zone name and class'''
|
|
|
+ return self._zone_name.to_text() + '/' + str(self._rrclass)
|
|
|
+
|
|
|
def connect_to_master(self):
|
|
|
'''Connect to master in TCP.'''
|
|
|
|
|
@@ -155,17 +504,67 @@ class XfrinConnection(asyncore.dispatcher):
|
|
|
logger.error(XFRIN_CONNECT_MASTER, self._master_address, str(e))
|
|
|
return False
|
|
|
|
|
|
+ def _get_zone_soa(self):
|
|
|
+ result, finder = self._datasrc_client.find_zone(self._zone_name)
|
|
|
+ if result != DataSourceClient.SUCCESS:
|
|
|
+ raise XfrinException('Zone not found in the given data ' +
|
|
|
+ 'source: ' + self.zone_str())
|
|
|
+ result, soa_rrset = finder.find(self._zone_name, RRType.SOA(),
|
|
|
+ None, ZoneFinder.FIND_DEFAULT)
|
|
|
+ if result != ZoneFinder.SUCCESS:
|
|
|
+ raise XfrinException('SOA RR not found in zone: ' +
|
|
|
+ self.zone_str())
|
|
|
+ # Especially for database-based zones, a working zone may be in
|
|
|
+ # a broken state where it has more than one SOA RR. We proactively
|
|
|
+ # check the condition and abort the xfr attempt if we identify it.
|
|
|
+ if soa_rrset.get_rdata_count() != 1:
|
|
|
+ raise XfrinException('Invalid number of SOA RRs for ' +
|
|
|
+ self.zone_str() + ': ' +
|
|
|
+ str(soa_rrset.get_rdata_count()))
|
|
|
+ return soa_rrset
|
|
|
+
|
|
|
def _create_query(self, query_type):
|
|
|
- '''Create dns query message. '''
|
|
|
+ '''Create an XFR-related query message.
|
|
|
+
|
|
|
+ query_type is either SOA, AXFR or IXFR. For type IXFR, it searches
|
|
|
+ the associated data source for the current SOA record to include
|
|
|
+ it in the query. If the corresponding zone or the SOA record
|
|
|
+ cannot be found, it raises an XfrinException exception. Note that
|
|
|
+ this may not necessarily a broken configuration; for the first attempt
|
|
|
+ of transfer the secondary may not have any boot-strap zone
|
|
|
+ information, in which case IXFR simply won't work. The xfrin
|
|
|
+ should then fall back to AXFR. _request_serial is recorded for
|
|
|
+ later use.
|
|
|
|
|
|
+ '''
|
|
|
msg = Message(Message.RENDER)
|
|
|
query_id = random.randint(0, 0xFFFF)
|
|
|
self._query_id = query_id
|
|
|
msg.set_qid(query_id)
|
|
|
msg.set_opcode(Opcode.QUERY())
|
|
|
msg.set_rcode(Rcode.NOERROR())
|
|
|
- query_question = Question(Name(self._zone_name), self._rrclass, query_type)
|
|
|
- msg.add_question(query_question)
|
|
|
+ msg.add_question(Question(self._zone_name, self._rrclass, query_type))
|
|
|
+ if query_type == RRType.IXFR():
|
|
|
+ # get the zone finder. this must be SUCCESS (not even
|
|
|
+ # PARTIALMATCH) because we are specifying the zone origin name.
|
|
|
+ zone_soa_rr = self._get_zone_soa()
|
|
|
+ msg.add_rrset(Message.SECTION_AUTHORITY, zone_soa_rr)
|
|
|
+ self._request_serial = get_soa_serial(zone_soa_rr.get_rdata()[0])
|
|
|
+ else:
|
|
|
+ # For AXFR, we temporarily provide backward compatible behavior
|
|
|
+ # where xfrin is responsible for creating zone in the corresponding
|
|
|
+ # DB table. Note that the code below uses the old data source
|
|
|
+ # API and assumes SQLite3 in an ugly manner. We'll have to
|
|
|
+ # develop a better way of managing zones in a generic way and
|
|
|
+ # eliminate the code like the one here.
|
|
|
+ try:
|
|
|
+ self._get_zone_soa()
|
|
|
+ except XfrinException:
|
|
|
+ def empty_rr_generator():
|
|
|
+ return []
|
|
|
+ isc.datasrc.sqlite3_ds.load(self._db_file,
|
|
|
+ self._zone_name.to_text(),
|
|
|
+ empty_rr_generator)
|
|
|
return msg
|
|
|
|
|
|
def _send_data(self, data):
|
|
@@ -256,39 +655,49 @@ class XfrinConnection(asyncore.dispatcher):
|
|
|
# now.
|
|
|
return XFRIN_OK
|
|
|
|
|
|
- def do_xfrin(self, check_soa, ixfr_first = False):
|
|
|
- '''Do xfr by sending xfr request and parsing response. '''
|
|
|
+ def do_xfrin(self, check_soa, request_type=RRType.AXFR()):
|
|
|
+ '''Do an xfr session by sending xfr request and parsing responses.'''
|
|
|
|
|
|
try:
|
|
|
ret = XFRIN_OK
|
|
|
+ self._request_type = request_type
|
|
|
+ # Right now RRType.[IA]XFR().to_text() is 'TYPExxx', so we need
|
|
|
+ # to hardcode here.
|
|
|
+ request_str = 'IXFR' if request_type == RRType.IXFR() else 'AXFR'
|
|
|
if check_soa:
|
|
|
- logstr = 'SOA check for \'%s\' ' % self._zone_name
|
|
|
ret = self._check_soa_serial()
|
|
|
|
|
|
if ret == XFRIN_OK:
|
|
|
- logger.info(XFRIN_AXFR_TRANSFER_STARTED, self._zone_name)
|
|
|
- self._send_query(RRType.AXFR())
|
|
|
- isc.datasrc.sqlite3_ds.load(self._db_file, self._zone_name,
|
|
|
- self._handle_xfrin_response)
|
|
|
-
|
|
|
- logger.info(XFRIN_AXFR_TRANSFER_SUCCESS, self._zone_name)
|
|
|
-
|
|
|
- except XfrinException as e:
|
|
|
- logger.error(XFRIN_AXFR_TRANSFER_FAILURE, self._zone_name, str(e))
|
|
|
- ret = XFRIN_FAIL
|
|
|
- #TODO, recover data source.
|
|
|
- except isc.datasrc.sqlite3_ds.Sqlite3DSError as e:
|
|
|
- logger.error(XFRIN_AXFR_DATABASE_FAILURE, self._zone_name, str(e))
|
|
|
+ logger.info(XFRIN_XFR_TRANSFER_STARTED, request_str,
|
|
|
+ self.zone_str())
|
|
|
+ self._send_query(self._request_type)
|
|
|
+ self.__state = XfrinInitialSOA()
|
|
|
+ self._handle_xfrin_responses()
|
|
|
+ logger.info(XFRIN_XFR_TRANSFER_SUCCESS, request_str,
|
|
|
+ self.zone_str())
|
|
|
+
|
|
|
+ except (XfrinException, XfrinProtocolError) as e:
|
|
|
+ logger.error(XFRIN_XFR_TRANSFER_FAILURE, request_str,
|
|
|
+ self.zone_str(), str(e))
|
|
|
ret = XFRIN_FAIL
|
|
|
- except UserWarning as e:
|
|
|
- # XXX: this is an exception from our C++ library via the
|
|
|
- # Boost.Python binding. It would be better to have more more
|
|
|
- # specific exceptions, but at this moment this is the finest
|
|
|
- # granularity.
|
|
|
- logger.error(XFRIN_AXFR_INTERNAL_FAILURE, self._zone_name, str(e))
|
|
|
+ except Exception as e:
|
|
|
+ # Catching all possible exceptions like this is generally not a
|
|
|
+ # good practice, but handling an xfr session could result in
|
|
|
+ # so many types of exceptions, including ones from the DNS library
|
|
|
+ # or from the data source library. Eventually we'd introduce a
|
|
|
+ # hierarchy for exception classes from a base "ISC exception" and
|
|
|
+ # catch it here, but until then we need broadest coverage so that
|
|
|
+ # we won't miss anything.
|
|
|
+
|
|
|
+ logger.error(XFRIN_XFR_OTHER_FAILURE, request_str,
|
|
|
+ self.zone_str(), str(e))
|
|
|
ret = XFRIN_FAIL
|
|
|
finally:
|
|
|
- self.close()
|
|
|
+ # Make sure any remaining transaction in the diff is closed
|
|
|
+ # (if not yet - possible in case of xfr-level exception) as soon
|
|
|
+ # as possible
|
|
|
+ self._diff = None
|
|
|
+ self.close()
|
|
|
|
|
|
return ret
|
|
|
|
|
@@ -318,9 +727,6 @@ class XfrinConnection(asyncore.dispatcher):
|
|
|
|
|
|
self._check_response_header(msg)
|
|
|
|
|
|
- if msg.get_rr_count(Message.SECTION_ANSWER) == 0:
|
|
|
- raise XfrinException('answer section is empty')
|
|
|
-
|
|
|
if msg.get_rr_count(Message.SECTION_QUESTION) > 1:
|
|
|
raise XfrinException('query section count greater than 1')
|
|
|
|
|
@@ -351,14 +757,14 @@ class XfrinConnection(asyncore.dispatcher):
|
|
|
yield (rrset_name, rrset_ttl, rrset_class, rrset_type,
|
|
|
rdata_text)
|
|
|
|
|
|
- def _handle_xfrin_response(self):
|
|
|
- '''Return a generator for the response to a zone transfer. '''
|
|
|
- while True:
|
|
|
+ def _handle_xfrin_responses(self):
|
|
|
+ read_next_msg = True
|
|
|
+ while read_next_msg:
|
|
|
data_len = self._get_request_response(2)
|
|
|
msg_len = socket.htons(struct.unpack('H', data_len)[0])
|
|
|
recvdata = self._get_request_response(msg_len)
|
|
|
msg = Message(Message.PARSE)
|
|
|
- msg.from_wire(recvdata)
|
|
|
+ msg.from_wire(recvdata, Message.PRESERVE_ORDER)
|
|
|
|
|
|
# TSIG related checks, including an unexpected signed response
|
|
|
self._check_response_tsig(msg, recvdata)
|
|
@@ -366,12 +772,12 @@ class XfrinConnection(asyncore.dispatcher):
|
|
|
# Perform response status validation
|
|
|
self._check_response_status(msg)
|
|
|
|
|
|
- answer_section = msg.get_section(Message.SECTION_ANSWER)
|
|
|
- for rr in self._handle_answer_section(answer_section):
|
|
|
- yield rr
|
|
|
+ for rr in msg.get_section(Message.SECTION_ANSWER):
|
|
|
+ rr_handled = False
|
|
|
+ while not rr_handled:
|
|
|
+ rr_handled = self.__state.handle_rr(self, rr)
|
|
|
|
|
|
- if self._soa_rr_count == 2:
|
|
|
- break
|
|
|
+ read_next_msg = self.__state.finish_message(self)
|
|
|
|
|
|
if self._shutdown_event.is_set():
|
|
|
raise XfrinException('xfrin is forced to stop')
|
|
@@ -393,16 +799,35 @@ class XfrinConnection(asyncore.dispatcher):
|
|
|
pass
|
|
|
|
|
|
def process_xfrin(server, xfrin_recorder, zone_name, rrclass, db_file,
|
|
|
- shutdown_event, master_addrinfo, check_soa, verbose,
|
|
|
- tsig_key):
|
|
|
+ shutdown_event, master_addrinfo, check_soa, tsig_key,
|
|
|
+ request_type):
|
|
|
xfrin_recorder.increment(zone_name)
|
|
|
+
|
|
|
+ # Create a data source client used in this XFR session. Right now we
|
|
|
+ # still assume an sqlite3-based data source, and use both the old and new
|
|
|
+ # data source APIs. We also need to use a mock client for tests.
|
|
|
+ # For a temporary workaround to deal with these situations, we skip the
|
|
|
+ # creation when the given file is none (the test case). Eventually
|
|
|
+ # this code will be much cleaner.
|
|
|
+ datasrc_client = None
|
|
|
+ if db_file is not None:
|
|
|
+ # temporary hardcoded sqlite initialization. Once we decide on
|
|
|
+ # the config specification, we need to update this (TODO)
|
|
|
+ # this may depend on #1207, or any followup ticket created for #1207
|
|
|
+ datasrc_type = "sqlite3"
|
|
|
+ datasrc_config = "{ \"database_file\": \"" + db_file + "\"}"
|
|
|
+ datasrc_client = DataSourceClient(datasrc_type, datasrc_config)
|
|
|
+
|
|
|
+ # Create a TCP connection for the XFR session and perform the operation.
|
|
|
sock_map = {}
|
|
|
- conn = XfrinConnection(sock_map, zone_name, rrclass, db_file,
|
|
|
- shutdown_event, master_addrinfo,
|
|
|
- tsig_key, verbose)
|
|
|
+ conn = XfrinConnection(sock_map, zone_name, rrclass, datasrc_client,
|
|
|
+ shutdown_event, master_addrinfo, tsig_key)
|
|
|
+ # XXX: We still need _db_file for temporary workaround in _create_query().
|
|
|
+ # This should be removed when we eliminate the need for the workaround.
|
|
|
+ conn._db_file = db_file
|
|
|
ret = XFRIN_FAIL
|
|
|
if conn.connect_to_master():
|
|
|
- ret = conn.do_xfrin(check_soa)
|
|
|
+ ret = conn.do_xfrin(check_soa, request_type)
|
|
|
|
|
|
# Publish the zone transfer result news, so zonemgr can reset the
|
|
|
# zone timer, and xfrout can notify the zone's slaves if the result
|
|
@@ -541,13 +966,12 @@ class ZoneInfo:
|
|
|
(str(self.master_addr), self.master_port))
|
|
|
|
|
|
class Xfrin:
|
|
|
- def __init__(self, verbose = False):
|
|
|
+ def __init__(self):
|
|
|
self._max_transfers_in = 10
|
|
|
self._zones = {}
|
|
|
self._cc_setup()
|
|
|
self.recorder = XfrinRecorder()
|
|
|
self._shutdown_event = threading.Event()
|
|
|
- self._verbose = verbose
|
|
|
|
|
|
def _cc_setup(self):
|
|
|
'''This method is used only as part of initialization, but is
|
|
@@ -646,7 +1070,7 @@ class Xfrin:
|
|
|
rrclass,
|
|
|
self._get_db_file(),
|
|
|
master_addr,
|
|
|
- zone_info.tsig_key,
|
|
|
+ zone_info.tsig_key, RRType.AXFR(),
|
|
|
True)
|
|
|
answer = create_answer(ret[0], ret[1])
|
|
|
|
|
@@ -659,14 +1083,17 @@ class Xfrin:
|
|
|
rrclass)
|
|
|
zone_info = self._get_zone_info(zone_name, rrclass)
|
|
|
tsig_key = None
|
|
|
+ request_type = RRType.AXFR()
|
|
|
if zone_info:
|
|
|
tsig_key = zone_info.tsig_key
|
|
|
+ if not zone_info.ixfr_disabled:
|
|
|
+ request_type = RRType.IXFR()
|
|
|
db_file = args.get('db_file') or self._get_db_file()
|
|
|
ret = self.xfrin_start(zone_name,
|
|
|
rrclass,
|
|
|
db_file,
|
|
|
master_addr,
|
|
|
- tsig_key,
|
|
|
+ tsig_key, request_type,
|
|
|
(False if command == 'retransfer' else True))
|
|
|
answer = create_answer(ret[0], ret[1])
|
|
|
|
|
@@ -746,7 +1173,8 @@ class Xfrin:
|
|
|
news(command: zone_new_data_ready) to zone manager and xfrout.
|
|
|
if xfrin failed, just tell the bad news to zone manager, so that
|
|
|
it can reset the refresh timer for that zone. '''
|
|
|
- param = {'zone_name': zone_name, 'zone_class': zone_class.to_text()}
|
|
|
+ param = {'zone_name': zone_name.to_text(),
|
|
|
+ 'zone_class': zone_class.to_text()}
|
|
|
if xfr_result == XFRIN_OK:
|
|
|
msg = create_command(notify_out.ZONE_NEW_DATA_READY_CMD, param)
|
|
|
# catch the exception, in case msgq has been killed.
|
|
@@ -783,8 +1211,8 @@ class Xfrin:
|
|
|
while not self._shutdown_event.is_set():
|
|
|
self._cc_check_command()
|
|
|
|
|
|
- def xfrin_start(self, zone_name, rrclass, db_file, master_addrinfo, tsig_key,
|
|
|
- check_soa = True):
|
|
|
+ def xfrin_start(self, zone_name, rrclass, db_file, master_addrinfo,
|
|
|
+ tsig_key, request_type, check_soa=True):
|
|
|
if "pydnspp" not in sys.modules:
|
|
|
return (1, "xfrin failed, can't load dns message python library: 'pydnspp'")
|
|
|
|
|
@@ -798,13 +1226,12 @@ class Xfrin:
|
|
|
xfrin_thread = threading.Thread(target = process_xfrin,
|
|
|
args = (self,
|
|
|
self.recorder,
|
|
|
- zone_name.to_text(),
|
|
|
+ zone_name,
|
|
|
rrclass,
|
|
|
db_file,
|
|
|
self._shutdown_event,
|
|
|
master_addrinfo, check_soa,
|
|
|
- self._verbose,
|
|
|
- tsig_key))
|
|
|
+ tsig_key, request_type))
|
|
|
|
|
|
xfrin_thread.start()
|
|
|
return (0, 'zone xfrin is started')
|
|
@@ -823,9 +1250,9 @@ def set_signal_handler():
|
|
|
|
|
|
def set_cmd_options(parser):
|
|
|
parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
|
|
|
- help="display more about what is going on")
|
|
|
+ help="This option is obsolete and has no effect.")
|
|
|
|
|
|
-def main(xfrin_class, use_signal = True):
|
|
|
+def main(xfrin_class, use_signal=True):
|
|
|
"""The main loop of the Xfrin daemon.
|
|
|
|
|
|
@param xfrin_class: A class of the Xfrin object. This is normally Xfrin,
|
|
@@ -842,7 +1269,7 @@ def main(xfrin_class, use_signal = True):
|
|
|
|
|
|
if use_signal:
|
|
|
set_signal_handler()
|
|
|
- xfrind = xfrin_class(verbose = options.verbose)
|
|
|
+ xfrind = xfrin_class()
|
|
|
xfrind.startup()
|
|
|
except KeyboardInterrupt:
|
|
|
logger.info(XFRIN_STOPPED_BY_KEYBOARD)
|