Parcourir la source

[master] Merge branch 'trac2977'

Conflicts:
	src/bin/d2/Makefile.am
	src/bin/d2/d2_messages.mes
	src/bin/d2/tests/Makefile.am
Marcin Siodelski il y a 11 ans
Parent
commit
5a67a8982b

+ 2 - 0
src/bin/d2/Makefile.am

@@ -56,6 +56,7 @@ b10_dhcp_ddns_SOURCES += d2_config.cc d2_config.h
 b10_dhcp_ddns_SOURCES += d2_cfg_mgr.cc d2_cfg_mgr.h
 b10_dhcp_ddns_SOURCES += d2_update_message.cc d2_update_message.h
 b10_dhcp_ddns_SOURCES += d2_zone.cc d2_zone.h
+b10_dhcp_ddns_SOURCES += dns_client.cc dns_client.h
 b10_dhcp_ddns_SOURCES += ncr_msg.cc ncr_msg.h
 
 nodist_b10_dhcp_ddns_SOURCES = d2_messages.h d2_messages.cc
@@ -64,6 +65,7 @@ EXTRA_DIST += d2_messages.mes
 b10_dhcp_ddns_LDADD = $(top_builddir)/src/lib/log/libb10-log.la
 b10_dhcp_ddns_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
 b10_dhcp_ddns_LDADD += $(top_builddir)/src/lib/cc/libb10-cc.la
+b10_dhcp_ddns_LDADD += $(top_builddir)/src/lib/asiodns/libb10-asiodns.la
 b10_dhcp_ddns_LDADD += $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
 b10_dhcp_ddns_LDADD += $(top_builddir)/src/lib/config/libb10-cfgclient.la
 b10_dhcp_ddns_LDADD += $(top_builddir)/src/lib/dhcpsrv/libb10-dhcpsrv.la 

+ 5 - 0
src/bin/d2/d2_messages.mes

@@ -129,6 +129,11 @@ This is warning message issued when there are no domains in the configuration
 which match the cited fully qualified domain name (FQDN).  The DNS Update 
 request for the FQDN cannot be processed.
 
+% DHCP_DDNS_INVALID_RESPONSE received response to DNS Update message is malformed: %1
+This is a debug message issued when the DHCP-DDNS application encountered an error
+while decoding a response to DNS Update message. Typically, this error will be
+encountered when a response message is malformed.
+
 % DHCP_DDNS_PROCESS_INIT application init invoked
 This is a debug message issued when the Dhcp-Ddns application enters
 its init method.

+ 2 - 2
src/bin/d2/d2_process.cc

@@ -76,8 +76,8 @@ D2Process::command(const std::string& command, isc::data::ConstElementPtr args){
     // @todo This is the initial implementation.  If and when D2 is extended
     // to support its own commands, this implementation must change. Otherwise
     // it should reject all commands as it does now.
-    LOG_DEBUG(dctl_logger, DBGLVL_TRACE_BASIC,
-              DHCP_DDNS_COMMAND).arg(command).arg(args->str());
+    LOG_DEBUG(dctl_logger, DBGLVL_TRACE_BASIC, DHCP_DDNS_COMMAND)
+        .arg(command).arg(args ? args->str() : "(no args)");
 
     return (isc::config::createAnswer(COMMAND_INVALID, "Unrecognized command: "
                                       + command));

+ 1 - 1
src/bin/d2/d2_process.h

@@ -85,7 +85,7 @@ public:
     ///
     /// @param command is a string label representing the command to execute.
     /// @param args is a set of arguments (if any) required for the given
-    /// command.
+    /// command. It can be a NULL pointer if no arguments exist for a command.
     /// @return an Element that contains the results of command composed
     /// of an integer status value (0 means successful, non-zero means failure),
     /// and a string explanation of the outcome.

+ 4 - 0
src/bin/d2/d2_update_message.h

@@ -63,6 +63,10 @@ public:
         isc::Exception(file, line, what) {}
 };
 
+class D2UpdateMessage;
+
+/// @brief Pointer to the DNS Update Message.
+typedef boost::shared_ptr<D2UpdateMessage> D2UpdateMessagePtr;
 
 /// @brief The @c D2UpdateMessage encapsulates a DNS Update message.
 ///

+ 14 - 9
src/bin/d2/d_controller.cc

@@ -46,7 +46,7 @@ DControllerBase::setController(const DControllerBasePtr& controller) {
 }
 
 void
-DControllerBase::launch(int argc, char* argv[]) {
+DControllerBase::launch(int argc, char* argv[], const bool test_mode) {
     // Step 1 is to parse the command line arguments.
     try {
         parseArgs(argc, argv);
@@ -55,12 +55,16 @@ DControllerBase::launch(int argc, char* argv[]) {
         throw; // rethrow it
     }
 
-    // Now that we know what the mode flags are, we can init logging.
-    // If standalone is enabled, do not buffer initial log messages
-    isc::log::initLogger(bin_name_,
-                         ((verbose_ && stand_alone_)
-                          ? isc::log::DEBUG : isc::log::INFO),
-                         isc::log::MAX_DEBUG_LEVEL, NULL, !stand_alone_);
+    // Do not initialize logger here if we are running unit tests. It would
+    // replace an instance of unit test specific logger.
+    if (!test_mode) {
+        // Now that we know what the mode flags are, we can init logging.
+        // If standalone is enabled, do not buffer initial log messages
+        isc::log::initLogger(bin_name_,
+                             ((verbose_ && stand_alone_)
+                              ? isc::log::DEBUG : isc::log::INFO),
+                             isc::log::MAX_DEBUG_LEVEL, NULL, !stand_alone_);
+    }
 
     LOG_DEBUG(dctl_logger, DBGLVL_START_SHUT, DCTL_STARTING)
               .arg(app_name_).arg(getpid());
@@ -295,7 +299,8 @@ DControllerBase::commandHandler(const std::string& command,
                                 isc::data::ConstElementPtr args) {
 
     LOG_DEBUG(dctl_logger, DBGLVL_COMMAND, DCTL_COMMAND_RECEIVED)
-              .arg(controller_->getAppName()).arg(command).arg(args->str());
+        .arg(controller_->getAppName()).arg(command)
+        .arg(args ? args->str() : "(no args)");
 
     // Invoke the instance method on the controller singleton.
     return (controller_->executeCommand(command, args));
@@ -368,7 +373,7 @@ DControllerBase::executeCommand(const std::string& command,
         if (rcode == COMMAND_INVALID)
         {
             // It wasn't controller command, so may be an application command.
-            answer = process_->command(command,args);
+            answer = process_->command(command, args);
         }
     }
 

+ 14 - 2
src/bin/d2/d_controller.h

@@ -144,8 +144,19 @@ public:
     /// arguments. Note this method is deliberately not virtual to ensure the
     /// proper sequence of events occur.
     ///
+    /// This function can be run in the test mode. It prevents initialization
+    /// of D2 module logger. This is used in unit tests which initialize logger
+    /// in their main function. Such logger uses environmental variables to
+    /// control severity, verbosity etc. Reinitialization of logger by this
+    /// function would replace unit tests specific logger configuration with
+    /// this suitable for D2 running as a bind10 module.
+    ///
     /// @param argc  is the number of command line arguments supplied
     /// @param argv  is the array of string (char *) command line arguments
+    /// @param test_mode is a bool value which indicates if
+    /// @c DControllerBase::launch should be run in the test mode (if true).
+    /// This parameter doesn't have default value to force test implementers to
+    /// enable test mode explicitly.
     ///
     /// @throw throws one of the following exceptions:
     /// InvalidUsage - Indicates invalid command line.
@@ -156,7 +167,7 @@ public:
     /// process event loop.
     /// SessionEndError - Could not disconnect from BIND10 (integrated mode
     /// only).
-    void launch(int argc, char* argv[]);
+    void launch(int argc, char* argv[], const bool test_mode);
 
     /// @brief A dummy configuration handler that always returns success.
     ///
@@ -198,7 +209,8 @@ public:
     /// the virtual instance method, executeCommand.
     ///
     /// @param command textual representation of the command
-    /// @param args parameters of the command
+    /// @param args parameters of the command. It can be NULL pointer if no
+    /// arguments exist for a particular command.
     ///
     /// @return status of the processed command
     static isc::data::ConstElementPtr

+ 247 - 0
src/bin/d2/dns_client.cc

@@ -0,0 +1,247 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or 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 ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC 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.
+
+#include <d2/dns_client.h>
+#include <d2/d2_log.h>
+#include <dns/messagerenderer.h>
+#include <limits>
+
+namespace isc {
+namespace d2 {
+
+namespace {
+
+// OutputBuffer objects are pre-allocated before data is written to them.
+// This is a default number of bytes for the buffers we create within
+// DNSClient class.
+const size_t DEFAULT_BUFFER_SIZE = 128;
+
+}
+
+using namespace isc::util;
+using namespace isc::asiolink;
+using namespace isc::asiodns;
+using namespace isc::dns;
+
+// This class provides the implementation for the DNSClient. This allows for
+// the separation of the DNSClient interface from the implementation details.
+// Currently, implementation uses IOFetch object to handle asynchronous
+// communication with the DNS. This design may be revisited in the future. If
+// implementation is changed, the DNSClient API will remain unchanged thanks
+// to this separation.
+class DNSClientImpl : public asiodns::IOFetch::Callback {
+public:
+    // A buffer holding response from a DNS.
+    util::OutputBufferPtr in_buf_;
+    // A caller-supplied object holding a parsed response from DNS.
+    D2UpdateMessagePtr response_;
+    // A caller-supplied external callback which is invoked when DNS message
+    // exchange is complete or interrupted.
+    DNSClient::Callback* callback_;
+    // A Transport Layer protocol used to communicate with a DNS.
+    DNSClient::Protocol proto_;
+
+    // Constructor and Destructor
+    DNSClientImpl(D2UpdateMessagePtr& response_placeholder,
+                  DNSClient::Callback* callback,
+                  const DNSClient::Protocol proto);
+    virtual ~DNSClientImpl();
+
+    // This internal callback is called when the DNS update message exchange is
+    // complete. It further invokes the external callback provided by a caller.
+    // Before external callback is invoked, an object of the D2UpdateMessage
+    // type, representing a response from the server is set.
+    virtual void operator()(asiodns::IOFetch::Result result);
+
+    // Starts asynchronous DNS Update.
+    void doUpdate(asiolink::IOService& io_service,
+                  const asiolink::IOAddress& ns_addr,
+                  const uint16_t ns_port,
+                  D2UpdateMessage& update,
+                  const unsigned int wait);
+
+    // This function maps the IO error to the DNSClient error.
+    DNSClient::Status getStatus(const asiodns::IOFetch::Result);
+};
+
+DNSClientImpl::DNSClientImpl(D2UpdateMessagePtr& response_placeholder,
+                             DNSClient::Callback* callback,
+                             const DNSClient::Protocol proto)
+    : in_buf_(new OutputBuffer(DEFAULT_BUFFER_SIZE)),
+      response_(response_placeholder), callback_(callback), proto_(proto) {
+
+    // @todo Currently we only support UDP. The support for TCP is planned for
+    // the future release.
+    if (proto_ == DNSClient::TCP) {
+        isc_throw(isc::NotImplemented, "TCP is currently not supported as a"
+                  << " Transport protocol for DNS Updates; please use UDP");
+    }
+
+    // Given that we already eliminated the possibility that TCP is used, it
+    // would be sufficient  to check that (proto != DNSClient::UDP). But, once
+    // support TCP is added the check above will disappear and the extra check
+    // will be needed here anyway.
+    // Note that cascaded check is used here instead of:
+    //   if (proto_ != DNSClient::TCP && proto_ != DNSClient::UDP)..
+    // because some versions of GCC compiler complain that check above would
+    // be always 'false' due to limited range of enumeration. In fact, it is
+    // possible to pass out of range integral value through enum and it should
+    // be caught here.
+    if (proto_ != DNSClient::TCP) {
+        if (proto_ != DNSClient::UDP) {
+            isc_throw(isc::NotImplemented, "invalid transport protocol type '"
+                      << proto_ << "' specified for DNS Updates");
+        }
+    }
+
+    if (!response_) {
+        isc_throw(BadValue, "a pointer to an object to encapsulate the DNS"
+                  " server must be provided; found NULL value");
+
+    }
+}
+
+DNSClientImpl::~DNSClientImpl() {
+}
+
+void
+DNSClientImpl::operator()(asiodns::IOFetch::Result result) {
+    // Get the status from IO. If no success, we just call user's callback
+    // and pass the status code.
+    DNSClient::Status status = getStatus(result);
+    if (status == DNSClient::SUCCESS) {
+        InputBuffer response_buf(in_buf_->getData(), in_buf_->getLength());
+        // Server's response may be corrupted. In such case, fromWire will
+        // throw an exception. We want to catch this exception to return
+        // appropriate status code to the caller and log this event.
+        try {
+            response_->fromWire(response_buf);
+
+        } catch (const Exception& ex) {
+            status = DNSClient::INVALID_RESPONSE;
+            LOG_DEBUG(dctl_logger, DBGLVL_TRACE_DETAIL,
+                      DHCP_DDNS_INVALID_RESPONSE).arg(ex.what());
+
+        }
+    }
+
+    // Once we are done with internal business, let's call a callback supplied
+    // by a caller.
+    if (callback_ != NULL) {
+        (*callback_)(status);
+    }
+}
+
+DNSClient::Status
+DNSClientImpl::getStatus(const asiodns::IOFetch::Result result) {
+    switch (result) {
+    case IOFetch::SUCCESS:
+        return (DNSClient::SUCCESS);
+
+    case IOFetch::TIME_OUT:
+        return (DNSClient::TIMEOUT);
+
+    case IOFetch::STOPPED:
+        return (DNSClient::IO_STOPPED);
+
+    default:
+        ;
+    }
+    return (DNSClient::OTHER);
+}
+
+void
+DNSClientImpl::doUpdate(IOService& io_service,
+                        const IOAddress& ns_addr,
+                        const uint16_t ns_port,
+                        D2UpdateMessage& update,
+                        const unsigned int wait) {
+    // A renderer is used by the toWire function which creates the on-wire data
+    // from the DNS Update message. A renderer has its internal buffer where it
+    // renders data by default. However, this buffer can't be directly accessed.
+    // Fortunately, the renderer's API accepts user-supplied buffers. So, let's
+    // create our own buffer and pass it to the renderer so as the message is
+    // rendered to this buffer. Finally, we pass this buffer to IOFetch.
+    dns::MessageRenderer renderer;
+    OutputBufferPtr msg_buf(new OutputBuffer(DEFAULT_BUFFER_SIZE));
+    renderer.setBuffer(msg_buf.get());
+
+    // Render DNS Update message. This may throw a bunch of exceptions if
+    // invalid message object is given.
+    update.toWire(renderer);
+
+    // IOFetch has all the mechanisms that we need to perform asynchronous
+    // communication with the DNS server. The last but one argument points to
+    // this object as a completion callback for the message exchange. As a
+    // result operator()(Status) will be called.
+
+    // Timeout value is explicitly cast to the int type to avoid warnings about
+    // overflows when doing implicit cast. It should have been checked by the
+    // caller that the unsigned timeout value will fit into int.
+    IOFetch io_fetch(IOFetch::UDP, io_service, msg_buf, ns_addr, ns_port,
+                     in_buf_, this, static_cast<int>(wait));
+    // Post the task to the task queue in the IO service. Caller will actually
+    // run these tasks by executing IOService::run.
+    io_service.post(io_fetch);
+}
+
+
+DNSClient::DNSClient(D2UpdateMessagePtr& response_placeholder,
+                     Callback* callback, const DNSClient::Protocol proto)
+    : impl_(new DNSClientImpl(response_placeholder, callback, proto)) {
+}
+
+DNSClient::~DNSClient() {
+    delete (impl_);
+}
+
+unsigned int
+DNSClient::getMaxTimeout() {
+    static const unsigned int max_timeout = std::numeric_limits<int>::max();
+    return (max_timeout);
+}
+
+void
+DNSClient::doUpdate(IOService&,
+                    const IOAddress&,
+                    const uint16_t,
+                    D2UpdateMessage&,
+                    const unsigned int,
+                    const dns::TSIGKey&) {
+    isc_throw(isc::NotImplemented, "TSIG is currently not supported for"
+              "DNS Update message");
+}
+
+void
+DNSClient::doUpdate(IOService& io_service,
+                    const IOAddress& ns_addr,
+                    const uint16_t ns_port,
+                    D2UpdateMessage& update,
+                    const unsigned int wait) {
+    // The underlying implementation which we use to send DNS Updates uses
+    // signed integers for timeout. If we want to avoid overflows we need to
+    // respect this limitation here.
+    if (wait > getMaxTimeout()) {
+        isc_throw(isc::BadValue, "A timeout value for DNS Update request must"
+                  " not exceed " << getMaxTimeout()
+                  << ". Provided timeout value is '" << wait << "'");
+    }
+    impl_->doUpdate(io_service, ns_addr, ns_port, update, wait);
+}
+
+
+
+} // namespace d2
+} // namespace isc
+

+ 192 - 0
src/bin/d2/dns_client.h

@@ -0,0 +1,192 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or 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 ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC 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.
+
+#ifndef DNS_CLIENT_H
+#define DNS_CLIENT_H
+
+#include <d2/d2_update_message.h>
+
+#include <asiolink/io_service.h>
+#include <util/buffer.h>
+
+#include <asiodns/io_fetch.h>
+#include <dns/tsig.h>
+
+namespace isc {
+namespace d2 {
+
+class DNSClient;
+typedef boost::shared_ptr<DNSClient> DNSClientPtr;
+
+/// DNSClient class implementation.
+class DNSClientImpl;
+
+/// @brief The @c DNSClient class handles communication with the DNS server.
+///
+/// Communication with the DNS server is asynchronous. Caller must provide a
+/// callback, which will be invoked when the response from the DNS server is
+/// received, a timeout has occurred or IO service has been stopped for any
+/// reason. The caller-supplied callback is called by the internal callback
+/// operator implemented by @c DNSClient. This callback is responsible for
+/// initializing the @c D2UpdateMessage instance which encapsulates the response
+/// from the DNS. This initialization does not take place if the response from
+/// DNS is not received.
+///
+/// Caller must supply a pointer to the @c D2UpdateMessage object, which will
+/// encapsulate DNS response, through class constructor. An exception will be
+/// thrown if the pointer is not initialized by the caller.
+///
+/// @todo Ultimately, this class will support both TCP and UDP Transport.
+/// Currently only UDP is supported and can be specified as a preferred
+/// protocol. @c DNSClient constructor will throw an exception if TCP is
+/// specified. Once both protocols are supported, the @c DNSClient logic will
+/// try to obey caller's preference. However, it may use the other protocol if
+/// on its own discretion, when there is a legitimate reason to do so. For
+/// example, if communication with the server using preferred protocol fails.
+class DNSClient {
+public:
+
+    /// @brief Transport layer protocol used by a DNS Client to communicate
+    /// with a server.
+    enum Protocol {
+        UDP,
+        TCP
+    };
+
+    /// @brief A status code of the DNSClient.
+    enum Status {
+        SUCCESS,           ///< Response received and is ok.
+        TIMEOUT,           ///< No response, timeout.
+        IO_STOPPED,        ///< IO was stopped.
+        INVALID_RESPONSE,  ///< Response received but invalid.
+        OTHER              ///< Other, unclassified error.
+    };
+
+    /// @brief Callback for the @c DNSClient class.
+    ///
+    /// This is is abstract class which represents the external callback for the
+    /// @c DNSClient. Caller must implement this class and supply its instance
+    /// in the @c DNSClient constructor to get callbacks when the DNS Update
+    /// exchange is complete (@see @c DNSClient).
+    class Callback {
+    public:
+        /// @brief Virtual destructor.
+        virtual ~Callback() { }
+
+        /// @brief Function operator implementing a callback.
+        ///
+        /// @param status a @c DNSClient::Status enum representing status code
+        /// of DNSClient operation.
+        virtual void operator()(DNSClient::Status status) = 0;
+    };
+
+    /// @brief Constructor.
+    ///
+    /// @param response_placeholder Pointer to an object which will hold a
+    /// DNS server's response. Caller is responsible for allocating this object.
+    /// @param callback Pointer to an object implementing @c DNSClient::Callback
+    /// class. This object will be called when DNS message exchange completes or
+    /// if an error occurs. NULL value disables callback invocation.
+    /// @param proto caller's preference regarding Transport layer protocol to
+    /// be used by DNS Client to communicate with a server.
+    DNSClient(D2UpdateMessagePtr& response_placeholder, Callback* callback,
+              const Protocol proto = UDP);
+
+    /// @brief Virtual destructor, does nothing.
+    ~DNSClient();
+
+    ///
+    /// @name Copy constructor and assignment operator
+    ///
+    /// Copy constructor and assignment operator are private because there are
+    /// no use cases when @DNSClient instance will need to be copied. Also, it
+    /// is desired to avoid copying @DNSClient::impl_ pointer and external
+    /// callbacks.
+    ///
+    //@{
+private:
+    DNSClient(const DNSClient& source);
+    DNSClient& operator=(const DNSClient& source);
+    //@}
+
+public:
+
+    /// @brief Returns maximal allowed timeout value accepted by
+    /// @c DNSClient::doUpdate.
+    ///
+    /// @return maximal allowed timeout value accepted by @c DNSClient::doUpdate
+    static unsigned int getMaxTimeout();
+
+    /// @brief Start asynchronous DNS Update with TSIG.
+    ///
+    /// This function starts asynchronous DNS Update and returns. The DNS Update
+    /// will be executed by the specified IO service. Once the message exchange
+    /// with a DNS server is complete, timeout occurs or IO operation is
+    /// interrupted, the caller-supplied callback function will be invoked.
+    ///
+    /// An address and port of the DNS server is specified through the function
+    /// arguments so as the same instance of the @c DNSClient can be used to
+    /// initiate multiple message exchanges.
+    ///
+    /// @param io_service IO service to be used to run the message exchange.
+    /// @param ns_addr DNS server address.
+    /// @param ns_port DNS server port.
+    /// @param update A DNS Update message to be sent to the server.
+    /// @param wait A timeout (in seconds) for the response. If a response is
+    /// not received within the timeout, exchange is interrupted. This value
+    /// must not exceed maximal value for 'int' data type.
+    /// @param tsig_key An @c isc::dns::TSIGKey object representing TSIG
+    /// context which will be used to render the DNS Update message.
+    ///
+    /// @todo Implement TSIG Support. Currently any attempt to call this
+    /// function will result in exception.
+    void doUpdate(asiolink::IOService& io_service,
+                  const asiolink::IOAddress& ns_addr,
+                  const uint16_t ns_port,
+                  D2UpdateMessage& update,
+                  const unsigned int wait,
+                  const dns::TSIGKey& tsig_key);
+
+    /// @brief Start asynchronous DNS Update without TSIG.
+    ///
+    /// This function starts asynchronous DNS Update and returns. The DNS Update
+    /// will be executed by the specified IO service. Once the message exchange
+    /// with a DNS server is complete, timeout occurs or IO operation is
+    /// interrupted, the caller-supplied callback function will be invoked.
+    ///
+    /// An address and port of the DNS server is specified through the function
+    /// arguments so as the same instance of the @c DNSClient can be used to
+    /// initiate multiple message exchanges.
+    ///
+    /// @param io_service IO service to be used to run the message exchange.
+    /// @param ns_addr DNS server address.
+    /// @param ns_port DNS server port.
+    /// @param update A DNS Update message to be sent to the server.
+    /// @param wait A timeout (in seconds) for the response. If a response is
+    /// not received within the timeout, exchange is interrupted. This value
+    /// must not exceed maximal value for 'int' data type.
+    void doUpdate(asiolink::IOService& io_service,
+                  const asiolink::IOAddress& ns_addr,
+                  const uint16_t ns_port,
+                  D2UpdateMessage& update,
+                  const unsigned int wait);
+
+private:
+    DNSClientImpl* impl_;  ///< Pointer to DNSClient implementation.
+};
+
+} // namespace d2
+} // namespace isc
+
+#endif // DNS_CLIENT_H

+ 2 - 1
src/bin/d2/main.cc

@@ -38,7 +38,8 @@ int main(int argc, char* argv[]) {
     // Launch the controller passing in command line arguments.
     // Exit program with the controller's return code.
     try  {
-        controller->launch(argc, argv);
+        // 'false' value disables test mode.
+        controller->launch(argc, argv, false);
     } catch (const isc::Exception& ex) {
         std::cerr << "Service failed:" << ex.what() << std::endl;
         ret = EXIT_FAILURE;

+ 3 - 0
src/bin/d2/tests/Makefile.am

@@ -61,6 +61,7 @@ d2_unittests_SOURCES += ../d2_config.cc ../d2_config.h
 d2_unittests_SOURCES += ../d2_cfg_mgr.cc ../d2_cfg_mgr.h
 d2_unittests_SOURCES += ../d2_update_message.cc ../d2_update_message.h
 d2_unittests_SOURCES += ../d2_zone.cc ../d2_zone.h
+d2_unittests_SOURCES += ../dns_client.cc ../dns_client.h
 d2_unittests_SOURCES += ../ncr_msg.cc ../ncr_msg.h
 d2_unittests_SOURCES += d_test_stubs.cc d_test_stubs.h
 d2_unittests_SOURCES += d2_unittests.cc
@@ -71,6 +72,7 @@ d2_unittests_SOURCES += d_cfg_mgr_unittests.cc
 d2_unittests_SOURCES += d2_cfg_mgr_unittests.cc
 d2_unittests_SOURCES += d2_update_message_unittests.cc
 d2_unittests_SOURCES += d2_zone_unittests.cc
+d2_unittests_SOURCES += dns_client_unittests.cc
 d2_unittests_SOURCES += ncr_unittests.cc
 nodist_d2_unittests_SOURCES = ../d2_messages.h ../d2_messages.cc
 
@@ -79,6 +81,7 @@ d2_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)
 d2_unittests_LDADD = $(GTEST_LDADD)
 d2_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
 d2_unittests_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
+d2_unittests_LDADD += $(top_builddir)/src/lib/asiodns/libb10-asiodns.la
 d2_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
 d2_unittests_LDADD += $(top_builddir)/src/lib/cc/libb10-cc.la
 d2_unittests_LDADD += $(top_builddir)/src/lib/config/libb10-cfgclient.la

+ 1 - 1
src/bin/d2/tests/d_test_stubs.h

@@ -409,7 +409,7 @@ public:
     /// DControllerBase::launch for details.
     void launch(int argc, char* argv[]) {
         optind = 1;
-        getController()->launch(argc, argv);
+        getController()->launch(argc, argv, true);
     }
 
     /// @Wrapper to invoke the Controller's disconnectSession method.  Please

+ 408 - 0
src/bin/d2/tests/dns_client_unittests.cc

@@ -0,0 +1,408 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or 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 ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC 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.
+
+#include <config.h>
+#include <d2/dns_client.h>
+#include <asiodns/io_fetch.h>
+#include <asiodns/logger.h>
+#include <asiolink/interval_timer.h>
+#include <dns/rcode.h>
+#include <dns/rrclass.h>
+#include <dns/tsig.h>
+#include <asio/ip/udp.hpp>
+#include <asio/socket_base.hpp>
+#include <boost/bind.hpp>
+#include <boost/scoped_ptr.hpp>
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::asiodns;
+using namespace isc::d2;
+
+using namespace isc;
+using namespace isc::dns;
+using namespace isc::util;
+using namespace asio;
+using namespace asio::ip;
+
+namespace {
+
+const char* TEST_ADDRESS = "127.0.0.1";
+const uint16_t TEST_PORT = 5301;
+const size_t MAX_SIZE = 1024;
+const long TEST_TIMEOUT = 5 * 1000;
+
+// @brief Test Fixture class.
+//
+// This test fixture class implements DNSClient::Callback so as it can be
+// installed as a completion callback for tests it implements. This callback
+// is called when a DDNS transaction (send and receive) completes. This allows
+// for the callback function to directly access class members. In particular,
+// the callback function can access IOService on which run() was called and
+// call stop() on it.
+//
+// Many of the tests defined here schedule execution of certain tasks and block
+// until tasks are completed or a timeout is hit. However, if timeout is not
+// properly handled a task may be hanging for a long time. In order to prevent
+// it, the asiolink::IntervalTimer is used to break a running test if test
+// timeout is hit. This will result in test failure.
+class DNSClientTest : public virtual ::testing::Test, DNSClient::Callback {
+public:
+    IOService service_;
+    D2UpdateMessagePtr response_;
+    DNSClient::Status status_;
+    uint8_t receive_buffer_[MAX_SIZE];
+    DNSClientPtr dns_client_;
+    bool corrupt_response_;
+    bool expect_response_;
+    asiolink::IntervalTimer test_timer_;
+
+    // @brief Constructor.
+    //
+    // This constructor overrides the default logging level of asiodns logger to
+    // prevent it from emitting debug messages from IOFetch class. Such an error
+    // message can be emitted if timeout occurs when DNSClient class is
+    // waiting for a response. Some of the tests are checking DNSClient behavior
+    // in case when response from the server is not received. Tests output would
+    // become messy if such errors were logged.
+    DNSClientTest()
+        : service_(),
+          status_(DNSClient::SUCCESS),
+          corrupt_response_(false),
+          expect_response_(true),
+          test_timer_(service_) {
+        asiodns::logger.setSeverity(isc::log::INFO);
+        response_.reset(new D2UpdateMessage(D2UpdateMessage::INBOUND));
+        dns_client_.reset(new DNSClient(response_, this));
+
+        // Set the test timeout to break any running tasks if they hang.
+        test_timer_.setup(boost::bind(&DNSClientTest::testTimeoutHandler, this),
+                          TEST_TIMEOUT);
+    }
+
+    // @brief Destructor.
+    //
+    // Sets the asiodns logging level back to DEBUG.
+    virtual ~DNSClientTest() {
+        asiodns::logger.setSeverity(isc::log::DEBUG);
+    };
+
+    // @brief Exchange completion callback.
+    //
+    // This callback is called when the exchange with the DNS server is
+    // complete or an error occurred. This includes the occurrence of a timeout.
+    //
+    // @param status A status code returned by DNSClient.
+    virtual void operator()(DNSClient::Status status) {
+        status_ = status;
+        service_.stop();
+
+        if (expect_response_) {
+            if (!corrupt_response_) {
+                // We should have received a response.
+                EXPECT_EQ(DNSClient::SUCCESS, status_);
+
+                ASSERT_TRUE(response_);
+                EXPECT_EQ(D2UpdateMessage::RESPONSE, response_->getQRFlag());
+                ASSERT_EQ(1,
+                          response_->getRRCount(D2UpdateMessage::SECTION_ZONE));
+                D2ZonePtr zone = response_->getZone();
+                ASSERT_TRUE(zone);
+                EXPECT_EQ("example.com.", zone->getName().toText());
+                EXPECT_EQ(RRClass::IN().getCode(), zone->getClass().getCode());
+
+            } else {
+                EXPECT_EQ(DNSClient::INVALID_RESPONSE, status_);
+
+            }
+        // If we don't expect a response, the status should indicate a timeout.
+        } else {
+            EXPECT_EQ(DNSClient::TIMEOUT, status_);
+
+        }
+    }
+
+    // @brief Handler invoked when test timeout is hit.
+    //
+    // This callback stops all running (hanging) tasks on IO service.
+    void testTimeoutHandler() {
+        service_.stop();
+        FAIL() << "Test timeout hit.";
+    }
+
+    // @brief Handler invoked when test request is received.
+    //
+    // This callback handler is installed when performing async read on a
+    // socket to emulate reception of the DNS Update request by a server.
+    // As a result, this handler will send an appropriate DNS Update response
+    // message back to the address from which the request has come.
+    //
+    // @param socket A pointer to a socket used to receive a query and send a
+    // response.
+    // @param remote A pointer to an object which specifies the host (address
+    // and port) from which a request has come.
+    // @param receive_length A length (in bytes) of the received data.
+    // @param corrupt_response A bool value which indicates that the server's
+    // response should be invalid (true) or valid (false)
+    void udpReceiveHandler(udp::socket* socket, udp::endpoint* remote,
+                           size_t receive_length, const bool corrupt_response) {
+        // The easiest way to create a response message is to copy the entire
+        // request.
+        OutputBuffer response_buf(receive_length);
+        response_buf.writeData(receive_buffer_, receive_length);
+        // If a response is to be valid, we have to modify it slightly. If not,
+        // we leave it as is.
+        if (!corrupt_response) {
+            // For a valid response the QR bit must be set. This bit
+            // differentiates both types of messages. Note that the 3rd byte of
+            // the message header comprises this bit in the front followed by
+            // the message code and reserved zeros. Therefore, this byte
+            // has the following value:
+            //             10101000,
+            // where a leading bit is a QR flag. The hexadecimal value is 0xA8.
+            // Write it at message offset 2.
+            response_buf.writeUint8At(0xA8, 2);
+        }
+        // A response message is now ready to send. Send it!
+        socket->send_to(asio::buffer(response_buf.getData(),
+                                     response_buf.getLength()),
+                        *remote);
+    }
+
+    // This test verifies that when invalid response placeholder object is
+    // passed to a constructor, constructor throws the appropriate exception.
+    // It also verifies that the constructor will not throw if the supplied
+    // callback object is NULL.
+    void runConstructorTest() {
+        D2UpdateMessagePtr null_response;
+        EXPECT_THROW(DNSClient(null_response, this, DNSClient::UDP),
+                     isc::BadValue);
+        EXPECT_NO_THROW(DNSClient(response_, NULL, DNSClient::UDP));
+
+        // The TCP Transport is not supported right now. So, we return exception
+        // if caller specified TCP as a preferred protocol. This test will be
+        // removed once TCP is supported.
+        EXPECT_THROW(DNSClient(response_, NULL, DNSClient::TCP),
+                     isc::NotImplemented);
+    }
+
+    // This test verifies that it accepted timeout values belong to the range of
+    // <0, DNSClient::getMaxTimeout()>.
+    void runInvalidTimeoutTest() {
+
+        expect_response_ = false;
+
+        // Create outgoing message. Simply set the required message fields:
+        // error code and Zone section. This is enough to create on-wire format
+        // of this message and send it.
+        D2UpdateMessage message(D2UpdateMessage::OUTBOUND);
+        ASSERT_NO_THROW(message.setRcode(Rcode(Rcode::NOERROR_CODE)));
+        ASSERT_NO_THROW(message.setZone(Name("example.com"), RRClass::IN()));
+
+        // Start with a valid timeout equal to maximal allowed. This way we will
+        // ensure that doUpdate doesn't throw an exception for valid timeouts.
+        unsigned int timeout = DNSClient::getMaxTimeout();
+        EXPECT_NO_THROW(dns_client_->doUpdate(service_, IOAddress(TEST_ADDRESS),
+                                           TEST_PORT, message, timeout));
+
+        // Cross the limit and expect that exception is thrown this time.
+        timeout = DNSClient::getMaxTimeout() + 1;
+        EXPECT_THROW(dns_client_->doUpdate(service_, IOAddress(TEST_ADDRESS),
+                                           TEST_PORT, message, timeout),
+                     isc::BadValue);
+    }
+
+    // This test verifies that isc::NotImplemented exception is thrown when
+    // attempt to send DNS Update message with TSIG is attempted.
+    void runTSIGTest() {
+        // Create outgoing message. Simply set the required message fields:
+        // error code and Zone section. This is enough to create on-wire format
+        // of this message and send it.
+        D2UpdateMessage message(D2UpdateMessage::OUTBOUND);
+        ASSERT_NO_THROW(message.setRcode(Rcode(Rcode::NOERROR_CODE)));
+        ASSERT_NO_THROW(message.setZone(Name("example.com"), RRClass::IN()));
+
+        const int timeout = 0;
+        // Try to send DNS Update with TSIG key. Currently TSIG is not supported
+        // and therefore we expect an exception.
+        TSIGKey tsig_key("key.example:MSG6Ng==");
+        EXPECT_THROW(dns_client_->doUpdate(service_, IOAddress(TEST_ADDRESS),
+                                           TEST_PORT, message, timeout,
+                                           tsig_key),
+                     isc::NotImplemented);
+    }
+
+    // This test verifies the DNSClient behavior when a server does not respond
+    // do the DNS Update message. In such case, the callback function is
+    // expected to be called and the TIME_OUT error code should be returned.
+    void runSendNoReceiveTest() {
+        // We expect no response from a server.
+        expect_response_ = false;
+
+        // Create outgoing message. Simply set the required message fields:
+        // error code and Zone section. This is enough to create on-wire format
+        // of this message and send it.
+        D2UpdateMessage message(D2UpdateMessage::OUTBOUND);
+        ASSERT_NO_THROW(message.setRcode(Rcode(Rcode::NOERROR_CODE)));
+        ASSERT_NO_THROW(message.setZone(Name("example.com"), RRClass::IN()));
+
+        // Set the response wait time to 0 so as our test is not hanging. This
+        // should cause instant timeout.
+        const int timeout = 0;
+        // The doUpdate() function starts asynchronous message exchange with DNS
+        // server. When message exchange is done or timeout occurs, the
+        // completion callback will be triggered. The doUpdate function returns
+        // immediately.
+        EXPECT_NO_THROW(dns_client_->doUpdate(service_, IOAddress(TEST_ADDRESS),
+                                             TEST_PORT, message, timeout));
+
+        // This starts the execution of tasks posted to IOService. run() blocks
+        // until stop() is called in the completion callback function.
+        service_.run();
+
+    }
+
+    // This test verifies that DNSClient can send DNS Update and receive a
+    // corresponding response from a server.
+    void runSendReceiveTest(const bool corrupt_response,
+                            const bool two_sends) {
+        corrupt_response_ = corrupt_response;
+
+        // Create a request DNS Update message.
+        D2UpdateMessage message(D2UpdateMessage::OUTBOUND);
+        ASSERT_NO_THROW(message.setRcode(Rcode(Rcode::NOERROR_CODE)));
+        ASSERT_NO_THROW(message.setZone(Name("example.com"), RRClass::IN()));
+
+        // In order to perform the full test, when the client sends the request
+        // and receives a response from the server, we have to emulate the
+        // server's response in the test. A request will be sent via loopback
+        // interface to 127.0.0.1 and known test port. Response must be sent
+        // to 127.0.0.1 and a source port which has been used to send the
+        // request. A new socket is created, specifically to handle sending
+        // responses. The reuse address option is set so as both sockets can
+        // use the same address. This new socket is bound to the test address
+        // and port, where requests will be sent.
+        udp::socket udp_socket(service_.get_io_service(), asio::ip::udp::v4());
+        udp_socket.set_option(socket_base::reuse_address(true));
+        udp_socket.bind(udp::endpoint(address::from_string(TEST_ADDRESS),
+                                      TEST_PORT));
+        // Once socket is created, we can post an IO request to receive some
+        // a packet from this socket. This is asynchronous operation and
+        // nothing is received until another IO request to send a query is
+        // posted and the run() is invoked on this IO. A callback function is
+        // attached to this asynchronous read. This callback function requires
+        // that a socket object used to receive the request is passed to it,
+        // because the same socket will be used by the callback function to send
+        // a response. Also, the remote object is passed to the callback,
+        // because it holds a source address and port where request originated.
+        // Callback function will send a response to this address and port.
+        // The last parameter holds a length of the received request. It is
+        // required to construct a response.
+        udp::endpoint remote;
+        udp_socket.async_receive_from(asio::buffer(receive_buffer_,
+                                                   sizeof(receive_buffer_)),
+                                      remote,
+                                      boost::bind(&DNSClientTest::udpReceiveHandler,
+                                                  this, &udp_socket, &remote, _2,
+                                                  corrupt_response));
+
+        // The socket is now ready to receive the data. Let's post some request
+        // message then.
+        const int timeout = 5;
+        dns_client_->doUpdate(service_, IOAddress(TEST_ADDRESS), TEST_PORT,
+                             message, timeout);
+
+        // It is possible to request that two packets are sent concurrently.
+        if (two_sends) {
+            dns_client_->doUpdate(service_, IOAddress(TEST_ADDRESS), TEST_PORT,
+                                  message, timeout);
+
+        }
+
+        // Kick of the message exchange by actually running the scheduled
+        // "send" and "receive" operations.
+        service_.run();
+
+        udp_socket.close();
+
+    }
+};
+
+// Verify that the DNSClient object can be created if provided parameters are
+// valid. Constructor should throw exceptions when parameters are invalid.
+TEST_F(DNSClientTest, constructor) {
+    runConstructorTest();
+}
+
+// This test verifies that the maximal allowed timeout value is maximal int
+// value.
+TEST_F(DNSClientTest, getMaxTimeout) {
+    EXPECT_EQ(std::numeric_limits<int>::max(), DNSClient::getMaxTimeout());
+}
+
+// Verify that timeout is reported when no response is received from DNS.
+TEST_F(DNSClientTest, timeout) {
+    runSendNoReceiveTest();
+}
+
+// Verify that exception is thrown when invalid (too high) timeout value is
+// specified for asynchronous DNS Update.
+TEST_F(DNSClientTest, invalidTimeout) {
+    runInvalidTimeoutTest();
+}
+
+// Verify that exception is thrown when an attempt to send DNS Update with TSIG
+// is made. This test will be removed/changed once TSIG support is added.
+TEST_F(DNSClientTest, runTSIGTest) {
+    runTSIGTest();
+}
+
+// Verify that the DNSClient receives the response from DNS and the received
+// buffer can be decoded as DNS Update Response.
+TEST_F(DNSClientTest, sendReceive) {
+    // false means that server response is not corrupted.
+    runSendReceiveTest(false, false);
+}
+
+// Verify that the DNSClient reports an error when the response is received from
+// a DNS and this response is corrupted.
+TEST_F(DNSClientTest, sendReceiveCurrupted) {
+    // true means that server's response is corrupted.
+    runSendReceiveTest(true, false);
+}
+
+// Verify that it is possible to use the same DNSClient instance to
+// perform the following sequence of message exchanges:
+// 1. send
+// 2. receive
+// 3. send
+// 4. receive
+TEST_F(DNSClientTest, sendReceiveTwice) {
+    runSendReceiveTest(false, false);
+    runSendReceiveTest(false, false);
+}
+
+// Verify that it is possible to use the DNSClient instance to perform the
+// following  sequence of message exchanges:
+// 1. send
+// 2. send
+// 3. receive
+// 4. receive
+TEST_F(DNSClientTest, concurrentSendReceive) {
+    runSendReceiveTest(false, true);
+}
+
+} // End of anonymous namespace