BIND 10 master, updated. 80c131f5b0763753d199b0fb9b51f10990bcd92b [master] Merge branch 'trac1371'
BIND 10 source code commits
bind10-changes at lists.isc.org
Mon Nov 21 23:20:14 UTC 2011
The branch, master has been updated
via 80c131f5b0763753d199b0fb9b51f10990bcd92b (commit)
via a01eb512f67a14855fc9be9fff561c3c86634e0b (commit)
via 635662711c673bbcfc8fac95c96cfdc33702ca94 (commit)
via 15e23bca2cf7f266d32c6bb30a142a80ee543227 (commit)
via ddb6d109c0947f203eaa6265a22d2fb3b166db0b (commit)
via 2eb9f486619e27aee0684f840c85d152b3ddfe0f (commit)
via 71378c1048bb610c748788dabfd04e421f6b4ac0 (commit)
via de43982b90d0fafd6b4e1857e366a6cd983cfab7 (commit)
via fb33c8d379f9e75b82edafff45d4dc13fda62630 (commit)
via 4f02b45248227dd98904b61bbcd2e6cff36b5fd6 (commit)
via 54d9d7c1597df3bcdf47d07db040f63f7008c6a7 (commit)
via 48c07943ac1dd24922f46cf970c214b5cf24813f (commit)
via bea7b0e3fde35a335bb9e6cf170b0fc240650275 (commit)
via 9b1c64b7d164b6b27d126e55391b2bbafeaf8c00 (commit)
via 96bf3ab5271347542e13b52e2c37b9c8810a6fad (commit)
via c59bb2dcd90a5d580a7f3c9e42a54a080f763add (commit)
via 319bc2d65301606aa938363dcb30a8519755886e (commit)
via d953caeeaf821743ed27ef4a47a45bef66615dc9 (commit)
via 5d382b4295b8455fae844a5ca94886788f6cb19b (commit)
via d08c42ad20f2c91bf64ef47ed893fa2aac4ff037 (commit)
via 08915b387e64f3cf9d9a86a5a21c4492db3a488c (commit)
via 1d4541dfd067cd2f0c9e155049c2b7f9d70fa896 (commit)
via ecf6a71b5845c6710119dd97b500c7edeb3f44c2 (commit)
via a24c6579ab039afd67ecb50a71b9fc8eabf9b6c7 (commit)
via 3647e8ff9c194c1c0a576558f4f49ba4ff2614e7 (commit)
via c3d71baca757b39e13968369e0afb39dd4472eb8 (commit)
via a9040d4aba8e3c01a77236c81f07e2b06b300918 (commit)
via 35556de064c193779c3cd5e5b0fde583f4a8d598 (commit)
via c4f22c20ee19e1ffba43914671c059a434f4518c (commit)
via 12b72af07f5e06cf172b115b0acba3fbe3554467 (commit)
via ecd9c5fc4b3cf747e2b5a221504feac3adeb236e (commit)
via d3db538710b6547cc2e04127fb5fc9d2d5a181f9 (commit)
via 2ab2fd55d4a12d1469060a3657893121114e2e2f (commit)
via 2dd7ee33a13a07a00e22fbc81ecb8b19b57efa8f (commit)
via 5cea4cfbee9770f4299f5a701af89f7cbf977ef4 (commit)
via 1af57091dc0c38cff538de2470275f25caeb2eab (commit)
via 256c0a08483ac2bf396dfa8424b4c02f0681a0f4 (commit)
via 5c92f567d93977bd56a7ed2898c7bee098b552ab (commit)
via 4a68215905542025570f06fcc703fa44d6b37cfd (commit)
via 315f4999df039dbb2baa77ee12afa0dfbe01dc25 (commit)
from ec1cc2b4be6e19519644534889865a3ee2c81a8a (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
commit 80c131f5b0763753d199b0fb9b51f10990bcd92b
Merge: ec1cc2b a01eb51
Author: JINMEI Tatuya <jinmei at isc.org>
Date: Mon Nov 21 15:10:48 2011 -0800
[master] Merge branch 'trac1371'
-----------------------------------------------------------------------
Summary of changes:
doc/guide/bind10-guide.xml | 66 ++++-
src/bin/xfrout/b10-xfrout.xml | 25 ++
src/bin/xfrout/tests/Makefile.am | 4 +-
src/bin/xfrout/tests/testdata/creatediff.py | 58 ++++
src/bin/xfrout/tests/testdata/example.com | 2 +-
src/bin/xfrout/tests/testdata/test.sqlite3 | Bin 11264 -> 12288 bytes
src/bin/xfrout/tests/xfrout_test.py.in | 356 +++++++++++++++++---
src/bin/xfrout/xfrout.py.in | 221 ++++++++++--
src/bin/xfrout/xfrout_messages.mes | 39 +++
src/lib/python/isc/datasrc/client_inc.cc | 2 +
src/lib/python/isc/datasrc/client_python.cc | 36 ++-
src/lib/python/isc/datasrc/tests/Makefile.am | 1 +
src/lib/python/isc/datasrc/tests/datasrc_test.py | 23 +-
.../datasrc/tests/testdata/test.sqlite3.nodiffs | Bin 43008 -> 43008 bytes
src/lib/python/isc/testutils/Makefile.am | 2 +-
src/lib/python/isc/testutils/rrset_utils.py | 63 ++++
16 files changed, 775 insertions(+), 123 deletions(-)
create mode 100755 src/bin/xfrout/tests/testdata/creatediff.py
copy src/lib/{ => python/isc}/datasrc/tests/testdata/test.sqlite3.nodiffs (100%)
create mode 100644 src/lib/python/isc/testutils/rrset_utils.py
-----------------------------------------------------------------------
diff --git a/doc/guide/bind10-guide.xml b/doc/guide/bind10-guide.xml
index 21bb671..711b144 100644
--- a/doc/guide/bind10-guide.xml
+++ b/doc/guide/bind10-guide.xml
@@ -1369,20 +1369,72 @@ what if a NOTIFY is sent?
The <command>b10-xfrout</command> process is started by
<command>bind10</command>.
When the <command>b10-auth</command> authoritative DNS server
- receives an AXFR request, <command>b10-xfrout</command>
- sends the zone.
- This is used to provide master DNS service to share zones
+ receives an AXFR or IXFR request, <command>b10-auth</command>
+ internally forwards the request to <command>b10-xfrout</command>,
+ which handles the rest of request processing.
+ This is used to provide primary DNS service to share zones
to secondary name servers.
The <command>b10-xfrout</command> is also used to send
- NOTIFY messages to slaves.
+ NOTIFY messages to secondary servers.
</para>
+ <para>
+ A global or per zone <option>transfer_acl</option> configuration
+ can be used to control accessibility of the outbound zone
+ transfer service.
+ By default, <command>b10-xfrout</command> allows any clients to
+ perform zone transfers for any zones:
+ </para>
+
+ <screen>> <userinput>config show Xfrout/transfer_acl</userinput>
+Xfrout/transfer_acl[0] {"action": "ACCEPT"} any (default)</screen>
+
+ <para>
+ You can change this to, for example, rejecting all transfer
+ requests by default while allowing requests for the transfer
+ of zone "example.com" from 192.0.2.1 and 2001:db8::1 as follows:
+ </para>
+
+ <screen>> <userinput>config set Xfrout/transfer_acl[0] {"action": "REJECT"}</userinput>
+> <userinput>config add Xfrout/zone_config</userinput>
+> <userinput>config set Xfrout/zone_config[0]/origin "example.com"</userinput>
+> <userinput>config set Xfrout/zone_config[0]/transfer_acl [{"action": "ACCEPT", "from": "192.0.2.1"},</userinput>
+<userinput> {"action": "ACCEPT", "from": "2001:db8::1"}]</userinput>
+> <userinput>config commit</userinput></screen>
+
<note><simpara>
- The current development release of BIND 10 only supports
- AXFR. (IXFR is not supported.)
- Access control is not yet provided.
+ In the above example the lines
+ for <option>transfer_acl</option> were divided for
+ readability. In the actual input it must be in a single line.
</simpara></note>
+ <para>
+ If you want to require TSIG in access control, a separate TSIG
+ "key ring" must be configured specifically
+ for <command>b10-xfrout</command> as well as a system wide
+ key ring, both containing a consistent set of keys.
+ For example, to change the previous example to allowing requests
+ from 192.0.2.1 signed by a TSIG with a key name of
+ "key.example", you'll need to do this:
+ </para>
+
+ <screen>> <userinput>config set tsig_keys/keys ["key.example:<base64-key>"]</userinput>
+> <userinput>config set Xfrout/tsig_keys/keys ["key.example:<base64-key>"]</userinput>
+> <userinput>config set Xfrout/zone_config[0]/transfer_acl [{"action": "ACCEPT", "from": "192.0.2.1", "key": "key.example"}]</userinput>
+> <userinput>config commit</userinput></screen>
+
+ <para>
+ The first line of configuration defines a system wide key ring.
+ This is necessary because the <command>b10-auth</command> server
+ also checks TSIGs and it uses the system wide configuration.
+ </para>
+
+ <note><simpara>
+ In a future version, <command>b10-xfrout</command> will also
+ use the system wide TSIG configuration.
+ The way to specify zone specific configuration (ACLs, etc) is
+ likely to be changed, too.
+ </simpara></note>
<!--
TODO:
diff --git a/src/bin/xfrout/b10-xfrout.xml b/src/bin/xfrout/b10-xfrout.xml
index 9889b80..4f6a7fa 100644
--- a/src/bin/xfrout/b10-xfrout.xml
+++ b/src/bin/xfrout/b10-xfrout.xml
@@ -98,6 +98,31 @@
that can run concurrently. The default is 10.
</para>
<para>
+ <varname>tsig_key_ring</varname>
+ A list of TSIG keys (each of which is in the form of
+ name:base64-key[:algorithm]) used for access control on transfer
+ requests.
+ The default is an empty list.
+ </para>
+ <para>
+ <varname>transfer_acl</varname>
+ A list of ACL elements that apply to all transfer requests by
+ default (unless overridden in zone_config). See the BIND 10
+ guide for configuration examples.
+ The default is an element that allows any transfer requests.
+ </para>
+ <para>
+ <varname>zone_config</varname>
+ A list of JSON objects (i.e. maps) that define per zone
+ configuration concerning <command>b10-xfrout</command>.
+ The supported names of each object are "origin" (the origin
+ name of the zone), "class" (the RR class of the zone, optional,
+ default to "IN"), and "acl_element" (ACL only applicable to
+ transfer requests for that zone).
+ See the BIND 10 guide for configuration examples.
+ The default is an empty list, that is, no zone specific configuration.
+ </para>
+ <para>
<varname>log_name</varname>
<!-- TODO -->
</para>
diff --git a/src/bin/xfrout/tests/Makefile.am b/src/bin/xfrout/tests/Makefile.am
index 509df79..ad6d7e6 100644
--- a/src/bin/xfrout/tests/Makefile.am
+++ b/src/bin/xfrout/tests/Makefile.am
@@ -3,8 +3,8 @@ PYTESTS = xfrout_test.py
noinst_SCRIPTS = $(PYTESTS)
EXTRA_DIST = testdata/test.sqlite3
-# This one is actually not necessary, but added for reference
-EXTRA_DIST += testdata/example.com
+# These are actually not necessary, but added for reference
+EXTRA_DIST += testdata/example.com testdata/creatediff.py
# If necessary (rare cases), explicitly specify paths to dynamic libraries
# required by loadable python modules.
diff --git a/src/bin/xfrout/tests/testdata/creatediff.py b/src/bin/xfrout/tests/testdata/creatediff.py
new file mode 100755
index 0000000..dab6622
--- /dev/null
+++ b/src/bin/xfrout/tests/testdata/creatediff.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3.1
+
+# Copyright (C) 2011 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.
+
+'''This script was used to create zone differences for IXFR tests.
+
+The result was stored in the test SQLite3 database file, so this script
+itself isn't necessary for testing. It's provided here for reference
+purposes.
+
+'''
+
+import isc.datasrc
+import isc.log
+from isc.dns import *
+from isc.testutils.rrset_utils import *
+
+isc.log.init("dummy") # XXX
+
+ZONE_NAME = Name('example.com')
+NS_NAME_STR = 'a.dns.example.com'
+NS_NAME = Name(NS_NAME_STR)
+
+client = isc.datasrc.DataSourceClient('sqlite3',
+ '{ "database_file": "test.sqlite3" }')
+
+# Install the initial data
+updater = client.get_updater(ZONE_NAME, True)
+updater.add_rrset(create_soa(2011111802))
+updater.add_rrset(create_ns(NS_NAME_STR))
+updater.add_rrset(create_a(NS_NAME, '192.0.2.53'))
+updater.add_rrset(create_aaaa(NS_NAME, '2001:db8::1'))
+updater.commit()
+
+# Incremental update to generate diffs
+updater = client.get_updater(ZONE_NAME, False, True)
+updater.delete_rrset(create_soa(2011111802))
+updater.add_rrset(create_soa(2011111900))
+updater.add_rrset(create_a(NS_NAME, '192.0.2.2', 7200))
+updater.delete_rrset(create_soa(2011111900))
+updater.delete_rrset(create_a(NS_NAME, '192.0.2.53'))
+updater.delete_rrset(create_aaaa(NS_NAME, '2001:db8::1'))
+updater.add_rrset(create_soa(2011112001))
+updater.add_rrset(create_a(NS_NAME, '192.0.2.1'))
+updater.commit()
diff --git a/src/bin/xfrout/tests/testdata/example.com b/src/bin/xfrout/tests/testdata/example.com
index 25c5e6a..8458d09 100644
--- a/src/bin/xfrout/tests/testdata/example.com
+++ b/src/bin/xfrout/tests/testdata/example.com
@@ -1,6 +1,6 @@
;; This is the source of a zone stored in test.sqlite3. It's provided
;; for reference purposes only.
-example.com. 3600 IN SOA a.dns.example.com. mail.example.com. 1 1 1 1 1
+example.com. 3600 IN SOA master.example.com. admin.example.com. 2011112001 3600 1800 2419200 7200
example.com. 3600 IN NS a.dns.example.com.
a.dns.example.com. 3600 IN A 192.0.2.1
a.dns.example.com. 7200 IN A 192.0.2.2
diff --git a/src/bin/xfrout/tests/testdata/test.sqlite3 b/src/bin/xfrout/tests/testdata/test.sqlite3
index af491f5..9eb14f1 100644
Binary files a/src/bin/xfrout/tests/testdata/test.sqlite3 and b/src/bin/xfrout/tests/testdata/test.sqlite3 differ
diff --git a/src/bin/xfrout/tests/xfrout_test.py.in b/src/bin/xfrout/tests/xfrout_test.py.in
index 4aca929..7e0af42 100644
--- a/src/bin/xfrout/tests/xfrout_test.py.in
+++ b/src/bin/xfrout/tests/xfrout_test.py.in
@@ -22,6 +22,7 @@ from isc.testutils.tsigctx_mock import MockTSIGContext
from isc.cc.session import *
import isc.config
from isc.dns import *
+from isc.testutils.rrset_utils import *
from xfrout import *
import xfrout
import isc.log
@@ -30,6 +31,16 @@ import isc.acl.dns
TESTDATA_SRCDIR = os.getenv("TESTDATASRCDIR")
TSIG_KEY = TSIGKey("example.com:SFuWd/q99SzF8Yzd1QbB9g==")
+#
+# Commonly used (mostly constant) test parameters
+#
+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_CURRENT_VERSION = 2011112001
+
# our fake socket, where we can read and insert messages
class MySocket():
def __init__(self, family, type):
@@ -69,6 +80,38 @@ class MockDataSrcClient:
def __init__(self, type, config):
pass
+ def find_zone(self, zone_name):
+ '''Mock version of find_zone().
+
+ It returns itself (subsequently acting as a mock ZoneFinder) for
+ some test zone names. For a special name it returns NOTFOUND to
+ emulate the condition where the specified zone doen't exist.
+
+ '''
+ 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().
+
+ (At the moment) this method only handles query for type SOA.
+ By default it returns a normal SOA RR(set) whose owner name is
+ the query name It also emulates some unusual cases for special
+ zone names.
+
+ '''
+ if 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 = create_soa(SOA_CURRENT_VERSION)
+ soa_rrset.add_rdata(soa_rrset.get_rdata()[0])
+ return (ZoneFinder.SUCCESS, soa_rrset)
+ elif rrtype == RRType.SOA():
+ return (ZoneFinder.SUCCESS, create_soa(SOA_CURRENT_VERSION))
+ raise ValueError('Unexpected input to mock finder: bug in test case?')
+
def get_iterator(self, zone_name, adjust_ttl=False):
if zone_name == Name('notauth.example.com'):
raise isc.datasrc.Error('no such zone')
@@ -78,19 +121,20 @@ 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 = create_soa(SOA_CURRENT_VERSION)
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 zone_name == Name('notauth2.example.com'):
+ return isc.datasrc.ZoneJournalReader.NO_SUCH_ZONE, None
+ if zone_name == Name('nojournal.example.com'):
+ raise isc.datasrc.NotImplemented('journaling not supported')
+ if begin_serial == IXFR_NG_VERSION:
+ return isc.datasrc.ZoneJournalReader.NO_SUCH_VERSION, None
+ return isc.datasrc.ZoneJournalReader.SUCCESS, self
+
class MyCCSession(isc.config.ConfigData):
def __init__(self):
module_spec = isc.config.module_spec_from_file(
@@ -159,15 +203,44 @@ class TestXfroutSessionBase(unittest.TestCase):
def message_has_tsig(self, msg):
return msg.get_tsig_record() is not None
- def create_request_data(self, with_question=True, with_tsig=False):
+ def create_request_data(self, with_question=True, with_tsig=False,
+ ixfr=None, qtype=None, zone_name=TEST_ZONE_NAME,
+ soa_class=TEST_RRCLASS, num_soa=1):
+ '''Create a commonly used XFR request data.
+
+ By default the request type is AXFR; if 'ixfr' is an integer,
+ the request type will be IXFR and an SOA with the serial being
+ the value of the parameter will be included in the authority
+ section.
+
+ This method has various minor parameters only for creating bad
+ format requests for testing purposes:
+ qtype: the RR type of the question section. By default automatically
+ determined by the value of ixfr, but could be an invalid type
+ for testing.
+ zone_name: the query (zone) name. for IXFR, it's also used as
+ the owner name of the SOA in the authority section.
+ soa_class: IXFR only. The RR class of the SOA RR in the authority
+ section.
+ num_soa: IXFR only. The number of SOA RDATAs in the authority
+ section.
+ '''
msg = Message(Message.RENDER)
query_id = 0x1035
msg.set_qid(query_id)
msg.set_opcode(Opcode.QUERY())
msg.set_rcode(Rcode.NOERROR())
+ req_type = RRType.AXFR() if ixfr is None else RRType.IXFR()
if with_question:
- msg.add_question(Question(Name("example.com"), RRClass.IN(),
- RRType.AXFR()))
+ msg.add_question(Question(zone_name, RRClass.IN(),
+ req_type if qtype is None else qtype))
+ if req_type == RRType.IXFR():
+ soa = RRset(zone_name, soa_class, RRType.SOA(), RRTTL(0))
+ # In the RDATA only the serial matters.
+ for i in range(0, num_soa):
+ soa.add_rdata(Rdata(RRType.SOA(), soa_class,
+ 'm r ' + str(ixfr) + ' 1 1 1 1'))
+ msg.add_rrset(Message.SECTION_AUTHORITY, soa)
renderer = MessageRenderer()
if with_tsig:
@@ -178,6 +251,13 @@ class TestXfroutSessionBase(unittest.TestCase):
request_data = renderer.get_data()
return request_data
+ def set_request_type(self, type):
+ self.xfrsess._request_type = type
+ if type == RRType.AXFR():
+ self.xfrsess._request_typestr = 'AXFR'
+ else:
+ self.xfrsess._request_typestr = 'IXFR'
+
def setUp(self):
self.sock = MySocket(socket.AF_INET,socket.SOCK_STREAM)
self.xfrsess = MyXfroutSession(self.sock, None, Dbserver(),
@@ -188,13 +268,9 @@ class TestXfroutSessionBase(unittest.TestCase):
isc.acl.dns.REQUEST_LOADER.load(
[{"action": "ACCEPT"}]),
{})
+ self.set_request_type(RRType.AXFR()) # test AXFR by default
self.mdata = self.create_request_data()
- self.soa_rrset = RRset(Name('example.com'), RRClass.IN(), RRType.SOA(),
- RRTTL(3600))
- self.soa_rrset.add_rdata(Rdata(RRType.SOA(), RRClass.IN(),
- 'master.Example.com. ' +
- 'admin.exAmple.com. ' +
- '1234 3600 1800 2419200 7200'))
+ self.soa_rrset = create_soa(SOA_CURRENT_VERSION)
# some test replaces a module-wide function. We should ensure the
# original is used elsewhere.
self.orig_get_rrset_len = xfrout.get_rrset_len
@@ -222,7 +298,7 @@ class TestXfroutSession(TestXfroutSessionBase):
# set up a bogus request, which should result in FORMERR. (it only
# has to be something that is different from the previous case)
self.xfrsess._request_data = \
- self.create_request_data(with_question=False)
+ self.create_request_data(ixfr=IXFR_OK_VERSION, num_soa=2)
# Replace the data source client to avoid datasrc related exceptions
self.xfrsess.ClientClass = MockDataSrcClient
XfroutSession._handle(self.xfrsess)
@@ -241,13 +317,24 @@ class TestXfroutSession(TestXfroutSessionBase):
XfroutSession._handle(self.xfrsess)
def test_parse_query_message(self):
+ # Valid AXFR
[get_rcode, get_msg] = self.xfrsess._parse_query_message(self.mdata)
+ self.assertEqual(RRType.AXFR(), self.xfrsess._request_type)
self.assertEqual(get_rcode.to_text(), "NOERROR")
- # Broken request: no question
- request_data = self.create_request_data(with_question=False)
+ # Valid IXFR
+ request_data = self.create_request_data(ixfr=2011111801)
rcode, msg = self.xfrsess._parse_query_message(request_data)
- self.assertEqual(Rcode.FORMERR(), rcode)
+ self.assertEqual(RRType.IXFR(), self.xfrsess._request_type)
+ self.assertEqual(Rcode.NOERROR(), rcode)
+
+ # Broken request: no question
+ self.assertRaises(RuntimeError, self.xfrsess._parse_query_message,
+ self.create_request_data(with_question=False))
+
+ # Broken request: invalid RR type (neither AXFR nor IXFR)
+ self.assertRaises(RuntimeError, self.xfrsess._parse_query_message,
+ self.create_request_data(qtype=RRType.A()))
# tsig signed query message
request_data = self.create_request_data(with_tsig=True)
@@ -436,7 +523,7 @@ class TestXfroutSession(TestXfroutSessionBase):
RRTTL(3600))
soa_rrset.add_rdata(Rdata(RRType.SOA(), RRClass.IN(),
'master.Example.com. admin.exAmple.com. ' +
- '1234 3600 1800 2419200 7200'))
+ '2011112001 3600 1800 2419200 7200'))
msg.add_rrset(Message.SECTION_ANSWER, soa_rrset)
self.xfrsess._send_message(self.sock, msg)
send_out_data = self.sock.readsent()[2:]
@@ -587,16 +674,101 @@ class TestXfroutSession(TestXfroutSessionBase):
def test_get_rrset_len(self):
self.assertEqual(82, get_rrset_len(self.soa_rrset))
- def test_check_xfrout_available(self):
+ def test_xfrout_axfr_setup(self):
self.xfrsess.ClientClass = MockDataSrcClient
- self.assertEqual(self.xfrsess._check_xfrout_available(
- Name('example.com')), Rcode.NOERROR())
- self.assertEqual(self.xfrsess._check_xfrout_available(
- Name('notauth.example.com')), Rcode.NOTAUTH())
- self.assertEqual(self.xfrsess._check_xfrout_available(
- Name('nosoa.example.com')), Rcode.SERVFAIL())
- self.assertEqual(self.xfrsess._check_xfrout_available(
- Name('multisoa.example.com')), Rcode.SERVFAIL())
+ # Successful case. A zone iterator should be set up.
+ self.assertEqual(self.xfrsess._xfrout_setup(
+ self.getmsg(), TEST_ZONE_NAME, TEST_RRCLASS), Rcode.NOERROR())
+ self.assertNotEqual(None, self.xfrsess._iterator)
+
+ # Failure cases
+ self.assertEqual(self.xfrsess._xfrout_setup(
+ self.getmsg(), Name('notauth.example.com'), TEST_RRCLASS),
+ Rcode.NOTAUTH())
+ self.assertEqual(self.xfrsess._xfrout_setup(
+ self.getmsg(), Name('nosoa.example.com'), TEST_RRCLASS),
+ Rcode.SERVFAIL())
+ self.assertEqual(self.xfrsess._xfrout_setup(
+ self.getmsg(), Name('multisoa.example.com'), TEST_RRCLASS),
+ Rcode.SERVFAIL())
+
+ def test_xfrout_ixfr_setup(self):
+ self.xfrsess.ClientClass = MockDataSrcClient
+ self.set_request_type(RRType.IXFR())
+
+ # 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._xfrout_setup(
+ self.getmsg(), TEST_ZONE_NAME, TEST_RRCLASS), 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._xfrout_setup(
+ self.getmsg(), TEST_ZONE_NAME, TEST_RRCLASS), Rcode.NOERROR())
+ self.assertNotEqual(None, self.xfrsess._iterator)
+ self.assertEqual(None, self.xfrsess._jnl_reader)
+
+ # Successful case, but the requested SOA serial is equal to that of
+ # the local SOA. Both iterator and jnl_reader should be None,
+ # indicating that the response will contain just one SOA.
+ self.mdata = self.create_request_data(ixfr=SOA_CURRENT_VERSION)
+ self.assertEqual(self.xfrsess._xfrout_setup(
+ self.getmsg(), TEST_ZONE_NAME, TEST_RRCLASS), Rcode.NOERROR())
+ self.assertEqual(None, self.xfrsess._iterator)
+ self.assertEqual(None, self.xfrsess._jnl_reader)
+
+ # The data source doesn't support journaling. Should fallback to AXFR.
+ zone_name = Name('nojournal.example.com')
+ self.mdata = self.create_request_data(ixfr=IXFR_OK_VERSION,
+ zone_name=zone_name)
+ self.assertEqual(self.xfrsess._xfrout_setup(
+ self.getmsg(), zone_name, TEST_RRCLASS), Rcode.NOERROR())
+ self.assertNotEqual(None, self.xfrsess._iterator)
+
+ # Failure cases
+ zone_name = Name('notauth.example.com')
+ self.mdata = self.create_request_data(ixfr=IXFR_OK_VERSION,
+ zone_name=zone_name)
+ self.assertEqual(self.xfrsess._xfrout_setup(
+ self.getmsg(), zone_name, TEST_RRCLASS), Rcode.NOTAUTH())
+ # this is a strange case: zone's SOA will be found but the journal
+ # reader won't be created due to 'no such zone'.
+ zone_name = Name('notauth2.example.com')
+ self.mdata = self.create_request_data(ixfr=IXFR_OK_VERSION,
+ zone_name=zone_name)
+ self.assertEqual(self.xfrsess._xfrout_setup(
+ self.getmsg(), zone_name, TEST_RRCLASS), Rcode.NOTAUTH())
+ zone_name = Name('nosoa.example.com')
+ self.mdata = self.create_request_data(ixfr=IXFR_OK_VERSION,
+ zone_name=zone_name)
+ self.assertEqual(self.xfrsess._xfrout_setup(
+ self.getmsg(), zone_name, TEST_RRCLASS), Rcode.SERVFAIL())
+ zone_name = Name('multisoa.example.com')
+ self.mdata = self.create_request_data(ixfr=IXFR_OK_VERSION,
+ zone_name=zone_name)
+ self.assertEqual(self.xfrsess._xfrout_setup(
+ self.getmsg(), zone_name, TEST_RRCLASS), Rcode.SERVFAIL())
+
+ # query name doesn't match the SOA's owner
+ self.mdata = self.create_request_data(ixfr=IXFR_OK_VERSION)
+ self.assertEqual(self.xfrsess._xfrout_setup(
+ self.getmsg(), zone_name, TEST_RRCLASS), Rcode.FORMERR())
+
+ # query's RR class doesn't match the SOA's class
+ zone_name = TEST_ZONE_NAME # make sure the name matches this time
+ self.mdata = self.create_request_data(ixfr=IXFR_OK_VERSION,
+ soa_class=RRClass.CH())
+ self.assertEqual(self.xfrsess._xfrout_setup(
+ self.getmsg(), zone_name, TEST_RRCLASS), Rcode.FORMERR())
+
+ # multiple SOA RRs
+ self.mdata = self.create_request_data(ixfr=IXFR_OK_VERSION,
+ num_soa=2)
+ self.assertEqual(self.xfrsess._xfrout_setup(
+ self.getmsg(), zone_name, TEST_RRCLASS), Rcode.FORMERR())
def test_dns_xfrout_start_formerror(self):
# formerror
@@ -604,13 +776,10 @@ class TestXfroutSession(TestXfroutSessionBase):
sent_data = self.sock.readsent()
self.assertEqual(len(sent_data), 0)
- def default(self, param):
- return "example.com"
-
def test_dns_xfrout_start_notauth(self):
- def notauth(formpara):
+ def notauth(msg, name, rrclass):
return Rcode.NOTAUTH()
- self.xfrsess._check_xfrout_available = notauth
+ self.xfrsess._xfrout_setup = notauth
self.xfrsess.dns_xfrout_start(self.sock, self.mdata)
get_msg = self.sock.read_msg()
self.assertEqual(get_msg.get_rcode().to_text(), "NOTAUTH")
@@ -623,9 +792,9 @@ class TestXfroutSession(TestXfroutSessionBase):
self.assertEqual(self.sock.read_msg().get_rcode(), Rcode.SERVFAIL())
def test_dns_xfrout_start_noerror(self):
- def noerror(form):
+ def noerror(msg, name, rrclass):
return Rcode.NOERROR()
- self.xfrsess._check_xfrout_available = noerror
+ self.xfrsess._xfrout_setup = noerror
def myreply(msg, sock):
self.sock.send(b"success")
@@ -634,14 +803,14 @@ class TestXfroutSession(TestXfroutSessionBase):
self.xfrsess.dns_xfrout_start(self.sock, self.mdata)
self.assertEqual(self.sock.readsent(), b"success")
- def test_reply_xfrout_query_noerror(self):
+ def test_reply_xfrout_query_axfr(self):
self.xfrsess._soa = self.soa_rrset
self.xfrsess._iterator = [self.soa_rrset]
self.xfrsess._reply_xfrout_query(self.getmsg(), self.sock)
reply_msg = self.sock.read_msg()
self.assertEqual(reply_msg.get_rr_count(Message.SECTION_ANSWER), 2)
- def test_reply_xfrout_query_noerror_with_tsig(self):
+ def test_reply_xfrout_query_axfr_with_tsig(self):
rrset = RRset(Name('a.example.com'), RRClass.IN(), RRType.A(),
RRTTL(3600))
rrset.add_rdata(Rdata(RRType.A(), RRClass.IN(), '192.0.2.1'))
@@ -669,6 +838,40 @@ class TestXfroutSession(TestXfroutSessionBase):
# and it should not have sent anything else
self.assertEqual(0, len(self.sock.sendqueue))
+ def test_reply_xfrout_query_ixfr(self):
+ # Creating a pure (incremental) IXFR response. Intermediate SOA
+ # RRs won't be skipped.
+ self.xfrsess._soa = create_soa(SOA_CURRENT_VERSION)
+ self.xfrsess._iterator = [create_soa(IXFR_OK_VERSION),
+ create_a(Name('a.example.com'), '192.0.2.2'),
+ create_soa(SOA_CURRENT_VERSION),
+ create_aaaa(Name('a.example.com'),
+ '2001:db8::1')]
+ self.xfrsess._jnl_reader = self.xfrsess._iterator
+ self.xfrsess._reply_xfrout_query(self.getmsg(), self.sock)
+ reply_msg = self.sock.read_msg(Message.PRESERVE_ORDER)
+ actual_records = reply_msg.get_section(Message.SECTION_ANSWER)
+
+ expected_records = self.xfrsess._iterator[:]
+ expected_records.insert(0, create_soa(SOA_CURRENT_VERSION))
+ expected_records.append(create_soa(SOA_CURRENT_VERSION))
+
+ self.assertEqual(len(expected_records), len(actual_records))
+ for (expected_rr, actual_rr) in zip(expected_records, actual_records):
+ self.assertTrue(expected_rr, actual_rr)
+
+ def test_reply_xfrout_query_ixfr_soa_only(self):
+ # Creating an IXFR response that contains only one RR, which is the
+ # SOA of the current version.
+ self.xfrsess._soa = create_soa(SOA_CURRENT_VERSION)
+ self.xfrsess._iterator = None
+ self.xfrsess._jnl_reader = None
+ self.xfrsess._reply_xfrout_query(self.getmsg(), self.sock)
+ reply_msg = self.sock.read_msg(Message.PRESERVE_ORDER)
+ answer = reply_msg.get_section(Message.SECTION_ANSWER)
+ self.assertEqual(1, len(answer))
+ self.assertTrue(create_soa(SOA_CURRENT_VERSION), answer[0])
+
class TestXfroutSessionWithSQLite3(TestXfroutSessionBase):
'''Tests for XFR-out sessions using an SQLite3 DB.
@@ -683,19 +886,72 @@ class TestXfroutSessionWithSQLite3(TestXfroutSessionBase):
self.xfrsess._request_data = self.mdata
self.xfrsess._server.get_db_file = lambda : TESTDATA_SRCDIR + \
'test.sqlite3'
+ self.ns_name = 'a.dns.example.com'
+
+ def check_axfr_stream(self, response):
+ '''Common checks for AXFR(-style) response for the test zone.
+ '''
+ # This zone contains two A RRs for the same name with different TTLs.
+ # These TTLs should be preseved in the AXFR stream.
+ actual_records = response.get_section(Message.SECTION_ANSWER)
+ expected_records = [create_soa(2011112001),
+ create_ns(self.ns_name),
+ create_a(Name(self.ns_name), '192.0.2.1', 3600),
+ create_a(Name(self.ns_name), '192.0.2.2', 7200),
+ create_soa(2011112001)]
+ self.assertEqual(len(expected_records), len(actual_records))
+ for (expected_rr, actual_rr) in zip(expected_records, actual_records):
+ self.assertTrue(expected_rr, actual_rr)
def test_axfr_normal_session(self):
XfroutSession._handle(self.xfrsess)
response = self.sock.read_msg(Message.PRESERVE_ORDER);
self.assertEqual(Rcode.NOERROR(), response.get_rcode())
- # This zone contains two A RRs for the same name with different TTLs.
- # These TTLs should be preseved in the AXFR stream.
- actual_ttls = []
- for rr in response.get_section(Message.SECTION_ANSWER):
- if rr.get_type() == RRType.A() and \
- not rr.get_ttl() in actual_ttls:
- actual_ttls.append(rr.get_ttl().get_value())
- self.assertEqual([3600, 7200], sorted(actual_ttls))
+ self.check_axfr_stream(response)
+
+ def test_ixfr_to_axfr(self):
+ self.xfrsess._request_data = \
+ self.create_request_data(ixfr=IXFR_NG_VERSION)
+ XfroutSession._handle(self.xfrsess)
+ response = self.sock.read_msg(Message.PRESERVE_ORDER);
+ self.assertEqual(Rcode.NOERROR(), response.get_rcode())
+ # This is an AXFR-style IXFR. So the question section should indicate
+ # that it's an IXFR resposne.
+ self.assertEqual(RRType.IXFR(), response.get_question()[0].get_type())
+ self.check_axfr_stream(response)
+
+ def test_ixfr_normal_session(self):
+ # See testdata/creatediff.py. There are 8 changes between two
+ # versions. So the answer section should contain all of these and
+ # two beginning and trailing SOAs.
+ self.xfrsess._request_data = \
+ self.create_request_data(ixfr=IXFR_OK_VERSION)
+ XfroutSession._handle(self.xfrsess)
+ response = self.sock.read_msg(Message.PRESERVE_ORDER);
+ actual_records = response.get_section(Message.SECTION_ANSWER)
+ expected_records = [create_soa(2011112001), create_soa(2011111802),
+ create_soa(2011111900),
+ create_a(Name(self.ns_name), '192.0.2.2', 7200),
+ create_soa(2011111900),
+ create_a(Name(self.ns_name), '192.0.2.53'),
+ create_aaaa(Name(self.ns_name), '2001:db8::1'),
+ create_soa(2011112001),
+ create_a(Name(self.ns_name), '192.0.2.1'),
+ create_soa(2011112001)]
+ self.assertEqual(len(expected_records), len(actual_records))
+ for (expected_rr, actual_rr) in zip(expected_records, actual_records):
+ self.assertTrue(expected_rr, actual_rr)
+
+ def test_ixfr_soa_only(self):
+ # The requested SOA serial is the latest one. The response should
+ # contain exactly one SOA of that serial.
+ self.xfrsess._request_data = \
+ self.create_request_data(ixfr=SOA_CURRENT_VERSION)
+ XfroutSession._handle(self.xfrsess)
+ response = self.sock.read_msg(Message.PRESERVE_ORDER);
+ answers = response.get_section(Message.SECTION_ANSWER)
+ self.assertEqual(1, len(answers))
+ self.assertTrue(create_soa(SOA_CURRENT_VERSION), answers[0])
class MyUnixSockServer(UnixSockServer):
def __init__(self):
diff --git a/src/bin/xfrout/xfrout.py.in b/src/bin/xfrout/xfrout.py.in
index 912a6b3..8990c13 100755
--- a/src/bin/xfrout/xfrout.py.in
+++ b/src/bin/xfrout/xfrout.py.in
@@ -22,7 +22,7 @@ import isc.cc
import threading
import struct
import signal
-from isc.datasrc import DataSourceClient
+from isc.datasrc import DataSourceClient, ZoneFinder, ZoneJournalReader
from socketserver import *
import os
from isc.config.ccsession import *
@@ -102,7 +102,7 @@ def format_zone_str(zone_name, zone_class):
zone_name (isc.dns.Name) name to format
zone_class (isc.dns.RRClass) class to format
"""
- return zone_name.to_text() + '/' + str(zone_class)
+ return zone_name.to_text(True) + '/' + str(zone_class)
# borrowed from xfrin.py @ #1298.
def format_addrinfo(addrinfo):
@@ -132,6 +132,11 @@ def get_rrset_len(rrset):
rrset.to_wire(bytes)
return len(bytes)
+def get_soa_serial(soa_rdata):
+ '''Extract the serial field of an SOA RDATA and returns it as an intger.
+ (borrowed from xfrin)
+ '''
+ return int(soa_rdata.to_text().split()[2])
class XfroutSession():
def __init__(self, sock_fd, request_data, server, tsig_key_ring, remote,
@@ -143,11 +148,13 @@ class XfroutSession():
self._tsig_ctx = None
self._tsig_len = 0
self._remote = remote
- self._request_type = 'AXFR' # could be IXFR when we support it
+ self._request_type = None
+ self._request_typestr = None
self._acl = default_acl
self._zone_config = zone_config
self.ClientClass = client_class # parameterize this for testing
- self._soa = None # will be set in _check_xfrout_available or in tests
+ self._soa = None # will be set in _xfrout_setup or in tests
+ self._jnl_reader = None # will be set to a reader for IXFR
self._handle()
def create_tsig_ctx(self, tsig_record, tsig_key_ring):
@@ -195,7 +202,8 @@ class XfroutSession():
tsig_record = msg.get_tsig_record()
if tsig_record is not None:
self._tsig_len = tsig_record.get_length()
- self._tsig_ctx = self.create_tsig_ctx(tsig_record, self._tsig_key_ring)
+ self._tsig_ctx = self.create_tsig_ctx(tsig_record,
+ self._tsig_key_ring)
tsig_error = self._tsig_ctx.verify(tsig_record, request_data)
if tsig_error != TSIGError.NOERROR:
return Rcode.NOTAUTH()
@@ -218,24 +226,38 @@ class XfroutSession():
return rcode, msg
# Make sure the question is valid. This should be ensured by
- # the auth server, but since it's far from our xfrout itself,
- # we check it by ourselves.
+ # the auth server, but since it's far from xfrout itself, we check
+ # it by ourselves. A viloation would be an internal bug, so we
+ # raise and stop here rather than returning a FORMERR or SERVFAIL.
if msg.get_rr_count(Message.SECTION_QUESTION) != 1:
- return Rcode.FORMERR(), msg
+ raise RuntimeError('Invalid number of question for XFR: ' +
+ str(msg.get_rr_count(Message.SECTION_QUESTION)))
+ question = msg.get_question()[0]
+
+ # Identify the request type
+ self._request_type = question.get_type()
+ if self._request_type == RRType.AXFR():
+ self._request_typestr = 'AXFR'
+ elif self._request_type == RRType.IXFR():
+ self._request_typestr = 'IXFR'
+ else:
+ # Likewise, this should be impossible.
+ raise RuntimeError('Unexpected XFR type: ' +
+ str(self._request_type))
# ACL checks
- zone_name = msg.get_question()[0].get_name()
- zone_class = msg.get_question()[0].get_class()
+ zone_name = question.get_name()
+ zone_class = question.get_class()
acl = self._get_transfer_acl(zone_name, zone_class)
acl_result = acl.execute(
isc.acl.dns.RequestContext(self._remote[2], msg.get_tsig_record()))
if acl_result == DROP:
- logger.info(XFROUT_QUERY_DROPPED, self._request_type,
+ logger.info(XFROUT_QUERY_DROPPED, self._request_typestr,
format_addrinfo(self._remote),
format_zone_str(zone_name, zone_class))
return None, None
elif acl_result == REJECT:
- logger.info(XFROUT_QUERY_REJECTED, self._request_type,
+ logger.info(XFROUT_QUERY_REJECTED, self._request_typestr,
format_addrinfo(self._remote),
format_zone_str(zone_name, zone_class))
return Rcode.REFUSED(), msg
@@ -295,23 +317,33 @@ class XfroutSession():
msg.set_rcode(rcode_)
self._send_message(sock_fd, msg, self._tsig_ctx)
- def _check_xfrout_available(self, zone_name):
- '''Check if xfr request can be responsed.
- TODO, Get zone's configuration from cfgmgr or some other place
- eg. check allow_transfer setting,
+ 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 (Rcode.NOTAUTH(), None)
+ result, soa_rrset = finder.find(zone_name, RRType.SOA(), None,
+ ZoneFinder.FIND_DEFAULT)
+ if result != ZoneFinder.SUCCESS:
+ 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 (Rcode.SERVFAIL(), None)
+ return (Rcode.NOERROR(), soa_rrset)
+
+ def __axfr_setup(self, zone_name):
+ '''Setup a zone iterator for AXFR or AXFR-style IXFR.
- # Identify the data source for the requested zone and see if it has
- # SOA while initializing objects used for request processing later.
- # 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
- # consider that one.
- datasrc_config = '{ "database_file": "' + \
- self._server.get_db_file() + '"}'
- self._datasrc_client = self.ClientClass('sqlite3', datasrc_config)
+ '''
try:
# Note that we enable 'separate_rrs'. In xfr-out we need to
# preserve as many things as possible (even if it's half broken)
@@ -336,6 +368,112 @@ class XfroutSession():
return Rcode.NOERROR()
+ def __ixfr_setup(self, request_msg, zone_name, zone_class):
+ '''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.
+
+ '''
+ # Check the authority section. Look for a SOA record with
+ # the same name and class as the question.
+ remote_soa = None
+ for auth_rrset in request_msg.get_section(Message.SECTION_AUTHORITY):
+ # Ignore data whose owner name is not the zone apex, and
+ # ignore non-SOA or different class of records.
+ if auth_rrset.get_name() != zone_name or \
+ auth_rrset.get_type() != RRType.SOA() or \
+ auth_rrset.get_class() != zone_class:
+ continue
+ if auth_rrset.get_rdata_count() != 1:
+ logger.info(XFROUT_IXFR_MULTIPLE_SOA,
+ format_addrinfo(self._remote))
+ return Rcode.FORMERR()
+ remote_soa = auth_rrset
+ if remote_soa is None:
+ logger.info(XFROUT_IXFR_NO_SOA, format_addrinfo(self._remote))
+ return Rcode.FORMERR()
+
+ # Retrieve the local SOA
+ rcode, self._soa = self._get_zone_soa(zone_name)
+ if rcode != Rcode.NOERROR():
+ return rcode
+
+ # RFC1995 says "If an IXFR query with the same or newer version
+ # number than that of the server is received, it is replied to with
+ # a single SOA record of the server's current version, just as
+ # in AXFR". The claim about AXFR is incorrect, but other than that,
+ # we do as the RFC says.
+ # Note: until we complete #1278 we can only check equality of the
+ # two serials. The "newer version" case would fall back to AXFR-style.
+ begin_serial = get_soa_serial(remote_soa.get_rdata()[0])
+ end_serial = get_soa_serial(self._soa.get_rdata()[0])
+ if begin_serial == end_serial:
+ # clear both iterator and jnl_reader to signal we won't do
+ # iteration in response generation
+ self._iterator = None
+ self._jnl_reader = None
+ logger.info(XFROUT_IXFR_UPTODATE, format_addrinfo(self._remote),
+ format_zone_str(zone_name, zone_class),
+ begin_serial, end_serial)
+ return Rcode.NOERROR()
+
+ # Set up the journal reader or fall back to AXFR-style IXFR
+ try:
+ code, self._jnl_reader = self._datasrc_client.get_journal_reader(
+ zone_name, begin_serial, end_serial)
+ except isc.datasrc.NotImplemented as ex:
+ # The underlying data source doesn't support journaling.
+ # Fall back to AXFR-style IXFR.
+ logger.info(XFROUT_IXFR_NO_JOURNAL_SUPPORT,
+ format_addrinfo(self._remote),
+ format_zone_str(zone_name, zone_class))
+ return self.__axfr_setup(zone_name)
+ if code == ZoneJournalReader.NO_SUCH_VERSION:
+ logger.info(XFROUT_IXFR_NO_VERSION, format_addrinfo(self._remote),
+ format_zone_str(zone_name, zone_class),
+ begin_serial, end_serial)
+ return self.__axfr_setup(zone_name)
+ if code == ZoneJournalReader.NO_SUCH_ZONE:
+ # this is quite unexpected as we know zone's SOA exists.
+ # It might be a bug or the data source is somehow broken,
+ # but it can still happen if someone has removed the zone
+ # between these two operations. We treat it as NOTAUTH.
+ logger.warn(XFROUT_IXFR_NO_ZONE, format_addrinfo(self._remote),
+ format_zone_str(zone_name, zone_class))
+ return Rcode.NOTAUTH()
+
+ # Use the reader as the iterator to generate the response.
+ self._iterator = self._jnl_reader
+
+ return Rcode.NOERROR()
+
+ def _xfrout_setup(self, request_msg, zone_name, zone_class):
+ '''Setup a context for xfr responses according to the request type.
+
+ This method identifies the most appropriate data source for the
+ request and set up a zone iterator or journal reader depending on
+ whether the request is AXFR or IXFR. If it identifies any protocol
+ level error it returns an RCODE other than NOERROR.
+
+ '''
+
+ # Identify the data source for the requested zone and see if it has
+ # SOA while initializing objects used for request processing later.
+ # 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
+ # 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():
+ return self.__axfr_setup(zone_name)
+ else:
+ return self.__ixfr_setup(request_msg, zone_name, zone_class)
def dns_xfrout_start(self, sock_fd, msg_query, quota_ok=True):
rcode_, msg = self._parse_query_message(msg_query)
@@ -348,7 +486,7 @@ class XfroutSession():
return self._reply_query_with_error_rcode(msg, sock_fd,
Rcode.FORMERR())
elif not quota_ok:
- logger.warn(XFROUT_QUERY_QUOTA_EXCCEEDED, self._request_type,
+ logger.warn(XFROUT_QUERY_QUOTA_EXCCEEDED, self._request_typestr,
format_addrinfo(self._remote),
self._server._max_transfers_out)
return self._reply_query_with_error_rcode(msg, sock_fd,
@@ -359,27 +497,26 @@ class XfroutSession():
zone_class = question.get_class()
zone_str = format_zone_str(zone_name, zone_class) # for logging
- # TODO: we should also include class in the check
try:
- rcode_ = self._check_xfrout_available(zone_name)
+ rcode_ = self._xfrout_setup(msg, zone_name, zone_class)
except Exception as ex:
- logger.error(XFROUT_XFR_TRANSFER_CHECK_ERROR, self._request_type,
+ logger.error(XFROUT_XFR_TRANSFER_CHECK_ERROR, self._request_typestr,
format_addrinfo(self._remote), zone_str, ex)
rcode_ = Rcode.SERVFAIL()
if rcode_ != Rcode.NOERROR():
- logger.info(XFROUT_AXFR_TRANSFER_FAILED, self._request_type,
+ logger.info(XFROUT_AXFR_TRANSFER_FAILED, self._request_typestr,
format_addrinfo(self._remote), zone_str, rcode_)
return self._reply_query_with_error_rcode(msg, sock_fd, rcode_)
try:
- logger.info(XFROUT_AXFR_TRANSFER_STARTED, self._request_type,
+ logger.info(XFROUT_AXFR_TRANSFER_STARTED, self._request_typestr,
format_addrinfo(self._remote), zone_str)
self._reply_xfrout_query(msg, sock_fd)
except Exception as err:
- logger.error(XFROUT_AXFR_TRANSFER_ERROR, self._request_type,
+ logger.error(XFROUT_AXFR_TRANSFER_ERROR, self._request_typestr,
format_addrinfo(self._remote), zone_str, err)
pass
- logger.info(XFROUT_AXFR_TRANSFER_DONE, self._request_type,
+ logger.info(XFROUT_AXFR_TRANSFER_DONE, self._request_typestr,
format_addrinfo(self._remote), zone_str)
def _clear_message(self, msg):
@@ -409,22 +546,31 @@ 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()
msg.set_header_flag(Message.HEADERFLAG_AA)
- msg.add_rrset(Message.SECTION_ANSWER, self._soa)
+ # If the iterator is None, we are responding to IXFR with a single
+ # SOA RR.
+ if self._iterator is None:
+ self._send_message_with_last_soa(msg, sock_fd, self._soa, 0)
+ return
+
+ # Add the beginning SOA
+ msg.add_rrset(Message.SECTION_ANSWER, self._soa)
message_upper_len = get_rrset_len(self._soa) + self._tsig_len
+ # Add the rest of the zone/diff contets
for rrset in self._iterator:
# Check if xfrout is shutdown
if self._server._shutdown_event.is_set():
logger.info(XFROUT_STOPPING)
return
- if rrset.get_type() == RRType.SOA():
+ # For AXFR (or AXFR-style IXFR), in which case _jnl_reader is None,
+ # we should skip SOAs from the iterator.
+ if self._jnl_reader is None and rrset.get_type() == RRType.SOA():
continue
# We calculate the maximum size of the RRset (i.e. the
@@ -445,6 +591,7 @@ class XfroutSession():
# Reserve tsig space for signed packet
message_upper_len = rrset_len + self._tsig_len
+ # Add and send the trailing SOA
self._send_message_with_last_soa(msg, sock_fd, self._soa,
message_upper_len)
diff --git a/src/bin/xfrout/xfrout_messages.mes b/src/bin/xfrout/xfrout_messages.mes
index 894ade5..f6cca9d 100644
--- a/src/bin/xfrout/xfrout_messages.mes
+++ b/src/bin/xfrout/xfrout_messages.mes
@@ -178,3 +178,42 @@ on, but the file is in use. The most likely cause is that another
xfrout daemon process is still running. This xfrout daemon (the one
printing this message) will not start.
+% XFROUT_IXFR_MULTIPLE_SOA IXFR client %1: authority section has multiple SOAs
+An IXFR request was received with more than one SOA RRs in the authority
+section. The xfrout daemon rejects the request with an RCODE of
+FORMERR.
+
+% XFROUT_IXFR_NO_SOA IXFR client %1: missing SOA
+An IXFR request was received with no SOA RR in the authority section.
+The xfrout daemon rejects the request with an RCODE of FORMERR.
+
+% XFROUT_IXFR_NO_JOURNAL_SUPPORT IXFR client %1, %2: journaling not supported in the data source, falling back to AXFR
+An IXFR request was received but the underlying data source did
+not support journaling. The xfrout daemon fell back to AXFR-style
+IXFR.
+
+% XFROUT_IXFR_UPTODATE IXFR client %1, %2: client version is new enough (theirs=%3, ours=%4)
+An IXFR request was received, but the client's SOA version is the same as
+or newer than that of the server. The xfrout server responds to the
+request with the answer section being just one SOA of that version.
+Note: as of this wrting the 'newer version' cannot be identified due to
+the lack of support for the serial number arithmetic. This will soon
+be implemented.
+
+% XFROUT_IXFR_NO_VERSION IXFR client %1, %2: version (%3 to %4) not in journal, falling back to AXFR
+An IXFR request was received, but the requested range of differences
+were not found in the data source. The xfrout daemon fell back to
+AXFR-style IXFR.
+
+% XFROUT_IXFR_NO_ZONE IXFR client %1, %2: zone not found with journal
+The requested zone in IXFR was not found in the data source
+even though the xfrout daemon sucessfully found the SOA RR of the zone
+in the data source. This can happen if the administrator removed the
+zone from the data source within the small duration between these
+operations, but it's more likely to be a bug or broken data source.
+Unless you know why this message was logged, and especially if it
+happens often, it's advisable to check whether the data source is
+valid for this zone. The xfrout daemon considers it a possible,
+though unlikely, event, and returns a response with an RCODE of
+NOTAUTH.
+
diff --git a/src/lib/python/isc/datasrc/client_inc.cc b/src/lib/python/isc/datasrc/client_inc.cc
index 575e33c..e0c0f06 100644
--- a/src/lib/python/isc/datasrc/client_inc.cc
+++ b/src/lib/python/isc/datasrc/client_inc.cc
@@ -233,6 +233,8 @@ isc.datasrc.NotImplemented.\n\
Exceptions:\n\
isc.datasrc.NotImplemented The data source does not support differences.\n\
isc.datasrc.Error Other operational errors at the data source level.\n\
+ SystemError An unexpected error in the backend C++ code. Either a rare\n\
+ system error such as short memory or an implementation bug.\n\
\n\
Parameters:\n\
zone The name of the zone for which the difference should be\n\
diff --git a/src/lib/python/isc/datasrc/client_python.cc b/src/lib/python/isc/datasrc/client_python.cc
index 22b5a48..bdf84a3 100644
--- a/src/lib/python/isc/datasrc/client_python.cc
+++ b/src/lib/python/isc/datasrc/client_python.cc
@@ -182,19 +182,31 @@ DataSourceClient_getJournalReader(PyObject* po_self, PyObject* args) {
if (PyArg_ParseTuple(args, "O!kk", &name_type, &name_obj,
&begin_obj, &end_obj)) {
- pair<ZoneJournalReader::Result, ZoneJournalReaderPtr> result =
- self->cppobj->getInstance().getJournalReader(
- PyName_ToName(name_obj), static_cast<uint32_t>(begin_obj),
- static_cast<uint32_t>(end_obj));
- PyObject* po_reader;
- if (result.first == ZoneJournalReader::SUCCESS) {
- po_reader = createZoneJournalReaderObject(result.second, po_self);
- } else {
- po_reader = Py_None;
- Py_INCREF(po_reader); // this will soon be released
+ try {
+ pair<ZoneJournalReader::Result, ZoneJournalReaderPtr> result =
+ self->cppobj->getInstance().getJournalReader(
+ PyName_ToName(name_obj), static_cast<uint32_t>(begin_obj),
+ static_cast<uint32_t>(end_obj));
+ PyObject* po_reader;
+ if (result.first == ZoneJournalReader::SUCCESS) {
+ po_reader = createZoneJournalReaderObject(result.second,
+ po_self);
+ } else {
+ po_reader = Py_None;
+ Py_INCREF(po_reader); // this will soon be released
+ }
+ PyObjectContainer container(po_reader);
+ return (Py_BuildValue("(iO)", result.first, container.get()));
+ } catch (const isc::NotImplemented& ex) {
+ PyErr_SetString(getDataSourceException("NotImplemented"),
+ ex.what());
+ } catch (const DataSourceError& ex) {
+ PyErr_SetString(getDataSourceException("Error"), ex.what());
+ } catch (const std::exception& ex) {
+ PyErr_SetString(PyExc_SystemError, ex.what());
+ } catch (...) {
+ PyErr_SetString(PyExc_SystemError, "Unexpected exception");
}
- PyObjectContainer container(po_reader);
- return (Py_BuildValue("(iO)", result.first, container.get()));
}
return (NULL);
}
diff --git a/src/lib/python/isc/datasrc/tests/Makefile.am b/src/lib/python/isc/datasrc/tests/Makefile.am
index 411b5cc..400abcf 100644
--- a/src/lib/python/isc/datasrc/tests/Makefile.am
+++ b/src/lib/python/isc/datasrc/tests/Makefile.am
@@ -6,6 +6,7 @@ EXTRA_DIST = $(PYTESTS)
EXTRA_DIST += testdata/brokendb.sqlite3
EXTRA_DIST += testdata/example.com.sqlite3
+EXTRA_DIST += testdata/test.sqlite3.nodiffs
CLEANFILES = $(abs_builddir)/rwtest.sqlite3.copied
# If necessary (rare cases), explicitly specify paths to dynamic libraries
diff --git a/src/lib/python/isc/datasrc/tests/datasrc_test.py b/src/lib/python/isc/datasrc/tests/datasrc_test.py
index b0e06fd..e46c177 100644
--- a/src/lib/python/isc/datasrc/tests/datasrc_test.py
+++ b/src/lib/python/isc/datasrc/tests/datasrc_test.py
@@ -17,6 +17,7 @@ import isc.log
import isc.datasrc
from isc.datasrc import ZoneFinder, ZoneJournalReader
from isc.dns import *
+from isc.testutils.rrset_utils import rrsets_equal
import unittest
import sqlite3
import os
@@ -40,19 +41,6 @@ def add_rrset(rrset_list, name, rrclass, rrtype, ttl, rdatas):
rrset_to_add.add_rdata(isc.dns.Rdata(rrtype, rrclass, rdata))
rrset_list.append(rrset_to_add)
-# helper function, we have no direct rrset comparison atm
-def rrsets_equal(a, b):
- # no accessor for sigs either (so this only checks name, class, type, ttl,
- # and rdata)
- # also, because of the fake data in rrsigs, if the type is rrsig, the
- # rdata is not checked
- return a.get_name() == b.get_name() and\
- a.get_class() == b.get_class() and\
- a.get_type() == b.get_type() and \
- a.get_ttl() == b.get_ttl() and\
- (a.get_type() == isc.dns.RRType.RRSIG() or
- sorted(a.get_rdata()) == sorted(b.get_rdata()))
-
# returns true if rrset is in expected_rrsets
# will remove the rrset from expected_rrsets if found
def check_for_rrset(expected_rrsets, rrset):
@@ -803,6 +791,15 @@ class JournalRead(unittest.TestCase):
# ZoneJournalReader can only be constructed via a factory
self.assertRaises(TypeError, ZoneJournalReader)
+ def test_journal_reader_old_schema(self):
+ # The database doesn't have a "diffs" table.
+ dbfile = TESTDATA_PATH + 'test.sqlite3.nodiffs'
+ client = isc.datasrc.DataSourceClient("sqlite3",
+ "{ \"database_file\": \"" + \
+ dbfile + "\" }")
+ self.assertRaises(isc.datasrc.Error, client.get_journal_reader,
+ self.zname, 0, 1)
+
if __name__ == "__main__":
isc.log.init("bind10")
isc.log.resetUnitTestRootLogger()
diff --git a/src/lib/python/isc/datasrc/tests/testdata/test.sqlite3.nodiffs b/src/lib/python/isc/datasrc/tests/testdata/test.sqlite3.nodiffs
new file mode 100644
index 0000000..cc8cfc3
Binary files /dev/null and b/src/lib/python/isc/datasrc/tests/testdata/test.sqlite3.nodiffs differ
diff --git a/src/lib/python/isc/testutils/Makefile.am b/src/lib/python/isc/testutils/Makefile.am
index 0b08257..5479d83 100644
--- a/src/lib/python/isc/testutils/Makefile.am
+++ b/src/lib/python/isc/testutils/Makefile.am
@@ -1,4 +1,4 @@
-EXTRA_DIST = __init__.py parse_args.py tsigctx_mock.py
+EXTRA_DIST = __init__.py parse_args.py tsigctx_mock.py rrset_utils.py
CLEANDIRS = __pycache__
diff --git a/src/lib/python/isc/testutils/rrset_utils.py b/src/lib/python/isc/testutils/rrset_utils.py
new file mode 100644
index 0000000..8c22d92
--- /dev/null
+++ b/src/lib/python/isc/testutils/rrset_utils.py
@@ -0,0 +1,63 @@
+# Copyright (C) 2011 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.
+
+'''Utility functions handling DNS RRsets commonly used for tests'''
+
+from isc.dns import *
+
+def rrsets_equal(a, b):
+ '''Compare two RRsets, return True if equal, otherwise False
+
+ We provide this function as part of test utils as we have no direct rrset
+ comparison atm. There's no accessor for sigs either (so this only checks
+ name, class, type, ttl, and rdata).
+ Also, since we often use fake data in RRSIGs, RRSIG RDATA are not checked.
+
+ '''
+ return a.get_name() == b.get_name() and \
+ a.get_class() == b.get_class() and \
+ a.get_type() == b.get_type() and \
+ a.get_ttl() == b.get_ttl() and \
+ (a.get_type() == RRType.RRSIG() or
+ sorted(a.get_rdata()) == sorted(b.get_rdata()))
+
+# The following are short cut utilities to create an RRset of a specific
+# RR type with one RDATA. Many of the RR parameters are common in most
+# tests, so we define default values for them for convenience.
+
+def create_a(name, address, ttl=3600):
+ rrset = RRset(name, RRClass.IN(), RRType.A(), RRTTL(ttl))
+ rrset.add_rdata(Rdata(RRType.A(), RRClass.IN(), address))
+ return rrset
+
+def create_aaaa(name, address, ttl=3600):
+ rrset = RRset(name, RRClass.IN(), RRType.AAAA(), RRTTL(ttl))
+ rrset.add_rdata(Rdata(RRType.AAAA(), RRClass.IN(), address))
+ return rrset
+
+def create_ns(nsname, name=Name('example.com'), ttl=3600):
+ '''For convenience we use a default name often used as a zone name'''
+ rrset = RRset(name, RRClass.IN(), RRType.NS(), RRTTL(ttl))
+ rrset.add_rdata(Rdata(RRType.NS(), RRClass.IN(), nsname))
+ return rrset
+
+def create_soa(serial, name=Name('example.com'), ttl=3600):
+ '''For convenience we use a default name often used as a zone name'''
+
+ rrset = RRset(name, RRClass.IN(), RRType.SOA(), RRTTL(ttl))
+ rdata_str = 'master.example.com. admin.example.com. ' + \
+ str(serial) + ' 3600 1800 2419200 7200'
+ rrset.add_rdata(Rdata(RRType.SOA(), RRClass.IN(), rdata_str))
+ return rrset
More information about the bind10-changes
mailing list