Commit a4f77631 authored by JINMEI Tatuya's avatar JINMEI Tatuya
Browse files

[master] [1299] added more detailed validity check on SOA response.

also introduce a separate log message ID for XfrinProtocolError at the
INFO level (ERROR isn't appropriate because these are generally uncontrollable
events), and made sure we use the XfrinProtocolError for errors due to
the remote behavior.
parent d3792aa7
......@@ -277,6 +277,7 @@ class MockXfrinConnection(XfrinConnection):
rcode=Rcode.NOERROR(),
questions=default_questions,
answers=default_answers,
authorities=[],
tsig_ctx=None):
resp = Message(Message.RENDER)
qid = self.qid
......@@ -291,6 +292,7 @@ class MockXfrinConnection(XfrinConnection):
resp.set_header_flag(Message.HEADERFLAG_AA)
[resp.add_question(q) for q in questions]
[resp.add_rrset(Message.SECTION_ANSWER, a) for a in answers]
[resp.add_rrset(Message.SECTION_AUTHORITY, a) for a in authorities]
renderer = MessageRenderer()
if tsig_ctx is not None:
......@@ -603,6 +605,7 @@ class TestXfrinConnection(unittest.TestCase):
'auth': True,
'rcode': Rcode.NOERROR(),
'answers': default_answers,
'authorities': [],
'tsig': False,
'axfr_after_soa': self._create_normal_response_data
}
......@@ -661,8 +664,9 @@ class TestXfrinConnection(unittest.TestCase):
response=self.soa_response_params['response'],
auth=self.soa_response_params['auth'],
rcode=self.soa_response_params['rcode'],
answers=self.soa_response_params['answers'],
questions=self.soa_response_params['questions'],
answers=self.soa_response_params['answers'],
authorities=self.soa_response_params['authorities'],
tsig_ctx=verify_ctx)
if self.soa_response_params['axfr_after_soa'] != None:
self.conn.response_generator = \
......@@ -846,8 +850,10 @@ class TestAXFR(TestXfrinConnection):
self.conn._tsig_key = TSIG_KEY
# server tsig check fail, return with RCODE 9 (NOTAUTH)
self.conn._send_query(RRType.SOA())
self.conn.reply_data = self.conn.create_response_data(rcode=Rcode.NOTAUTH())
self.assertRaises(XfrinException, self.conn._handle_xfrin_responses)
self.conn.reply_data = \
self.conn.create_response_data(rcode=Rcode.NOTAUTH())
self.assertRaises(XfrinProtocolError,
self.conn._handle_xfrin_responses)
def test_response_without_end_soa(self):
self.conn._send_query(RRType.AXFR())
......@@ -860,7 +866,8 @@ class TestAXFR(TestXfrinConnection):
def test_response_bad_qid(self):
self.conn._send_query(RRType.AXFR())
self.conn.reply_data = self.conn.create_response_data(bad_qid=True)
self.assertRaises(XfrinException, self.conn._handle_xfrin_responses)
self.assertRaises(XfrinProtocolError,
self.conn._handle_xfrin_responses)
def test_response_error_code_bad_sig(self):
self.conn._tsig_key = TSIG_KEY
......@@ -871,7 +878,7 @@ class TestAXFR(TestXfrinConnection):
rcode=Rcode.SERVFAIL())
# xfrin should check TSIG before other part of incoming message
# validate log message for XfrinException
self.__match_exception(XfrinException,
self.__match_exception(XfrinProtocolError,
"TSIG verify fail: BADSIG",
self.conn._handle_xfrin_responses)
......@@ -883,7 +890,7 @@ class TestAXFR(TestXfrinConnection):
self.conn.reply_data = self.conn.create_response_data(bad_qid=True)
# xfrin should check TSIG before other part of incoming message
# validate log message for XfrinException
self.__match_exception(XfrinException,
self.__match_exception(XfrinProtocolError,
"TSIG verify fail: BADKEY",
self.conn._handle_xfrin_responses)
......@@ -896,18 +903,21 @@ class TestAXFR(TestXfrinConnection):
self.conn._send_query(RRType.AXFR())
self.conn.reply_data = self.conn.create_response_data(
rcode=Rcode.SERVFAIL())
self.assertRaises(XfrinException, self.conn._handle_xfrin_responses)
self.assertRaises(XfrinProtocolError,
self.conn._handle_xfrin_responses)
def test_response_multi_question(self):
self.conn._send_query(RRType.AXFR())
self.conn.reply_data = self.conn.create_response_data(
questions=[example_axfr_question, example_axfr_question])
self.assertRaises(XfrinException, self.conn._handle_xfrin_responses)
self.assertRaises(XfrinProtocolError,
self.conn._handle_xfrin_responses)
def test_response_non_response(self):
self.conn._send_query(RRType.AXFR())
self.conn.reply_data = self.conn.create_response_data(response = False)
self.assertRaises(XfrinException, self.conn._handle_xfrin_responses)
self.assertRaises(XfrinProtocolError,
self.conn._handle_xfrin_responses)
def test_soacheck(self):
# we need to defer the creation until we know the QID, which is
......@@ -922,7 +932,7 @@ class TestAXFR(TestXfrinConnection):
def test_soacheck_badqid(self):
self.soa_response_params['bad_qid'] = True
self.conn.response_generator = self._create_soa_response_data
self.assertRaises(XfrinException, self.conn._check_soa_serial)
self.assertRaises(XfrinProtocolError, self.conn._check_soa_serial)
def test_soacheck_bad_qid_bad_sig(self):
self.conn._tsig_key = TSIG_KEY
......@@ -932,19 +942,19 @@ class TestAXFR(TestXfrinConnection):
self.conn.response_generator = self._create_soa_response_data
# xfrin should check TSIG before other part of incoming message
# validate log message for XfrinException
self.__match_exception(XfrinException,
self.__match_exception(XfrinProtocolError,
"TSIG verify fail: BADSIG",
self.conn._check_soa_serial)
def test_soacheck_non_response(self):
self.soa_response_params['response'] = False
self.conn.response_generator = self._create_soa_response_data
self.assertRaises(XfrinException, self.conn._check_soa_serial)
self.assertRaises(XfrinProtocolError, self.conn._check_soa_serial)
def test_soacheck_error_code(self):
self.soa_response_params['rcode'] = Rcode.SERVFAIL()
self.conn.response_generator = self._create_soa_response_data
self.assertRaises(XfrinException, self.conn._check_soa_serial)
self.assertRaises(XfrinProtocolError, self.conn._check_soa_serial)
def test_soacheck_notauth(self):
self.soa_response_params['auth'] = False
......@@ -996,6 +1006,49 @@ class TestAXFR(TestXfrinConnection):
RRType.AAAA())]
self.assertRaises(XfrinProtocolError, self.conn._check_soa_serial)
def test_soacheck_no_soa(self):
# The response just doesn't contain SOA without any other indication
# of errors.
self.conn.response_generator = self._create_soa_response_data
self.soa_response_params['answers'] = []
self.assertRaises(XfrinProtocolError, self.conn._check_soa_serial)
def test_soacheck_soa_name_mismatch(self):
self.conn.response_generator = self._create_soa_response_data
self.soa_response_params['answers'] = [create_soa(1234,
Name('example.org'))]
self.assertRaises(XfrinProtocolError, self.conn._check_soa_serial)
def test_soacheck_soa_class_mismatch(self):
self.conn.response_generator = self._create_soa_response_data
soa = RRset(TEST_ZONE_NAME, RRClass.CH(), RRType.SOA(), RRTTL(0))
soa.add_rdata(Rdata(RRType.SOA(), RRClass.CH(), 'm. r. 1234 0 0 0 0'))
self.soa_response_params['answers'] = [soa]
self.assertRaises(XfrinProtocolError, self.conn._check_soa_serial)
def test_soacheck_multiple_soa(self):
self.conn.response_generator = self._create_soa_response_data
self.soa_response_params['answers'] = [soa_rrset, soa_rrset]
self.assertRaises(XfrinProtocolError, self.conn._check_soa_serial)
def test_soacheck_cname_response(self):
self.conn.response_generator = self._create_soa_response_data
# Add SOA to answer, too, to make sure that it that deceives the parser
self.soa_response_params['answers'] = [soa_rrset, create_cname()]
self.assertRaises(XfrinProtocolError, self.conn._check_soa_serial)
def test_soacheck_referral_response(self):
self.conn.response_generator = self._create_soa_response_data
self.soa_response_params['answers'] = []
self.soa_response_params['authorities'] = [create_ns('ns.example.com')]
self.assertRaises(XfrinProtocolError, self.conn._check_soa_serial)
def test_soacheck_nodata_response(self):
self.conn.response_generator = self._create_soa_response_data
self.soa_response_params['answers'] = []
self.soa_response_params['authorities'] = [soa_rrset]
self.assertRaises(XfrinProtocolError, self.conn._check_soa_serial)
def test_soacheck_with_tsig(self):
# Use a mock tsig context emulating a validly signed response
self.conn._tsig_key = TSIG_KEY
......@@ -1013,7 +1066,7 @@ class TestAXFR(TestXfrinConnection):
self.soa_response_params['rcode'] = Rcode.NOTAUTH()
self.conn.response_generator = self._create_soa_response_data
self.assertRaises(XfrinException, self.conn._check_soa_serial)
self.assertRaises(XfrinProtocolError, self.conn._check_soa_serial)
def test_soacheck_with_tsig_noerror_badsig(self):
self.conn._tsig_key = TSIG_KEY
......@@ -1026,7 +1079,7 @@ class TestAXFR(TestXfrinConnection):
# treat this as a final failure (just as BIND 9 does).
self.conn.response_generator = self._create_soa_response_data
self.assertRaises(XfrinException, self.conn._check_soa_serial)
self.assertRaises(XfrinProtocolError, self.conn._check_soa_serial)
def test_soacheck_with_tsig_unsigned_response(self):
# we can use a real TSIGContext for this. the response doesn't
......@@ -1035,14 +1088,14 @@ class TestAXFR(TestXfrinConnection):
# it as a fatal transaction failure, too.
self.conn._tsig_key = TSIG_KEY
self.conn.response_generator = self._create_soa_response_data
self.assertRaises(XfrinException, self.conn._check_soa_serial)
self.assertRaises(XfrinProtocolError, self.conn._check_soa_serial)
def test_soacheck_with_unexpected_tsig_response(self):
# we reject unexpected TSIG in responses (following BIND 9's
# behavior)
self.soa_response_params['tsig'] = True
self.conn.response_generator = self._create_soa_response_data
self.assertRaises(XfrinException, self.conn._check_soa_serial)
self.assertRaises(XfrinProtocolError, self.conn._check_soa_serial)
def test_response_shutdown(self):
self.conn.response_generator = self._create_normal_response_data
......@@ -1309,6 +1362,13 @@ class TestAXFR(TestXfrinConnection):
self.conn.response_generator = self._create_soa_response_data
self.assertEqual(self.conn.do_xfrin(True), XFRIN_OK)
def test_do_soacheck_protocol_error(self):
# There are several cases, but at this level it's sufficient to check
# only one. We use the case where there's no SOA in the response.
self.soa_response_params['answers'] = []
self.conn.response_generator = self._create_soa_response_data
self.assertEqual(self.conn.do_xfrin(True), XFRIN_FAIL)
def test_do_soacheck_and_xfrin_with_tsig(self):
# We are going to have a SOA query/response transaction, followed by
# AXFR, all TSIG signed. xfrin should use a new TSIG context for
......
......@@ -24,6 +24,7 @@ import struct
import threading
import socket
import random
from functools import reduce
from optparse import OptionParser, OptionValueError
from isc.config.ccsession import *
from isc.notify import notify_out
......@@ -690,7 +691,8 @@ class XfrinConnection(asyncore.dispatcher):
if self._tsig_ctx is not None:
tsig_error = self._tsig_ctx.verify(tsig_record, response_data)
if tsig_error != TSIGError.NOERROR:
raise XfrinException('TSIG verify fail: %s' % str(tsig_error))
raise XfrinProtocolError('TSIG verify fail: %s' %
str(tsig_error))
elif tsig_record is not None:
# If the response includes a TSIG while we didn't sign the query,
# we treat it as an error. RFC doesn't say anything about this
......@@ -699,7 +701,7 @@ class XfrinConnection(asyncore.dispatcher):
# implementation would return such a response, and since this is
# part of security mechanism, it's probably better to be more
# strict.
raise XfrinException('Unexpected TSIG in response')
raise XfrinProtocolError('Unexpected TSIG in response')
def __parse_soa_response(self, msg, response_data):
'''Parse a response to SOA query and extranct the SOA from ansser.
......@@ -730,12 +732,34 @@ class XfrinConnection(asyncore.dispatcher):
raise XfrinProtocolError('Questions mismatch to ' +
'SOA query: ' + str(resp_question))
# Examine the answer section
# Look into the answer section for SOA
soa = None
for rrset in msg.get_section(Message.SECTION_ANSWER):
if rrset.get_type() == RRType.SOA():
soa = rrset
for rr in msg.get_section(Message.SECTION_ANSWER):
if rr.get_type() == RRType.SOA():
if soa is not None:
raise XfrinProtocolError('SOA response had multiple SOAs')
soa = rr
# There should not be a CNAME record at top of zone.
if rr.get_type() == RRType.CNAME():
raise XfrinProtocolError('SOA query resulted in CNAME')
# If SOA is not found, try to figure out the reason then report it.
if soa is None:
# See if we have any SOA records in the authority section.
for rr in msg.get_section(Message.SECTION_AUTHORITY):
if rr.get_type() == RRType.NS():
raise XfrinProtocolError('SOA query resulted in referral')
if rr.get_type() == RRType.SOA():
raise XfrinProtocolError('SOA query resulted in NODATA')
raise XfrinProtocolError('SOA query resulted in no SOA at all')
# Check if the SOA is really what we asked for
if soa.get_name() != self._zone_name or \
soa.get_class() != self._rrclass:
raise XfrinProtocolError("SOA response doesn't match query: " +
str(soa))
# All okay, return it
return soa
......@@ -751,14 +775,14 @@ class XfrinConnection(asyncore.dispatcher):
msg_len = socket.htons(struct.unpack('H', data_len)[0])
soa_response = self._get_request_response(msg_len)
msg = Message(Message.PARSE)
msg.from_wire(soa_response)
msg.from_wire(soa_response, Message.PRESERVE_ORDER)
# Validate/parse the rest of the response, and extract the SOA
# from the answer section
soa = self.__parse_soa_response(msg, soa_response)
primary_serial = get_soa_serial(soa.get_rdata()[0])
if (self._request_serial is not None) and \
if self._request_serial is not None and \
self._request_serial >= primary_serial:
if self._request_serial != primary_serial:
logger.info(XFRIN_ZONE_SERIAL_AHEAD, primary_serial,
......@@ -798,7 +822,12 @@ class XfrinConnection(asyncore.dispatcher):
# of trying another primary server, etc, but for now We treat it
# as "success".
pass
except (XfrinException, XfrinProtocolError) as e:
except XfrinProtocolError as e:
logger.info(XFRIN_XFR_TRANSFER_PROTOCOL_ERROR, request_str,
self.zone_str(),
format_addrinfo(self._master_addrinfo), str(e))
ret = XFRIN_FAIL
except XfrinException as e:
logger.error(XFRIN_XFR_TRANSFER_FAILURE, request_str,
self.zone_str(),
format_addrinfo(self._master_addrinfo), str(e))
......@@ -836,13 +865,14 @@ class XfrinConnection(asyncore.dispatcher):
msg_rcode = msg.get_rcode()
if msg_rcode != Rcode.NOERROR():
raise XfrinException('error response: %s' % msg_rcode.to_text())
raise XfrinProtocolError('error response: %s' %
msg_rcode.to_text())
if not msg.get_header_flag(Message.HEADERFLAG_QR):
raise XfrinException('response is not a response')
raise XfrinProtocolError('response is not a response')
if msg.get_qid() != self._query_id:
raise XfrinException('bad query id')
raise XfrinProtocolError('bad query id')
def _check_response_status(self, msg):
'''Check validation of xfr response. '''
......@@ -850,7 +880,7 @@ class XfrinConnection(asyncore.dispatcher):
self._check_response_header(msg)
if msg.get_rr_count(Message.SECTION_QUESTION) > 1:
raise XfrinException('query section count greater than 1')
raise XfrinProtocolError('query section count greater than 1')
def _handle_xfrin_responses(self):
read_next_msg = True
......
......@@ -60,8 +60,18 @@ The XFR transfer for the given zone has failed due to a problem outside
of the xfrin module. Possible reasons are a broken DNS message or failure
in database connection. The error is shown in the log message.
% XFRIN_XFR_TRANSFER_PROTOCOL_ERROR %1 transfer of zone %2 with %3 failed: %4
The XFR transfer for the given zone has failed due to a protocol
error, such as an unexpected response from the primary server. The
error is shown in the log message. It may be because the primary
server implementation is broken or (although less likely) there was
some attack attempt, but it can also happen due to configuration
mismatch such as the remote server does not have authority for the
zone any more but the local configuration hasn't been updated. So it
is recommended to check the primary server configuration.
% XFRIN_XFR_TRANSFER_FAILURE %1 transfer of zone %2 with %3 failed: %4
The XFR transfer for the given zone has failed due to a protocol error.
The XFR transfer for the given zone has failed due to an internal error.
The error is shown in the log message.
% XFRIN_XFR_TRANSFER_FALLBACK falling back from IXFR to AXFR for %1
......
......@@ -53,6 +53,12 @@ def create_ns(nsname, name=Name('example.com'), ttl=3600):
rrset.add_rdata(Rdata(RRType.NS(), RRClass.IN(), nsname))
return rrset
def create_cname(target='target.example.com', name=Name('example.com'),
ttl=3600):
rrset = RRset(name, RRClass.IN(), RRType.CNAME(), RRTTL(ttl))
rrset.add_rdata(Rdata(RRType.CNAME(), RRClass.IN(), target))
return rrset
def create_generic(name, rdlen, type=RRType('TYPE65300'), ttl=3600):
'''Create an RR of a general type with an arbitrary length of RDATA
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment