Browse Source

[master] Merge branch 'trac2934'

JINMEI Tatuya 12 years ago
parent
commit
bde0e94518

+ 1 - 0
configure.ac

@@ -1332,6 +1332,7 @@ AC_CONFIG_FILES([Makefile
                  tests/tools/perfdhcp/Makefile
                  tests/tools/perfdhcp/tests/Makefile
                  tests/tools/perfdhcp/tests/testdata/Makefile
+                 tests/lettuce/Makefile
                  m4macros/Makefile
                  dns++.pc
                ])

+ 55 - 0
src/bin/xfrout/tests/xfrout_test.py.in

@@ -18,6 +18,8 @@
 
 import unittest
 import os
+import socket
+import fcntl
 from isc.testutils.tsigctx_mock import MockTSIGContext
 from isc.testutils.ccsession_mock import MockModuleCCSession
 from isc.cc.session import *
@@ -190,6 +192,24 @@ class Dbserver:
     def decrease_transfers_counter(self):
         self.transfer_counter -= 1
 
+class TestUtility(unittest.TestCase):
+    """Test some utility functions."""
+
+    def test_make_blockign(self):
+        def is_blocking(fd):
+            return (fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_NONBLOCK) == 0
+
+        with socket.socket(socket.AF_INET, socket.SOCK_STREAM,
+                           socket.IPPROTO_TCP) as sock:
+            # By default socket is made blocking
+            self.assertTrue(is_blocking(sock.fileno()))
+            # make_blocking(False) makes it non blocking
+            xfrout.make_blocking(sock.fileno(), False)
+            self.assertFalse(is_blocking(sock.fileno()))
+            # make_blocking(True) makes it blocking again
+            xfrout.make_blocking(sock.fileno(), True)
+            self.assertTrue(is_blocking(sock.fileno()))
+
 class TestXfroutSessionBase(unittest.TestCase):
     '''Base class for tests related to xfrout sessions
 
@@ -269,6 +289,12 @@ class TestXfroutSessionBase(unittest.TestCase):
             self.xfrsess._request_typestr = 'IXFR'
 
     def setUp(self):
+        # xfrout.make_blocking won't work with faked socket, and we'd like to
+        # examine how it's called, so we replace it with our mock.
+        self.__orig_make_blocking = xfrout.make_blocking
+        self._make_blocking_history = []
+        xfrout.make_blocking = lambda x, y: self.__make_blocking(x, y)
+
         self.sock = MySocket(socket.AF_INET,socket.SOCK_STREAM)
         self.xfrsess = MyXfroutSession(self.sock, None, Dbserver(),
                                        TSIGKeyRing(),
@@ -285,7 +311,11 @@ class TestXfroutSessionBase(unittest.TestCase):
         # original is used elsewhere.
         self.orig_get_rrset_len = xfrout.get_rrset_len
 
+    def __make_blocking(self, fd, on):
+        self._make_blocking_history.append((fd, on))
+
     def tearDown(self):
+        xfrout.make_blocking = self.__orig_make_blocking
         xfrout.get_rrset_len = self.orig_get_rrset_len
         # transfer_counter must be always be reset no matter happens within
         # the XfroutSession object.  We check the condition here.
@@ -306,7 +336,12 @@ class TestXfroutSession(TestXfroutSessionBase):
     def test_quota_ok(self):
         '''The default case in terms of the xfrout quota.
 
+        It also confirms that the socket is made blocking.
+
         '''
+        # make_blocking shouldn't be called yet.
+        self.assertEqual([], self._make_blocking_history)
+
         # 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 = \
@@ -316,6 +351,26 @@ class TestXfroutSession(TestXfroutSessionBase):
         XfroutSession._handle(self.xfrsess)
         self.assertEqual(self.sock.read_msg().get_rcode(), Rcode.FORMERR)
 
+        # make_blocking should have been called in _handle().  Note that
+        # in the test fixture we handle fileno as a faked socket object, not
+        # as integer.
+        self.assertEqual([(self.sock, True)], self._make_blocking_history)
+
+    def test_make_blocking_fail(self):
+        """Check what if make_blocking() raises an exception."""
+
+        # make_blocking() can fail due to OSError.  It shouldn't cause
+        # disruption, and xfrout_start shouldn't be called.
+
+        def raise_exception():
+            raise OSError('faked error')
+        xfrout.make_blocking = lambda x, y: raise_exception()
+        self.start_arg = []
+        self.xfrsess.dns_xfrout_start = \
+            lambda s, x, y: self.start_arg.append((x, y))
+        XfroutSession._handle(self.xfrsess)
+        self.assertEqual([], self.start_arg)
+
     def test_exception_from_session(self):
         '''Test the case where the main processing raises an exception.
 

+ 26 - 0
src/bin/xfrout/xfrout.py.in

@@ -30,6 +30,7 @@ from isc.cc import SessionError, SessionTimeout
 from isc.statistics import Counters
 from isc.notify import notify_out
 import isc.util.process
+import fcntl
 import socket
 import select
 import errno
@@ -152,6 +153,27 @@ def get_soa_serial(soa_rdata):
     '''
     return Serial(int(soa_rdata.to_text().split()[2]))
 
+def make_blocking(filenum, on):
+    """A helper function to change blocking mode of the given socket.
+
+    It sets the mode of blocking I/O for the socket associated with filenum
+    (descriptor of the socket) according to parameter 'on': if it's True the
+    file will be made blocking; otherwise it will be made non-blocking.
+
+    The given filenum must be a descriptor of a socket (not an ordinary file
+    etc), but this function doesn't check that condition.
+
+    filenum(int): file number (descriptor) of the socket to update.
+    on(bool): whether enable (True) or disable (False) blocking I/O.
+
+    """
+    flags = fcntl.fcntl(filenum, fcntl.F_GETFL)
+    if on:                      # make it blocking
+        flags &= ~os.O_NONBLOCK
+    else:                       # make it non blocking
+        flags |= os.O_NONBLOCK
+    fcntl.fcntl(filenum, fcntl.F_SETFL, flags)
+
 class XfroutSession():
     def __init__(self, sock_fd, request_data, server, tsig_key_ring, remote,
                  default_acl, zone_config, client_class=DataSourceClient):
@@ -190,6 +212,10 @@ class XfroutSession():
         quota_ok = self._server.increase_transfers_counter()
         ex = None
         try:
+            # Before start, make sure the socket uses blocking I/O because
+            # responses will be sent in the blocking mode; otherwise it could
+            # result in EWOULDBLOCK and disrupt the session.
+            make_blocking(self._sock_fd, True)
             self.dns_xfrout_start(self._sock_fd, self._request_data, quota_ok)
         except Exception as e:
             # To avoid resource leak we need catch all possible exceptions

+ 6 - 0
src/lib/asiodns/asiodns_messages.mes

@@ -103,6 +103,12 @@ would be better to not be too verbose, but you might want to increase
 the log level and make sure there's no resource leak or other system
 level troubles when it's logged.
 
+% ASIODNS_TCP_CLOSE_NORESP_FAIL failed to close DNS/TCP socket with a client: %1
+A TCP DNS server tried to close a TCP socket used to communicate with
+a client without returning an answer (which normally happens for zone
+transfer requests), but it failed to do that.  See ASIODNS_TCP_CLOSE_FAIL
+for more details.
+
 % ASIODNS_TCP_GETREMOTE_FAIL failed to get remote address of a DNS TCP connection: %1
 A TCP DNS server tried to get the address and port of a remote client
 on a connected socket but failed.  It's expected to be rare but can

+ 7 - 0
src/lib/asiodns/tcp_server.cc

@@ -238,8 +238,15 @@ TCPServer::operator()(asio::error_code ec, size_t length) {
         // The 'done_' flag indicates whether we have an answer
         // to send back.  If not, exit the coroutine permanently.
         if (!done_) {
+            // Explicitly close() isn't necessary for most cases. But for the
+            // very connection, socket_ is shared with the original owner of
+            // the server object and would stay open.
             // TODO: should we keep the connection open for a short time
             // to see if new requests come in?
+            socket_->close(ec);
+            if (ec) {
+                LOG_DEBUG(logger, 0, ASIODNS_TCP_CLOSE_FAIL).arg(ec.message());
+            }
             return;
         }
 

+ 1 - 1
tests/Makefile.am

@@ -1 +1 @@
-SUBDIRS = tools
+SUBDIRS = tools lettuce

+ 1 - 0
tests/lettuce/Makefile.am

@@ -0,0 +1 @@
+noinst_SCRIPTS = setup_intree_bind10.sh

+ 5 - 2
tests/lettuce/README

@@ -6,7 +6,8 @@ these tests are specific for BIND10, but we are keeping in mind that RFC-related
 tests could be separated, so that we can test other systems as well.
 
 Prerequisites:
-- BIND 10 must be compiled or installed
+- BIND 10 must be compiled or installed (even when testing in-tree build;
+  see below)
 - dig
 - lettuce (http://lettuce.it)
 
@@ -32,7 +33,9 @@ By default it will use the build tree, but you can use an installed version
 of bind10 by passing -I as the first argument of run_lettuce.sh
 
 The tool 'dig' must be in the default search path of your environment. If
-you specified -I, so must the main bind10 program, as well as bindctl.
+you specified -I, so must the main BIND 10 programs.  And, with or without
+-I, some BIND 10 programs still have to be installed as they are invoked
+from test tools.  Those include bindctl and b10-loadzone.
 
 Due to the default way lettuce prints its output, it is advisable to run it
 in a terminal that is wide than the default. If you see a lot of lines twice

+ 41 - 0
tests/lettuce/configurations/xfrout_master.conf

@@ -0,0 +1,41 @@
+{
+    "version": 3,
+    "Logging": {
+        "loggers": [ {
+            "debuglevel": 99,
+            "severity": "DEBUG",
+            "name": "*"
+        } ]
+    },
+    "Auth": {
+        "database_file": "data/xfrout.sqlite3",
+        "listen_on": [ {
+            "address": "::1",
+            "port": 47806
+        } ]
+    },
+    "data_sources": {
+        "classes": {
+            "IN": [{
+                "type": "sqlite3",
+                "params": {
+                    "database_file": "data/xfrout.sqlite3"
+                }
+            }]
+        }
+    },
+    "Xfrout": {
+        "zone_config": [ {
+            "origin": "example.org"
+        } ]
+    },
+    "Init": {
+        "components": {
+            "b10-auth": { "kind": "needed", "special": "auth" },
+            "b10-xfrout": { "address": "Xfrout", "kind": "dispensable" },
+            "b10-zonemgr": { "address": "Zonemgr", "kind": "dispensable" },
+            "b10-stats": { "address": "Stats", "kind": "dispensable" },
+            "b10-cmdctl": { "special": "cmdctl", "kind": "needed" }
+        }
+    }
+}

+ 86 - 0
tests/lettuce/features/terrain/loadzone.py

@@ -0,0 +1,86 @@
+# Copyright (C) 2013  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.
+
+from lettuce import *
+import subprocess
+import tempfile
+
+def run_loadzone(zone, zone_file, db_file):
+    """Run b10-loadzone.
+
+    It currently only works for an SQLite3-based data source, and takes
+    its DB file as a parameter.
+
+    Parameters:
+    zone (str): the zone name
+    zone_file (str): master zone file for the zone
+    db_file (str): SQLite3 DB file to load the zone into
+
+    """
+    sqlite_datasrc_cfg = '{"database_file": "' + db_file + '"}'
+    args = ['b10-loadzone', '-c', sqlite_datasrc_cfg, zone, zone_file]
+    loadzone = subprocess.Popen(args, 1, None, None,
+                                subprocess.PIPE, subprocess.PIPE)
+    (stdout, stderr) = loadzone.communicate()
+    result = loadzone.returncode
+    world.last_loadzone_stdout = stdout
+    world.last_loadzone_stderr = stderr
+    assert result == 0, "loadzone exit code: " + str(result) +\
+                        "\nstdout:\n" + str(stdout) +\
+                        "stderr:\n" + str(stderr)
+
+@step('load zone (\S+) to DB file (\S+) from (\S+)')
+def load_zone_to_dbfile(step, zone, db_file, zone_file):
+    """Load a zone into a data source from a zone file.
+
+    It currently only works for an SQLite3-based data source.  Its
+    DB file name should be specified.
+
+    Step definition:
+    load zone <zone_name> to DB file <db_file> from <zone_file>
+
+    """
+    run_loadzone(zone, zone_file, db_file)
+
+@step('load (\d+) records for zone (\S+) to DB file (\S+)')
+def load_zone_rr_to_dbfile(step, num_records, zone, db_file):
+    """Load a zone with a specified number of RRs into a data source.
+
+    It currently only works for an SQLite3-based data source.  Its
+    DB file name should be specified.
+
+    It creates the content of the zone dynamically so the total number of
+    RRs of the zone is the specified number, including mandatory SOA and NS.
+
+    Step definition:
+    load zone <zone_name> to DB file <db_file> from <zone_file>
+
+    """
+    num_records = int(num_records)
+    assert num_records >= 2, 'zone must have at least 2 RRs: SOA and NS'
+    num_records -= 2
+    with tempfile.NamedTemporaryFile(mode='w', prefix='zone-file',
+                                     dir='data/', delete=True) as f:
+        filename = f.name
+        f.write('$TTL 3600\n')
+        f.write('$ORIGIN .\n')  # so it'll work whether or not zone ends with .
+        f.write(zone + ' IN SOA . . 0 0 0 0 0\n')
+        f.write(zone + ' IN NS 0.' + zone + '\n')
+        count = 0
+        while count < num_records:
+            f.write(str(count) + '.' + zone + ' IN A 192.0.2.1\n')
+            count += 1
+        f.flush()
+        run_loadzone(zone, f.name, db_file)

+ 3 - 1
tests/lettuce/features/terrain/terrain.py

@@ -83,7 +83,9 @@ copylist = [
     ["data/xfrin-notify.sqlite3.orig",
      "data/xfrin-notify.sqlite3"],
     ["data/ddns/example.org.sqlite3.orig",
-     "data/ddns/example.org.sqlite3"]
+     "data/ddns/example.org.sqlite3"],
+    ["data/empty_db.sqlite3",
+     "data/xfrout.sqlite3"]
 ]
 
 # This is a list of files that, if present, will be removed before a scenario

+ 63 - 8
tests/lettuce/features/terrain/transfer.py

@@ -18,7 +18,8 @@
 # and inspect the results.
 #
 # Like querying.py, it uses dig to do the transfers, and
-# places its output in a result structure
+# places its output in a result structure.  It also uses a custom client
+# implementation for less normal operations.
 #
 # This is done in a different file with different steps than
 # querying, because the format of dig's output is
@@ -58,7 +59,16 @@ class TransferResult(object):
             if len(line) > 0 and line[0] != ';':
                 self.records.append(line)
 
-@step('An AXFR transfer of ([\w.]+)(?: from ([^:]+|\[[0-9a-fA-F:]+\])(?::([0-9]+))?)?')
+def parse_addr_port(address, port):
+    if address is None:
+        address = "::1"         # default address
+    # convert [IPv6_addr] to IPv6_addr:
+    address = re.sub(r"\[(.+)\]", r"\1", address)
+    if port is None:
+        port = 47806            # default port
+    return (address, port)
+
+@step('An AXFR transfer of ([\w.]+)(?: from ([\d.]+|\[[0-9a-fA-F:]+\])(?::([0-9]+))?)?')
 def perform_axfr(step, zone_name, address, port):
     """
     Perform an AXFR transfer, and store the result as an instance of
@@ -70,15 +80,60 @@ def perform_axfr(step, zone_name, address, port):
     Address defaults to ::1
     Port defaults to 47806
     """
-    if address is None:
-        address = "::1"
-    # convert [IPv6_addr] to IPv6_addr:
-    address = re.sub(r"\[(.+)\]", r"\1", address)
-    if port is None:
-        port = 47806
+    (address, port) = parse_addr_port(address, port)
     args = [ 'dig', 'AXFR', '@' + str(address), '-p', str(port), zone_name ]
     world.transfer_result = TransferResult(args)
 
+@step('A customized AXFR transfer of ([\w.]+)(?: from ([\d.]+|\[[0-9a-fA-F:]+\])(?::([0-9]+))?)?(?: with pose of (\d+) seconds?)?')
+def perform_custom_axfr(step, zone_name, address, port, delay):
+    """Checks AXFR transfer, and store the result in the form of internal
+    CustomTransferResult class, which is compatible with TransferResult.
+
+    Step definition:
+    A customized AXFR transfer of <zone_name> [from <address>:<port>] [with pose of <delay> second]
+
+    If optional delay is specified (not None), it waits for the specified
+    seconds after sending the AXFR query before starting receiving
+    responses.  This emulates a slower AXFR client.
+
+    """
+
+    class CustomTransferResult:
+        """Store transfer result only on the number of received answer RRs.
+
+        To be compatible with TransferResult it stores the result in the
+        'records' attribute, which is a list.  But its content is
+        meaningless; its only use is to be used with
+        check_transfer_result_count where its length is of concern.
+
+        """
+        def __init__(self):
+            self.records = []
+
+    # Build arguments and run xfr-client.py.  On success, it simply dumps
+    # the number of received answer RRs to stdout.
+    (address, port) = parse_addr_port(address, port)
+    args = ['/bin/sh', 'run_python-tool.sh', 'tools/xfr-client.py',
+            '-s', address, '-p', str(port)]
+    if delay is not None:
+        args.extend(['-d', delay])
+    args.append(zone_name)
+    client = subprocess.Popen(args, 1, None, None, subprocess.PIPE,
+                              subprocess.PIPE)
+    (stdout, stderr) = client.communicate()
+    result = client.returncode
+    world.last_client_stdout = stdout
+    world.last_client_stderr = stderr
+    assert result == 0, "xfr-client exit code: " + str(result) +\
+                        "\nstdout:\n" + str(stdout) +\
+                        "stderr:\n" + str(stderr)
+    num_rrs = int(stdout.strip())
+
+    # Make the result object, storing dummy value (None) for the number of
+    # answer RRs in the records list.
+    world.transfer_result = CustomTransferResult()
+    world.transfer_result.records = [None for _ in range(0, num_rrs)]
+
 @step('An IXFR transfer of ([\w.]+) (\d+)(?: from ([^:]+)(?::([0-9]+))?)?(?: over (tcp|udp))?')
 def perform_ixfr(step, zone_name, serial, address, port, protocol):
     """

+ 39 - 0
tests/lettuce/features/xfrout_bind10.feature

@@ -0,0 +1,39 @@
+Feature: Xfrout
+    Tests for Xfrout, specific for BIND 10 behaviour.
+
+    Scenario: normal transfer with a moderate number of RRs
+
+    Load 100 records for zone example.org to DB file data/xfrout.sqlite3
+
+    Given I have bind10 running with configuration xfrout_master.conf
+    And wait for bind10 stderr message BIND10_STARTED_CC
+    And wait for bind10 stderr message CMDCTL_STARTED
+    And wait for bind10 stderr message AUTH_SERVER_STARTED
+    And wait for bind10 stderr message XFROUT_STARTED
+    And wait for bind10 stderr message ZONEMGR_STARTED
+
+    # The transferred zone should have the generated 100 RRs plush one
+    # trailing SOA.
+    When I do a customized AXFR transfer of example.org
+    Then transfer result should have 101 rrs
+
+    # Similar to the previous one, but using a much larger zone, and with
+    # a small delay at the client side.  It should still succeed.
+    # The specific delay (5 seconds) was chosen for an environment that
+    # revealed a bug which is now fixed to reproduce the issue; shorter delays
+    # didn't trigger the problem.  Depending on the OS implementation, machine
+    # speed, etc, the same delay may be too long or too short, but in any case
+    # the test should succeed now.
+    Scenario: transfer a large zone
+
+    Load 50000 records for zone example.org to DB file data/xfrout.sqlite3
+
+    Given I have bind10 running with configuration xfrout_master.conf
+    And wait for bind10 stderr message BIND10_STARTED_CC
+    And wait for bind10 stderr message CMDCTL_STARTED
+    And wait for bind10 stderr message AUTH_SERVER_STARTED
+    And wait for bind10 stderr message XFROUT_STARTED
+    And wait for bind10 stderr message ZONEMGR_STARTED
+
+    When I do a customized AXFR transfer of example.org from [::1]:47806 with pose of 5 seconds
+    Then transfer result should have 50001 rrs

+ 23 - 0
tests/lettuce/run_python-tool.sh

@@ -0,0 +1,23 @@
+#!/bin/sh
+
+# Copyright (C) 2013  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 runs the specified python program, referring to the in-tree
+# BIND 10 Python libraries (in case the program needs them)
+# usage example: run_python-tool.sh tools/xfr-client.py -p 5300 example.org
+
+. ./setup_intree_bind10.sh
+$PYTHON_EXEC $*

File diff suppressed because it is too large
+ 1 - 1
tests/lettuce/setup_intree_bind10.sh.in


+ 103 - 0
tests/lettuce/tools/xfr-client.py

@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+
+# Copyright (C) 2013  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.
+
+# A simple XFR client program with some customized behavior.
+# This is intended to provide some less common or even invalid client behavior
+# for some specific tests on outbound zone transfer.
+# It currently only supports AXFR, but can be extended to support IXFR
+# as we see the need for it.
+#
+# For command line usage, run this program with -h option.
+
+from isc.dns import *
+import sys
+import socket
+import struct
+import time
+from optparse import OptionParser
+
+parser = OptionParser(usage='usage: %prog [options] zone_name')
+parser.add_option('-d', '--delay', dest='delay', action='store', default=None,
+                  help='delay (sec) before receiving responses, ' +
+                  'emulating slow clients')
+parser.add_option('-s', '--server', dest='server_addr', action='store',
+                  default='::1',
+                  help="master server's address [default: %default]")
+parser.add_option('-p', '--port', dest='server_port', action='store',
+                  default=53,
+                  help="master server's TCP port [default: %default]")
+(options, args) = parser.parse_args()
+
+if len(args) == 0:
+    parser.error('missing argument')
+
+# Parse arguments and options, and creates client socket.
+zone_name = Name(args[0])
+gai = socket.getaddrinfo(options.server_addr, int(options.server_port), 0,
+                         socket.SOCK_STREAM, socket.IPPROTO_TCP,
+                         socket.AI_NUMERICHOST|socket.AI_NUMERICSERV)
+server_family, server_socktype, server_proto, server_sockaddr = \
+    (gai[0][0], gai[0][1], gai[0][2], gai[0][4])
+s = socket.socket(server_family, server_socktype, server_proto)
+s.connect(server_sockaddr)
+s.settimeout(60)                # safety net in case of hangup situation
+
+# Build XFR query.
+axfr_qry = Message(Message.RENDER)
+axfr_qry.set_rcode(Rcode.NOERROR)
+axfr_qry.set_opcode(Opcode.QUERY)
+axfr_qry.add_question(Question(zone_name, RRClass.IN, RRType.AXFR))
+
+renderer = MessageRenderer()
+axfr_qry.to_wire(renderer)
+qry_data = renderer.get_data()
+
+# Send the query
+hlen_data = struct.pack('H', socket.htons(len(qry_data)))
+s.send(hlen_data)
+s.send(qry_data)
+
+# If specified wait for the given period
+if options.delay is not None:
+    time.sleep(int(options.delay))
+
+def get_request_response(s, size):
+    """A helper function to receive data of specified length from a socket."""
+    recv_size = 0
+    data = b''
+    while recv_size < size:
+        need_recv_size = size - recv_size
+        tmp_data = s.recv(need_recv_size)
+        if len(tmp_data) == 0:
+            return None
+        recv_size += len(tmp_data)
+        data += tmp_data
+    return data
+
+# Receive responses until the connection is terminated, and dump the
+# number of received answer RRs to stdout.
+num_rrs = 0
+while True:
+    hlen_data = get_request_response(s, 2)
+    if hlen_data is None:
+        break
+    resp_data = get_request_response(
+        s, socket.ntohs(struct.unpack('H', hlen_data)[0]))
+    msg = Message(Message.PARSE)
+    msg.from_wire(resp_data, Message.PRESERVE_ORDER)
+    num_rrs += msg.get_rr_count(Message.SECTION_ANSWER)
+print(str(num_rrs))