Browse Source

[1372] more complete setup for IXFR. falling back to AXFR-style was supported.
also refactored the code to better clarify the code flow.

JINMEI Tatuya 13 years ago
parent
commit
256c0a0848
2 changed files with 126 additions and 64 deletions
  1. 51 16
      src/bin/xfrout/tests/xfrout_test.py.in
  2. 75 48
      src/bin/xfrout/xfrout.py.in

+ 51 - 16
src/bin/xfrout/tests/xfrout_test.py.in

@@ -36,6 +36,8 @@ TSIG_KEY = TSIGKey("example.com:SFuWd/q99SzF8Yzd1QbB9g==")
 TEST_ZONE_NAME_STR = "example.com."
 TEST_ZONE_NAME = Name(TEST_ZONE_NAME_STR)
 TEST_RRCLASS = RRClass.IN()
+IXFR_OK_VERSION = 2011111802
+IXFR_NG_VERSION = 2011112800
 
 # SOA intended to be used for the new SOA as a result of transfer.
 soa_rdata = Rdata(RRType.SOA(), TEST_RRCLASS,
@@ -83,6 +85,15 @@ class MockDataSrcClient:
     def __init__(self, type, config):
         pass
 
+    def __create_soa(self):
+        soa_rrset = RRset(self._zone_name, RRClass.IN(), RRType.SOA(),
+                          RRTTL(3600))
+        soa_rrset.add_rdata(Rdata(RRType.SOA(), RRClass.IN(),
+                                  'master.example.com. ' +
+                                  'admin.example.com. 1234 ' +
+                                  '3600 1800 2419200 7200'))
+        return soa_rrset
+
     def find_zone(self, zone_name):
         '''Mock version of find_zone().
 
@@ -91,9 +102,10 @@ class MockDataSrcClient:
         or PARTIALMATCH.
 
         '''
-        if zone_name == TEST_ZONE_NAME:
-            return (isc.datasrc.DataSourceClient.SUCCESS, self)
-        raise ValueError('Unexpected input to mock client: bug in test case?')
+        self._zone_name = zone_name
+        if zone_name == Name('notauth.example.com'):
+            return (isc.datasrc.DataSourceClient.NOTFOUND, None)
+        return (isc.datasrc.DataSourceClient.SUCCESS, self)
 
     def find(self, name, rrtype, target, options):
         '''Mock ZoneFinder.find().
@@ -104,6 +116,12 @@ class MockDataSrcClient:
 
         '''
         if name == TEST_ZONE_NAME and rrtype == RRType.SOA():
+            return (ZoneFinder.SUCCESS, self.__create_soa())
+        elif name == Name('nosoa.example.com') and rrtype == RRType.SOA():
+            return (ZoneFinder.NXDOMAIN, None)
+        elif name == Name('multisoa.example.com') and rrtype == RRType.SOA():
+            soa_rrset = self.__create_soa()
+            soa_rrset.add_rdata(soa_rrset.get_rdata()[0])
             return (ZoneFinder.SUCCESS, soa_rrset)
         raise ValueError('Unexpected input to mock finder: bug in test case?')
 
@@ -116,20 +134,14 @@ class MockDataSrcClient:
     def get_soa(self):  # emulate ZoneIterator.get_soa()
         if self._zone_name == Name('nosoa.example.com'):
             return None
-        soa_rrset = RRset(self._zone_name, RRClass.IN(), RRType.SOA(),
-                          RRTTL(3600))
-        soa_rrset.add_rdata(Rdata(RRType.SOA(), RRClass.IN(),
-                                  'master.example.com. ' +
-                                  'admin.example.com. 1234 ' +
-                                  '3600 1800 2419200 7200'))
+        soa_rrset = self.__create_soa()
         if self._zone_name == Name('multisoa.example.com'):
-            soa_rrset.add_rdata(Rdata(RRType.SOA(), RRClass.IN(),
-                                      'master.example.com. ' +
-                                      'admin.example.com. 1300 ' +
-                                      '3600 1800 2419200 7200'))
+            soa_rrset.add_rdata(soa_rrset.get_rdata()[0])
         return soa_rrset
 
     def get_journal_reader(self, zone_name, begin_serial, end_serial):
+        if begin_serial == IXFR_NG_VERSION:
+            return isc.datasrc.ZoneJournalReader.NO_SUCH_VERSION, self
         return isc.datasrc.ZoneJournalReader.SUCCESS, self
 
 class MyCCSession(isc.config.ConfigData):
@@ -663,8 +675,12 @@ class TestXfroutSession(TestXfroutSessionBase):
 
     def test_check_xfrout_axfr_available(self):
         self.xfrsess.ClientClass = MockDataSrcClient
+        # Successful case.  A zone iterator should be set up.
         self.assertEqual(self.xfrsess._check_xfrout_available(
                 self.getmsg(), Name('example.com')), Rcode.NOERROR())
+        self.assertNotEqual(None, self.xfrsess._iterator)
+
+        # Failure cases
         self.assertEqual(self.xfrsess._check_xfrout_available(
                 self.getmsg(), Name('notauth.example.com')), Rcode.NOTAUTH())
         self.assertEqual(self.xfrsess._check_xfrout_available(
@@ -675,10 +691,29 @@ class TestXfroutSession(TestXfroutSessionBase):
     def test_check_xfrout_ixfr_available(self):
         self.xfrsess.ClientClass = MockDataSrcClient
         self.set_request_type(RRType.IXFR())
-        self.mdata = self.create_request_data(ixfr=2011111802)
-        request_msg = self.getmsg()
+
+        # Successful case of pure IXFR.  A zone journal reader should be set
+        # up.
+        self.mdata = self.create_request_data(ixfr=IXFR_OK_VERSION)
         self.assertEqual(self.xfrsess._check_xfrout_available(
-                self.getmsg(), Name('example.com')), Rcode.NOERROR())
+                self.getmsg(), TEST_ZONE_NAME), Rcode.NOERROR())
+        self.assertNotEqual(None, self.xfrsess._jnl_reader)
+
+        # Successful case, but as a result of falling back to AXFR-style
+        # IXFR.  A zone iterator should be set up instead of a journal reader.
+        self.mdata = self.create_request_data(ixfr=IXFR_NG_VERSION)
+        self.assertEqual(self.xfrsess._check_xfrout_available(
+                self.getmsg(), TEST_ZONE_NAME), Rcode.NOERROR())
+        self.assertNotEqual(None, self.xfrsess._iterator)
+        self.assertEqual(None, self.xfrsess._jnl_reader)
+
+        # Failure cases
+        self.assertEqual(self.xfrsess._check_xfrout_available(
+                self.getmsg(), Name('notauth.example.com')), Rcode.NOTAUTH())
+        self.assertEqual(self.xfrsess._check_xfrout_available(
+                self.getmsg(), Name('nosoa.example.com')), Rcode.SERVFAIL())
+        self.assertEqual(self.xfrsess._check_xfrout_available(
+                self.getmsg(), Name('multisoa.example.com')), Rcode.SERVFAIL())
 
     def test_dns_xfrout_start_formerror(self):
         # formerror

+ 75 - 48
src/bin/xfrout/xfrout.py.in

@@ -22,7 +22,7 @@ import isc.cc
 import threading
 import struct
 import signal
-from isc.datasrc import DataSourceClient, ZoneFinder
+from isc.datasrc import DataSourceClient, ZoneFinder, ZoneJournalReader
 from socketserver import *
 import os
 from isc.config.ccsession import *
@@ -237,8 +237,8 @@ class XfroutSession():
         elif self._request_type == RRType.IXFR():
             self._request_typestr = 'IXFR'
         else:
-            # Likewise, this should be impossible.
-            raise Runtimeerror('Unexpected XFR type: ' + \
+            # Likewise, this should be impossible.  (TBD: to be tested)
+            raise RuntimeError('Unexpected XFR type: ' + \
                                    str(self._request_type))
 
         # ACL checks
@@ -314,19 +314,82 @@ class XfroutSession():
         self._send_message(sock_fd, msg, self._tsig_ctx)
 
     def _get_zone_soa(self, zone_name):
+        '''Retrieve the SOA RR of the given zone.
+
+        It returns a pair of RCODE and the SOA (in the form of RRset).
+        On success RCODE is NOERROR and returned SOA is not None;
+        on failure RCODE indicates the appropriate code in the context of
+        xfr processing, and the returned SOA is None.
+
+        '''
         result, finder = self._datasrc_client.find_zone(zone_name)
         if result != DataSourceClient.SUCCESS:
-            return None         # XXX
-        result, soa_rrset = finder.find(zone_name, RRType.SOA(),
-                                        None, ZoneFinder.FIND_DEFAULT)
+            return (Rcode.NOTAUTH(), None)
+        result, soa_rrset = finder.find(zone_name, RRType.SOA(), None,
+                                        ZoneFinder.FIND_DEFAULT)
         if result != ZoneFinder.SUCCESS:
-            return None
+            return (Rcode.SERVFAIL(), None)
         # 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:
-            return None
-        return soa_rrset
+            return (Rcode.SERVFAIL(), None)
+        return (Rcode.NOERROR(), soa_rrset)
+
+    def __setup_axfr(self, zone_name):
+        '''Setup a zone iterator for AXFR or AXFR-style IXFR.
+
+        '''
+        try:
+            # Note that we disable 'adjust_ttl'.  In xfr-out we need to
+            # preserve as many things as possible (even if it's half
+            # broken) stored in the zone.
+            self._iterator = self._datasrc_client.get_iterator(zone_name,
+                                                               False)
+        except isc.datasrc.Error:
+            # If the current name server does not have authority for the
+            # zone, xfrout can't serve for it, return rcode NOTAUTH.
+            # Note: this exception can happen for other reasons.  We should
+            # update get_iterator() API so that we can distinguish "no such
+            # zone" and other cases (#1373).  For now we consider all these
+            # cases as NOTAUTH.
+            return Rcode.NOTAUTH()
+
+        # If we are an authoritative name server for the zone, but fail
+        # to find the zone's SOA record in datasource, xfrout can't
+        # provide zone transfer for it.
+        self._soa = self._iterator.get_soa()
+        if self._soa is None or self._soa.get_rdata_count() != 1:
+            return Rcode.SERVFAIL()
+
+        return Rcode.NOERROR()
+
+    def __setup_ixfr(self, request_msg, zone_name):
+        '''Setup a zone journal reader for IXFR.
+
+        If the underlying data source does not know the requested range
+        of zone differences it automatically falls back to AXFR-style
+        IXFR by setting up a zone iterator instead of a journal reader.
+
+        '''
+        # TODO: more error case handling
+        remote_soa = None
+        for auth_rrset in request_msg.get_section(Message.SECTION_AUTHORITY):
+            if auth_rrset.get_type() != RRType.SOA():
+                continue
+            remote_soa = auth_rrset
+        rcode, self._soa = self._get_zone_soa(zone_name)
+        if rcode != Rcode.NOERROR():
+            return rcode
+        code, self._jnl_reader = self._datasrc_client.get_journal_reader(
+            remote_soa.get_name(), get_soa_serial(remote_soa.get_rdata()[0]),
+            get_soa_serial(self._soa.get_rdata()[0]))
+        if code == ZoneJournalReader.NO_SUCH_VERSION:
+            # fallback to AXFR-style IXFR
+            self._jnl_reader = None # clear it just in case
+            return self.__setup_axfr(zone_name)
+
+        return Rcode.NOERROR()
 
     def _check_xfrout_available(self, request_msg, zone_name):
         '''Check if xfr request can be responsed.
@@ -340,51 +403,16 @@ class XfroutSession():
         # We should eventually generalize this so that we can choose the
         # appropriate data source from (possible) multiple candidates.
         # We should eventually take into account the RR class here.
-        # For now, we  hardcode a particular type (SQLite3-based), and only
+        # For now, we hardcode a particular type (SQLite3-based), and only
         # consider that one.
         datasrc_config = '{ "database_file": "' + \
             self._server.get_db_file() + '"}'
         self._datasrc_client = self.ClientClass('sqlite3', datasrc_config)
 
         if self._request_type == RRType.AXFR():
-            try:
-                # Note that we disable 'adjust_ttl'.  In xfr-out we need to
-                # preserve as many things as possible (even if it's half
-                # broken) stored in the zone.
-                self._iterator = self._datasrc_client.get_iterator(zone_name,
-                                                                   False)
-            except isc.datasrc.Error:
-                # If the current name server does not have authority for the
-                # zone, xfrout can't serve for it, return rcode NOTAUTH.
-                # Note: this exception can happen for other reasons.  We should
-                # update get_iterator() API so that we can distinguish "no such
-                # zone" and other cases (#1373).  For now we consider all these
-                # cases as NOTAUTH.
-                return Rcode.NOTAUTH()
-
-            self._soa = self._iterator.get_soa()
+            return self.__setup_axfr(zone_name)
         else:
-            # TODO: error case handling
-            remote_soa = None
-            for auth_rrset in \
-                    request_msg.get_section(Message.SECTION_AUTHORITY):
-                if auth_rrset.get_type() != RRType.SOA():
-                    continue
-                remote_soa = auth_rrset
-            self._soa = self._get_zone_soa(remote_soa.get_name())
-            code, self._jnl_reader = self._datasrc_client.get_journal_reader(
-                remote_soa.get_name(),
-                get_soa_serial(remote_soa.get_rdata()[0]),
-                get_soa_serial(self._soa.get_rdata()[0]))
-
-        # If we are an authoritative name server for the zone, but fail
-        # to find the zone's SOA record in datasource, xfrout can't
-        # provide zone transfer for it.
-        if self._soa is None or self._soa.get_rdata_count() != 1:
-            return Rcode.SERVFAIL()
-
-        return Rcode.NOERROR()
-
+            return self.__setup_ixfr(request_msg, zone_name)
 
     def dns_xfrout_start(self, sock_fd, msg_query, quota_ok=True):
         rcode_, msg = self._parse_query_message(msg_query)
@@ -458,7 +486,6 @@ class XfroutSession():
         msg.add_rrset(Message.SECTION_ANSWER, rrset_soa)
         self._send_message(sock_fd, msg, self._tsig_ctx)
 
-
     def _reply_xfrout_query(self, msg, sock_fd):
         #TODO, there should be a better way to insert rrset.
         msg.make_response()