Browse Source

[master] Merge branch 'trac3035'

Marcin Siodelski 11 years ago
parent
commit
f617e6af8c

+ 1 - 0
doc/devel/mainpage.dox

@@ -54,6 +54,7 @@
  *   - @subpage dhcpv4ConfigParser
  *   - @subpage dhcpv4ConfigInherit
  *   - @subpage dhcpv4OptionsParse
+ *   - @subpage dhcpv4DDNSIntegration
  *   - @subpage dhcpv4Other
  * - @subpage dhcp6
  *   - @subpage dhcpv6Session

+ 1 - 0
src/bin/dhcp4/Makefile.am

@@ -59,6 +59,7 @@ nodist_b10_dhcp4_SOURCES = dhcp4_messages.h dhcp4_messages.cc
 EXTRA_DIST += dhcp4_messages.mes
 
 b10_dhcp4_LDADD  = $(top_builddir)/src/lib/dhcp/libb10-dhcp++.la
+b10_dhcp4_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libb10-dhcp_ddns.la
 b10_dhcp4_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
 b10_dhcp4_LDADD += $(top_builddir)/src/lib/dhcpsrv/libb10-dhcpsrv.la
 b10_dhcp4_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la

+ 72 - 1
src/bin/dhcp4/dhcp4.dox

@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2013  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-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
@@ -84,6 +84,77 @@ counterpart. See \ref dhcpv6ConfigInherit.
 The DHCPv4 server uses the same logic to supply custom callback function to
 parse message option as DHCPv6 server implementation. See \ref dhcpv6OptionsParse.
 
+@section dhcpv4DDNSIntegration DHCPv4 Server Support for the Dynamic DNS Updates
+T
+he DHCPv4 server supports processing of the DHCPv4 Client FQDN option (RFC4702)
+and the DHCPv4 Host Name option (RFC2132). Client may send one of these options
+to convey its fully qualified or partial name to the server. The server may use
+this name to perform DNS updates for the client. If server receives both options
+in the same message, the DHCPv4 Client FQDN %Option is processed and the Host
+Name option is ignored. If only Host Name Option is present in the client's
+message, it is used to update DNS.
+
+Server may be configured to use a different name to perform DNS update for the
+client. In this case the server will return one of the DHCPv4 Client FQDN or
+Host Name %Option in its response with the name which was selected for the
+client to indicate that this name will be used to perform DNS update.
+
+The b10-dhcp-ddns process is responsible for the actual communication with the
+DNS, i.e. to send DNS update messages. The b10-dhcp4 module is responsible for
+generating @ref isc::dhcp_ddns::NameChangeRequest and sending it to
+the b10-dhcp-ddns module. The @ref isc::dhcp_ddns::NameChangeRequest object
+represents changes to the DNS bindings, related to acquisition, renewal or
+release of the DHCP lease. The b10-dhcp4 module implements the simple FIFO queue
+of the NameChangeRequest objects. The module logic, which processes the incoming
+DHCPv4 Client FQDN and Host Name Options puts these requests into the FIFO queue.
+
+@todo Currently the FIFO queue is not processed after the NameChangeRequests are
+generated and added to it. In the future implementation steps it is planned to
+create a code which will check if there are any outstanding requests in the queue
+and send them to the b10-dhcp-ddns module when server is idle waiting for DHCP
+messages.
+
+When client gets an address from the server, a DHCPv4 server may generate 0, 1
+or 2 NameChangeRequests during single message processing. Server generates no
+NameChangeRequests if it is not configured to update DNS or it rejects the DNS
+update for any other reason.
+
+Server may generate 1 NameChangeRequest in a case when client acquired a new
+lease or it releases an existing lease. In the former case, the NameChangeRequest
+type is CHG_ADD, which indicates that the b10-dhcp-ddns module should add a new
+DNS binding for the client, and it is assumed that there is no DNS binding for
+this client already. In the latter case, the NameChangeRequest type is CHG_REMOVE
+to indicate to the b10-dhcp-ddns module that an existing DNS binding should be
+removed from the DNS. The binding consists of the forward and reverse mapping.
+The server may only remove the mapping which it had added. Therefore, the lease
+database holds the information which updates (no update, reverse only update,
+forward only update or both reverse and forward update) have been performed when
+the lease was acquired or renewed. Server checks this information to make a
+decision which mapping it is supposed to remove when lease is released.
+
+Server may generate 2 NameChangeRequests in case a client is renewing a lease and
+it already has a DNS binding for that lease. The DHCPv4 server will check if
+there is an existing lease for the client which has sent a message and if DNS
+Updates had been performed for this lease. If the notion of client's FQDN changes,
+comparing to the information stored in the lease database, the DHCPv4 has to
+remove an existing binding from the DNS and then add a new binding according to
+the new FQDN information received from the client. If the client's FQDN
+information (including the client's name and type of update performed) doesn't
+change comparing to the NameChangeRequest is not generated.
+
+The DHCPv4 Client FQDN %Option comprises flags which communicate to the server
+what updates (if any) client expects the server to perform. Server may be
+configured to obey client's preference or to do FQDN processing in a different way.
+If the server overrides client's preference it will communicate it by sending
+the DHCPv4 Client FQDN %Option in its responses to a client, with the appropriate
+flags set.
+
+@todo Note, that current implementation doesn't allow configuration of the
+server's behaviour with respect to DNS Updates. This is planned for the future.
+The default behaviour is constituted by the set of constants defined in the
+(upper part of) dhcp4_srv.cc file. Once the configuration is implemented,
+these constants will be removed.
+
 @section dhcpv4Other Other DHCPv4 topics
 
  For hooks API support in DHCPv4, see @ref dhcpv4Hooks.

+ 34 - 0
src/bin/dhcp4/dhcp4_messages.mes

@@ -27,6 +27,11 @@ successfully established a session with the BIND 10 control channel.
 This debug message is issued just before the IPv4 DHCP server attempts
 to establish a session with the BIND 10 control channel.
 
+% DHCP4_CLIENT_NAME_PROC_FAIL failed to process the fqdn or hostname sent by a client: %1
+This debug message is issued when the DHCP server was unable to process the
+FQDN or Hostname option sent by a client. This is likely because the client's
+name was malformed or due to internal server error.
+
 % DHCP4_COMMAND_RECEIVED received command %1, arguments: %2
 A debug message listing the command (and possible arguments) received
 from the BIND 10 control system by the IPv4 DHCP server.
@@ -70,6 +75,16 @@ This message is printed when DHCPv4 server disables an interface from being
 used to receive DHCPv4 traffic. Sockets on this interface will not be opened
 by the Interface Manager until interface is enabled.
 
+% DHCP4_DHCID_COMPUTE_ERROR failed to compute the DHCID for lease: %1, reason: %2
+This error message is logged when the attempt to compute DHCID for a specified
+lease has failed. The lease details and reason for failure is logged in the
+message.
+
+% DHCP4_EMPTY_HOSTNAME received empty hostname from the client, skipping processing of this option
+This debug message is issued when the server received an empty Hostname option
+from a client. Server does not process empty Hostname options and therefore
+option is skipped.
+
 % DHCP4_HOOK_BUFFER_RCVD_SKIP received DHCPv4 buffer was dropped because a callout set the skip flag.
 This debug message is printed when a callout installed on buffer4_receive
 hook point set the skip flag. For this particular hook point, the
@@ -134,6 +149,19 @@ specified client after receiving a REQUEST message from it.  There are many
 possible reasons for such a failure. Additional messages will indicate the
 reason.
 
+% DHCP4_NAME_GEN_UPDATE_FAIL failed to update the lease after generating name for a client: %1
+This message indicates the failure when trying to update the lease and/or
+options in the server's response with the hostname generated by the server
+from the acquired address. The message argument indicates the reason for the
+failure.
+
+% DHCP4_NCR_CREATION_FAILED failed to generate name change requests for DNS: %1
+This message indicates that server was unable to generate NameChangeRequests
+which should be sent to the b10-dhcp_ddns module to create
+new DNS records for the lease being acquired or to update existing records
+for the renewed lease. The reason for the failure is printed in the logged
+message.
+
 % DHCP4_NO_SOCKETS_OPEN no interface configured to listen to DHCP traffic
 This warning message is issued when current server configuration specifies
 no interfaces that server should listen on, or specified interfaces are not
@@ -211,6 +239,12 @@ failure is given in the message.
 % DHCP4_QUERY_DATA received packet type %1, data is <%2>
 A debug message listing the data received from the client.
 
+% DHCP4_QUEUE_NCR name change request to %1 DNS entry queued: %2
+A debug message which is logged when the NameChangeRequest to add or remove
+a DNS entries for a particular lease has been queued. The first parameter of
+this log message indicates whether the DNS entry is to be added or removed.
+The second parameter carries the details of the NameChangeRequest.
+
 % DHCP4_RELEASE address %1 belonging to client-id %2, hwaddr %3 was released properly.
 This debug message indicates that an address was released properly. It
 is a normal operation during client shutdown.

+ 439 - 6
src/bin/dhcp4/dhcp4_srv.cc

@@ -35,6 +35,7 @@
 #include <dhcpsrv/utils.h>
 #include <hooks/callout_handle.h>
 #include <hooks/hooks_manager.h>
+#include <util/strutil.h>
 
 #include <boost/algorithm/string/erase.hpp>
 #include <boost/bind.hpp>
@@ -46,6 +47,7 @@
 using namespace isc;
 using namespace isc::asiolink;
 using namespace isc::dhcp;
+using namespace isc::dhcp_ddns;
 using namespace isc::hooks;
 using namespace isc::log;
 using namespace std;
@@ -79,6 +81,36 @@ Dhcp4Hooks Hooks;
 namespace isc {
 namespace dhcp {
 
+namespace {
+
+// @todo The following constants describe server's behavior with respect to the
+// DHCPv4 Client FQDN Option sent by a client. They will be removed
+// when DDNS parameters for DHCPv4 are implemented with the ticket #3033.
+
+// @todo Additional configuration parameter which we may consider is the one
+// that controls whether the DHCP server sends the removal NameChangeRequest
+// if it discovers that the entry for the particular client exists or that
+// it always updates the DNS.
+
+// Should server always include the FQDN option in its response, regardless
+// if it has been requested in Parameter Request List Option (Disabled).
+const bool FQDN_ALWAYS_INCLUDE = false;
+// Enable A RR update delegation to the client (Disabled).
+const bool FQDN_ALLOW_CLIENT_UPDATE = false;
+// Globally enable updates (Enabled).
+const bool FQDN_ENABLE_UPDATE = true;
+// Do update, even if client requested no updates with N flag (Disabled).
+const bool FQDN_OVERRIDE_NO_UPDATE = false;
+// Server performs an update when client requested delegation (Enabled).
+const bool FQDN_OVERRIDE_CLIENT_UPDATE = true;
+// The fully qualified domain-name suffix if partial name provided by
+// a client.
+const char* FQDN_PARTIAL_SUFFIX = "example.com";
+// Should server replace the domain-name supplied by the client (Disabled).
+const bool FQDN_REPLACE_CLIENT_NAME = false;
+
+}
+
 /// @brief file name of a server-id file
 ///
 /// Server must store its server identifier in persistent storage that must not
@@ -442,6 +474,9 @@ Dhcpv4Srv::run() {
             LOG_ERROR(dhcp4_logger, DHCP4_PACKET_SEND_FAIL)
                 .arg(e.what());
         }
+
+        // Send NameChangeRequests to the b10-dhcp_ddns module.
+        sendNameChangeRequests();
     }
 
     return (true);
@@ -551,6 +586,52 @@ Dhcpv4Srv::srvidToString(const OptionPtr& srvid) {
     return (addrs[0].toText());
 }
 
+isc::dhcp_ddns::D2Dhcid
+Dhcpv4Srv::computeDhcid(const Lease4Ptr& lease) {
+    if (!lease) {
+        isc_throw(DhcidComputeError, "a pointer to the lease must be not"
+                  " NULL to compute DHCID");
+
+    } else if (lease->hostname_.empty()) {
+        isc_throw(DhcidComputeError, "unable to compute the DHCID for the"
+                  " lease which has empty hostname set");
+
+    }
+
+    // In order to compute DHCID the client's hostname must be encoded in
+    // canonical wire format. It is unlikely that the FQDN is malformed
+    // because it is validated by the classes which encapsulate options
+    // carrying client FQDN. However, if the client name was carried in the
+    // Hostname option it is more likely as it carries the hostname as a
+    // regular string.
+    std::vector<uint8_t> fqdn_wire;
+    try {
+        OptionDataTypeUtil::writeFqdn(lease->hostname_, fqdn_wire, true);
+
+    } catch (const Exception& ex) {
+        isc_throw(DhcidComputeError, "unable to compute DHCID because the"
+                  " hostname: " << lease->hostname_ << " is invalid");
+
+    }
+
+    // Prefer client id to HW address to compute DHCID. If Client Id is
+    // NULL, use HW address.
+    try {
+        if (lease->client_id_) {
+            return (D2Dhcid(lease->client_id_->getClientId(), fqdn_wire));
+
+        } else {
+            HWAddrPtr hwaddr(new HWAddr(lease->hwaddr_, HTYPE_ETHER));
+            return (D2Dhcid(hwaddr, fqdn_wire));
+        }
+    } catch (const Exception& ex) {
+        isc_throw(DhcidComputeError, "unable to compute DHCID: "
+                  << ex.what());
+
+    }
+
+}
+
 void
 Dhcpv4Srv::copyDefaultFields(const Pkt4Ptr& question, Pkt4Ptr& answer) {
     answer->setIface(question->getIface());
@@ -739,6 +820,265 @@ Dhcpv4Srv::appendBasicOptions(const Pkt4Ptr& question, Pkt4Ptr& msg) {
 }
 
 void
+Dhcpv4Srv::processClientName(const Pkt4Ptr& query, Pkt4Ptr& answer) {
+    // It is possible that client has sent both Client FQDN and Hostname
+    // option. In such case, server should prefer Client FQDN option and
+    // ignore the Hostname option.
+    try {
+        Option4ClientFqdnPtr fqdn = boost::dynamic_pointer_cast<Option4ClientFqdn>
+            (query->getOption(DHO_FQDN));
+        if (fqdn) {
+            processClientFqdnOption(fqdn, answer);
+
+        } else {
+            OptionCustomPtr hostname = boost::dynamic_pointer_cast<OptionCustom>
+                (query->getOption(DHO_HOST_NAME));
+            if (hostname) {
+                processHostnameOption(hostname, answer);
+            }
+
+        }
+    } catch (const Exception& ex) {
+        // In some rare cases it is possible that the client's name processing
+        // fails. For example, the Hostname option may be malformed, or there
+        // may be an error in the server's logic which would cause multiple
+        // attempts to add the same option to the response message. This
+        // error message aggregates all these errors so they can be diagnosed
+        // from the log. We don't want to throw an exception here because,
+        // it will impact the processing of the whole packet. We rather want
+        // the processing to continue, even if the client's name is wrong.
+        LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL_DATA, DHCP4_CLIENT_NAME_PROC_FAIL)
+            .arg(ex.what());
+    }
+}
+
+void
+Dhcpv4Srv::processClientFqdnOption(const Option4ClientFqdnPtr& fqdn,
+                                   Pkt4Ptr& answer) {
+    // Create the DHCPv4 Client FQDN Option to be included in the server's
+    // response to a client.
+    Option4ClientFqdnPtr fqdn_resp(new Option4ClientFqdn(*fqdn));
+
+    // RFC4702, section 4 - set 'NOS' flags to 0.
+    fqdn_resp->setFlag(Option4ClientFqdn::FLAG_S, 0);
+    fqdn_resp->setFlag(Option4ClientFqdn::FLAG_O, 0);
+    fqdn_resp->setFlag(Option4ClientFqdn::FLAG_N, 0);
+
+    // Conditions when N flag has to be set to indicate that server will not
+    // perform DNS updates:
+    // 1. Updates are globally disabled,
+    // 2. Client requested no update and server respects it,
+    // 3. Client requested that the foward DNS update is delegated to the client
+    //    but server neither respects requests for forward update delegation nor
+    //    it is configured to send update on its own when client requested
+    //    delegation.
+    if (!FQDN_ENABLE_UPDATE ||
+        (fqdn->getFlag(Option4ClientFqdn::FLAG_N) &&
+         !FQDN_OVERRIDE_NO_UPDATE) ||
+        (!fqdn->getFlag(Option4ClientFqdn::FLAG_S) &&
+         !FQDN_ALLOW_CLIENT_UPDATE && !FQDN_OVERRIDE_CLIENT_UPDATE)) {
+        fqdn_resp->setFlag(Option4ClientFqdn::FLAG_N, true);
+
+    // Conditions when S flag is set to indicate that server will perform DNS
+    // update on its own:
+    // 1. Client requested that server performs DNS update and DNS updates are
+    //    globally enabled.
+    // 2. Client requested that server delegates forward update to the client
+    //    but server doesn't respect requests for delegation and it is
+    // configured to perform an update on its own when client requested the
+    // delegation.
+    } else  if (fqdn->getFlag(Option4ClientFqdn::FLAG_S) ||
+                (!fqdn->getFlag(Option4ClientFqdn::FLAG_S) &&
+                 !FQDN_ALLOW_CLIENT_UPDATE && FQDN_OVERRIDE_CLIENT_UPDATE)) {
+        fqdn_resp->setFlag(Option4ClientFqdn::FLAG_S, true);
+    }
+
+    // Server MUST set the O flag if it has overriden the client's setting
+    // of S flag.
+    if (fqdn->getFlag(Option4ClientFqdn::FLAG_S) !=
+        fqdn_resp->getFlag(Option4ClientFqdn::FLAG_S)) {
+        fqdn_resp->setFlag(Option4ClientFqdn::FLAG_O, true);
+    }
+
+    // If client suppled partial or empty domain-name, server should generate
+    // one.
+    if (fqdn->getDomainNameType() == Option4ClientFqdn::PARTIAL) {
+        std::ostringstream name;
+        if (fqdn->getDomainName().empty() || FQDN_REPLACE_CLIENT_NAME) {
+            fqdn_resp->setDomainName("", Option4ClientFqdn::PARTIAL);
+
+        } else {
+            name << fqdn->getDomainName();
+            name << "." << FQDN_PARTIAL_SUFFIX;
+            fqdn_resp->setDomainName(name.str(), Option4ClientFqdn::FULL);
+
+        }
+
+    // Server may be configured to replace a name supplied by a client, even if
+    // client supplied fully qualified domain-name. The empty domain-name is
+    // is set to indicate that the name must be generated when the new lease
+    // is acquired.
+    } else if(FQDN_REPLACE_CLIENT_NAME) {
+        fqdn_resp->setDomainName("", Option4ClientFqdn::PARTIAL);
+    }
+
+    // Add FQDN option to the response message. Note that, there may be some
+    // cases when server may choose not to include the FQDN option in a
+    // response to a client. In such cases, the FQDN should be removed from the
+    // outgoing message. In theory we could cease to include the FQDN option
+    // in this function until it is confirmed that it should be included.
+    // However, we include it here for simplicity. Functions used to acquire
+    // lease for a client will scan the response message for FQDN and if it
+    // is found they will take necessary actions to store the FQDN information
+    // in the lease database as well as to generate NameChangeRequests to DNS.
+    // If we don't store the option in the reponse message, we will have to
+    // propagate it in the different way to the functions which acquire the
+    // lease. This would require modifications to the API of this class.
+    answer->addOption(fqdn_resp);
+}
+
+void
+Dhcpv4Srv::processHostnameOption(const OptionCustomPtr& opt_hostname,
+                                 Pkt4Ptr& answer) {
+    // Do nothing if the DNS updates are disabled.
+    if (!FQDN_ENABLE_UPDATE) {
+        return;
+    }
+
+    std::string hostname = isc::util::str::trim(opt_hostname->readString());
+    unsigned int label_count = OptionDataTypeUtil::getLabelCount(hostname);
+    // The hostname option sent by the client should be at least 1 octet long.
+    // If it isn't we ignore this option.
+    if (label_count == 0) {
+        LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL_DATA, DHCP4_EMPTY_HOSTNAME);
+        return;
+    }
+    // Copy construct the hostname provided by the client. It is entirely
+    // possible that we will use the hostname option provided by the client
+    // to perform the DNS update and we will send the same option to him to
+    // indicate that we accepted this hostname.
+    OptionCustomPtr opt_hostname_resp(new OptionCustom(*opt_hostname));
+
+    // The hostname option may be unqualified or fully qualified. The lab_count
+    // holds the number of labels for the name. The number of 1 means that
+    // there is only root label "." (even for unqualified names, as the
+    // getLabelCount function treats each name as a fully qualified one).
+    // By checking the number of labels present in the hostname we may infer
+    // whether client has sent the fully qualified or unqualified hostname.
+
+    // @todo We may want to reconsider whether it is appropriate for the
+    // client to send a root domain name as a Hostname. There are
+    // also extensions to the auto generation of the client's name,
+    // e.g. conversion to the puny code which may be considered at some point.
+    // For now, we just remain liberal and expect that the DNS will handle
+    // conversion if needed and possible.
+    if (FQDN_REPLACE_CLIENT_NAME || (label_count < 2)) {
+        opt_hostname_resp->writeString("");
+    // If there are two labels, it means that the client has specified
+    // the unqualified name. We have to concatenate the unqalified name
+    // with the domain name.
+    } else if (label_count == 2) {
+        std::ostringstream resp_hostname;
+        resp_hostname << hostname << "." << FQDN_PARTIAL_SUFFIX << ".";
+        opt_hostname_resp->writeString(resp_hostname.str());
+    }
+
+    answer->addOption(opt_hostname_resp);
+}
+
+void
+Dhcpv4Srv::createNameChangeRequests(const Lease4Ptr& lease,
+                                    const Lease4Ptr& old_lease) {
+    if (!lease) {
+        isc_throw(isc::Unexpected,
+                  "NULL lease specified when creating NameChangeRequest");
+    }
+
+    // If old lease is not NULL, it is an indication that the lease has
+    // just been renewed. In such case we may need to generate the
+    // additional NameChangeRequest to remove an existing entry before
+    // we create a NameChangeRequest to add the entry for an updated lease.
+    // We may also decide not to generate any requests at all. This is when
+    // we discover that nothing has changed in the client's FQDN data.
+    if (old_lease) {
+        if (!lease->matches(*old_lease)) {
+            isc_throw(isc::Unexpected,
+                      "there is no match between the current instance of the"
+                      " lease: " << lease->toText() << ", and the previous"
+                      " instance: " << lease->toText());
+        } else {
+            // There will be a NameChangeRequest generated to remove existing
+            // DNS entries if the following conditions are met:
+            // - The hostname is set for the existing lease, we can't generate
+            //   removal request for non-existent hostname.
+            // - A server has performed reverse, forward or both updates.
+            // - FQDN data between the new and old lease do not match.
+            if  ((lease->hostname_ != old_lease->hostname_) ||
+                 (lease->fqdn_fwd_ != old_lease->fqdn_fwd_) ||
+                 (lease->fqdn_rev_ != old_lease->fqdn_rev_)) {
+                queueNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE,
+                                       old_lease);
+
+            // If FQDN data from both leases match, there is no need to update.
+            } else if ((lease->hostname_ == old_lease->hostname_) &&
+                       (lease->fqdn_fwd_ == old_lease->fqdn_fwd_) &&
+                       (lease->fqdn_rev_ == old_lease->fqdn_rev_)) {
+                return;
+            }
+
+        }
+    }
+
+    // We may need to generate the NameChangeRequest for the new lease. It
+    // will be generated only if hostname is set and if forward or reverse
+    // update has been requested.
+    queueNameChangeRequest(isc::dhcp_ddns::CHG_ADD, lease);
+}
+
+void
+Dhcpv4Srv::
+queueNameChangeRequest(const isc::dhcp_ddns::NameChangeType chg_type,
+                       const Lease4Ptr& lease) {
+    // The hostname must not be empty, and at least one type of update
+    // should be requested.
+    if (!lease || lease->hostname_.empty() ||
+        (!lease->fqdn_rev_ && !lease->fqdn_fwd_)) {
+        return;
+    }
+
+    // Create the DHCID for the NameChangeRequest.
+    D2Dhcid dhcid;
+    try {
+        dhcid  = computeDhcid(lease);
+    } catch (const DhcidComputeError& ex) {
+        LOG_ERROR(dhcp4_logger, DHCP4_DHCID_COMPUTE_ERROR)
+            .arg(lease->toText())
+            .arg(ex.what());
+        return;
+    }
+    // Create NameChangeRequest
+    NameChangeRequest ncr(chg_type, lease->fqdn_fwd_, lease->fqdn_rev_,
+                          lease->hostname_, lease->addr_.toText(),
+                          dhcid, lease->cltt_ + lease->valid_lft_,
+                          lease->valid_lft_);
+    // And queue it.
+    LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL_DATA, DHCP4_QUEUE_NCR)
+        .arg(chg_type == CHG_ADD ? "add" : "remove")
+        .arg(ncr.toText());
+    name_change_reqs_.push(ncr);
+}
+
+void
+Dhcpv4Srv::sendNameChangeRequests() {
+    while (!name_change_reqs_.empty()) {
+        // @todo Once next NameChangeRequest is picked from the queue
+        // we should send it to the b10-dhcp_ddns module. Currently we
+        // just drop it.
+        name_change_reqs_.pop();
+    }
+}
+
+void
 Dhcpv4Srv::assignLease(const Pkt4Ptr& question, Pkt4Ptr& answer) {
 
     // We need to select a subnet the client is connected in.
@@ -794,6 +1134,28 @@ Dhcpv4Srv::assignLease(const Pkt4Ptr& question, Pkt4Ptr& answer) {
 
     CalloutHandlePtr callout_handle = getCalloutHandle(question);
 
+    std::string hostname;
+    bool fqdn_fwd = false;
+    bool fqdn_rev = false;
+    OptionCustomPtr opt_hostname;
+    Option4ClientFqdnPtr fqdn = boost::dynamic_pointer_cast<
+        Option4ClientFqdn>(answer->getOption(DHO_FQDN));
+    if (fqdn) {
+        hostname = fqdn->getDomainName();
+        fqdn_fwd = fqdn->getFlag(Option4ClientFqdn::FLAG_S);
+        fqdn_rev = !fqdn->getFlag(Option4ClientFqdn::FLAG_N);
+    } else {
+        opt_hostname = boost::dynamic_pointer_cast<OptionCustom>
+            (answer->getOption(DHO_HOST_NAME));
+        if (opt_hostname) {
+            hostname = opt_hostname->readString();
+            // @todo It could be configurable what sort of updates the server
+            // is doing when Hostname option was sent.
+            fqdn_fwd = true;
+            fqdn_rev = true;
+        }
+    }
+
     // Use allocation engine to pick a lease for this client. Allocation engine
     // will try to honour the hint, but it is just a hint - some other address
     // may be used instead. If fake_allocation is set to false, the lease will
@@ -801,7 +1163,8 @@ Dhcpv4Srv::assignLease(const Pkt4Ptr& question, Pkt4Ptr& answer) {
     // @todo pass the actual FQDN data.
     Lease4Ptr old_lease;
     Lease4Ptr lease = alloc_engine_->allocateLease4(subnet, client_id, hwaddr,
-                                                    hint, false, false, "",
+                                                      hint, fqdn_fwd, fqdn_rev,
+                                                      hostname,
                                                     fake_allocation,
                                                     callout_handle,
                                                     old_lease);
@@ -817,6 +1180,42 @@ Dhcpv4Srv::assignLease(const Pkt4Ptr& question, Pkt4Ptr& answer) {
 
         answer->setYiaddr(lease->addr_);
 
+        // If there has been Client FQDN or Hostname option sent, but the
+        // hostname is empty, it means that server is responsible for
+        // generating the entire hostname for the client. The example of the
+        // client's name, generated from the IP address is: host-192-0-2-3.
+        if ((fqdn || opt_hostname) && lease->hostname_.empty()) {
+            hostname = lease->addr_.toText();
+            // Replace dots with hyphens.
+            std::replace(hostname.begin(), hostname.end(), '.', '-');
+            ostringstream stream;
+            // The partial suffix will need to be replaced with the actual
+            // domain-name for the client when configuration is implemented.
+            stream << "host-" << hostname << "." << FQDN_PARTIAL_SUFFIX << ".";
+            lease->hostname_ = stream.str();
+            // The operations below are rather safe, but we want to catch
+            // any potential exceptions (e.g. invalid lease database backend
+            // implementation) and log an error.
+            try {
+                // The lease update should be safe, because the lease should
+                // be already in the database. In most cases the exception
+                // would be thrown if the lease was missing.
+                LeaseMgrFactory::instance().updateLease4(lease);
+                // The name update in the option should be also safe,
+                // because the generated name is well formed.
+                if (fqdn) {
+                    fqdn->setDomainName(lease->hostname_,
+                                        Option4ClientFqdn::FULL);
+                } else if (opt_hostname) {
+                    opt_hostname->writeString(lease->hostname_);
+                }
+
+            } catch (const Exception& ex) {
+                LOG_ERROR(dhcp4_logger, DHCP4_NAME_GEN_UPDATE_FAIL)
+                    .arg(ex.what());
+            }
+        }
+
         // IP Address Lease time (type 51)
         opt = OptionPtr(new Option(Option::V4, DHO_DHCP_LEASE_TIME));
         opt->setUint32(lease->valid_lft_);
@@ -828,6 +1227,21 @@ Dhcpv4Srv::assignLease(const Pkt4Ptr& question, Pkt4Ptr& answer) {
         // @todo: send renew timer option (T1, option 58)
         // @todo: send rebind timer option (T2, option 59)
 
+        // @todo Currently the NameChangeRequests are always generated if
+        // real (not fake) allocation is being performed. Should we have
+        // control switch to enable/disable NameChangeRequest creation?
+        // Perhaps we need a way to detect whether the b10-dhcp-ddns module
+        // is up an running?
+        if (!fake_allocation) {
+            try {
+                createNameChangeRequests(lease, old_lease);
+            } catch (const Exception& ex) {
+                LOG_ERROR(dhcp4_logger, DHCP4_NCR_CREATION_FAILED)
+                    .arg(ex.what());
+            }
+
+        }
+
     } else {
         // Allocation engine did not allocate a lease. The engine logged
         // cause of that failure. The only thing left is to insert
@@ -841,6 +1255,9 @@ Dhcpv4Srv::assignLease(const Pkt4Ptr& question, Pkt4Ptr& answer) {
 
         answer->setType(DHCPNAK);
         answer->setYiaddr(IOAddress("0.0.0.0"));
+
+        answer->delOption(DHO_FQDN);
+        answer->delOption(DHO_HOST_NAME);
     }
 }
 
@@ -919,6 +1336,12 @@ Dhcpv4Srv::processDiscover(Pkt4Ptr& discover) {
     copyDefaultFields(discover, offer);
     appendDefaultOptions(offer, DHCPOFFER);
 
+    // If DISCOVER message contains the FQDN or Hostname option, server
+    // may respond to the client with the appropriate FQDN or Hostname
+    // option to indicate that whether it will take responsibility for
+    // updating DNS when the client sends REQUEST message.
+    processClientName(discover, offer);
+
     assignLease(discover, offer);
 
     // Adding any other options makes sense only when we got the lease.
@@ -946,6 +1369,12 @@ Dhcpv4Srv::processRequest(Pkt4Ptr& request) {
     copyDefaultFields(request, ack);
     appendDefaultOptions(ack, DHCPACK);
 
+    // If REQUEST message contains the FQDN or Hostname option, server
+    // should respond to the client with the appropriate FQDN or Hostname
+    // option to indicate if it takes responsibility for the DNS updates.
+    // This is performed by the function below.
+    processClientName(request, ack);
+
     // Note that we treat REQUEST message uniformly, regardless if this is a
     // first request (requesting for new address), renewing existing address
     // or even rebinding.
@@ -979,12 +1408,12 @@ Dhcpv4Srv::processRelease(Pkt4Ptr& release) {
 
     try {
         // Do we have a lease for that particular address?
-        Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(release->getYiaddr());
+        Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(release->getCiaddr());
 
         if (!lease) {
             // No such lease - bogus release
             LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL, DHCP4_RELEASE_FAIL_NO_LEASE)
-                .arg(release->getYiaddr().toText())
+                .arg(release->getCiaddr().toText())
                 .arg(release->getHWAddr()->toText())
                 .arg(client_id ? client_id->toText() : "(no client-id)");
             return;
@@ -995,7 +1424,7 @@ Dhcpv4Srv::processRelease(Pkt4Ptr& release) {
         if (lease->hwaddr_ != release->getHWAddr()->hwaddr_) {
             // @todo: Print hwaddr from lease as part of ticket #2589
             LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL, DHCP4_RELEASE_FAIL_WRONG_HWADDR)
-                .arg(release->getYiaddr().toText())
+                .arg(release->getCiaddr().toText())
                 .arg(client_id ? client_id->toText() : "(no client-id)")
                 .arg(release->getHWAddr()->toText());
             return;
@@ -1005,7 +1434,7 @@ Dhcpv4Srv::processRelease(Pkt4Ptr& release) {
         // the client sent us.
         if (lease->client_id_ && client_id && *lease->client_id_ != *client_id) {
             LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL, DHCP4_RELEASE_FAIL_WRONG_CLIENT_ID)
-                .arg(release->getYiaddr().toText())
+                .arg(release->getCiaddr().toText())
                 .arg(client_id->toText())
                 .arg(lease->client_id_->toText());
             return;
@@ -1045,11 +1474,15 @@ Dhcpv4Srv::processRelease(Pkt4Ptr& release) {
             bool success = LeaseMgrFactory::instance().deleteLease(lease->addr_);
 
             if (success) {
-                // Release successful - we're done here
+                // Release successful
                 LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL, DHCP4_RELEASE)
                     .arg(lease->addr_.toText())
                     .arg(client_id ? client_id->toText() : "(no client-id)")
                     .arg(release->getHWAddr()->toText());
+
+                // Remove existing DNS entries for the lease, if any.
+                queueNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, lease);
+
             } else {
                 // Release failed -
                 LOG_ERROR(dhcp4_logger, DHCP4_RELEASE_FAIL)

+ 150 - 0
src/bin/dhcp4/dhcp4_srv.h

@@ -18,6 +18,9 @@
 #include <dhcp/dhcp4.h>
 #include <dhcp/pkt4.h>
 #include <dhcp/option.h>
+#include <dhcp/option4_client_fqdn.h>
+#include <dhcp/option_custom.h>
+#include <dhcp_ddns/ncr_msg.h>
 #include <dhcpsrv/subnet.h>
 #include <dhcpsrv/alloc_engine.h>
 #include <hooks/callout_handle.h>
@@ -25,10 +28,18 @@
 #include <boost/noncopyable.hpp>
 
 #include <iostream>
+#include <queue>
 
 namespace isc {
 namespace dhcp {
 
+/// @brief Exception thrown when DHCID computation failed.
+class DhcidComputeError : public isc::Exception {
+public:
+    DhcidComputeError(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
 /// @brief DHCPv4 server service.
 ///
 /// This singleton class represents DHCPv4 server. It contains all
@@ -262,6 +273,117 @@ protected:
     /// @param msg the message to add options to.
     void appendBasicOptions(const Pkt4Ptr& question, Pkt4Ptr& msg);
 
+    /// @brief Processes Client FQDN and Hostname Options sent by a client.
+    ///
+    /// Client may send Client FQDN or Hostname option to communicate its name
+    /// to the server. Server may use this name to perform DNS update for the
+    /// lease being assigned to a client. If server takes responsibility for
+    /// updating DNS for a client it may communicate it by sending the Client
+    /// FQDN or Hostname %Option back to the client. Server select a different
+    /// name than requested by a client to update DNS. In such case, the server
+    /// stores this different name in its response.
+    ///
+    /// Client should not send both Client FQDN and Hostname options. However,
+    /// if client sends both options, server should prefer Client FQDN option
+    /// and ignore the Hostname option. If Client FQDN option is not present,
+    /// the Hostname option is processed.
+    ///
+    /// The Client FQDN %Option is processed by this function as described in
+    /// RFC4702.
+    ///
+    /// In response to a Hostname %Option sent by a client, the server may send
+    /// Hostname option with the same or different hostname. If different
+    /// hostname is sent, it is an indication to the client that server has
+    /// overridden the client's preferred name and will rather use this
+    /// different name to update DNS. However, since Hostname option doesn't
+    /// carry an information whether DNS update will be carried by the server
+    /// or not, the client is responsible for checking whether DNS update
+    /// has been performed.
+    ///
+    /// After successful processing options stored in the first parameter,
+    /// this function may add Client FQDN or Hostname option to the response
+    /// message. In some cases, server may cease to add any options to the
+    /// response, i.e. when server doesn't support DNS updates.
+    ///
+    /// This function does not throw. It simply logs the debug message if the
+    /// processing of the FQDN or Hostname failed.
+    ///
+    /// @param query A DISCOVER or REQUEST message from a cient.
+    /// @param [out] answer A response message to be sent to a client.
+    void processClientName(const Pkt4Ptr& query, Pkt4Ptr& answer);
+
+private:
+    /// @brief Process Client FQDN %Option sent by a client.
+    ///
+    /// This function is called by the @c Dhcpv4Srv::processClientName when
+    /// the client has sent the FQDN option in its message to the server.
+    /// It comprises the actual logic to parse the FQDN option and prepare
+    /// the FQDN option to be sent back to the client in the server's
+    /// response.
+    ///
+    /// @param fqdn An DHCPv4 Client FQDN %Option sent by a client.
+    /// @param [out] answer A response message to be sent to a client.
+    void processClientFqdnOption(const Option4ClientFqdnPtr& fqdn,
+                                 Pkt4Ptr& answer);
+
+    /// @brief Process Hostname %Option sent by a client.
+    ///
+    /// This function is called by the @c DHcpv4Srv::processClientName when
+    /// the client has sent the Hostname option in its message to the server.
+    /// It comprises the actual logic to parse the Hostname option and
+    /// prepare the Hostname option to be sent back to the client in the
+    /// server's response.
+    ///
+    /// @param opt_hostname An @c OptionCustom object encapsulating the Hostname
+    /// %Option.
+    /// @param [out] answer A response message to be sent to a client.
+    void processHostnameOption(const OptionCustomPtr& opt_hostname,
+                               Pkt4Ptr& answer);
+
+protected:
+
+    /// @brief Creates NameChangeRequests which correspond to the lease
+    /// which has been acquired.
+    ///
+    /// If this function is called whe an existing lease is renewed, it
+    /// may generate NameChangeRequest to remove existing DNS entries which
+    /// correspond to the old lease instance. This function may cease to
+    /// generate NameChangeRequests if the notion of the client's FQDN hasn't
+    /// changed between an old and new lease.
+    ///
+    /// @param lease A pointer to the new lease which has been acquired.
+    /// @param old_lease A pointer to the instance of the old lease which has
+    /// been replaced by the new lease passed in the first argument. The NULL
+    /// value indicates that the new lease has been allocated, rather than
+    /// lease being renewed.
+    void createNameChangeRequests(const Lease4Ptr& lease,
+                                  const Lease4Ptr& old_lease);
+
+    /// @brief Creates the NameChangeRequest and adds to the queue for
+    /// processing.
+    ///
+    /// This function adds the @c isc::dhcp_ddns::NameChangeRequest to the
+    /// queue and emits the debug message which indicates whether the request
+    /// being added is to remove DNS entry or add a new entry. This function
+    /// is exception free.
+    ///
+    /// @param chg_type A type of the NameChangeRequest (ADD or REMOVE).
+    /// @param lease A lease for which the NameChangeRequest is created and
+    /// queued.
+    void queueNameChangeRequest(const isc::dhcp_ddns::NameChangeType chg_type,
+                                const Lease4Ptr& lease);
+
+    /// @brief Sends all outstanding NameChangeRequests to b10-dhcp-ddns module.
+    ///
+    /// The purpose of this function is to pick all outstanding
+    /// NameChangeRequests from the FIFO queue and send them to b10-dhcp-ddns
+    /// module.
+    ///
+    /// @todo Currently this function simply removes all requests from the
+    /// queue but doesn't send them anywhere. In the future, the
+    /// NameChangeSender will be used to deliver requests to the other module.
+    void sendNameChangeRequests();
+
     /// @brief Attempts to renew received addresses
     ///
     /// Attempts to renew existing lease. This typically includes finding a lease that
@@ -336,6 +458,28 @@ protected:
     /// @return string representation
     static std::string srvidToString(const OptionPtr& opt);
 
+    /// @brief Computes DHCID from a lease.
+    ///
+    /// This method creates an object which represents DHCID. The DHCID value
+    /// is computed as described in RFC4701. The section 3.3. of RFC4701
+    /// specifies the DHCID RR Identifier Type codes:
+    /// - 0x0000 The 1 octet htype followed by glen octets of chaddr
+    /// - 0x0001 The data octets from the DHCPv4 client's Client Identifier
+    /// option.
+    /// - 0x0002 The client's DUID.
+    ///
+    /// Currently this function supports first two of these identifiers.
+    /// The 0x0001 is preferred over the 0x0000 - if the client identifier
+    /// option is present, the former is used. If the client identifier
+    /// is absent, the latter is used.
+    ///
+    /// @todo Implement support for 0x0002 DHCID identifier type.
+    ///
+    /// @param lease A pointer to the structure describing a lease.
+    /// @return An object encapsulating DHCID to be used for DNS updates.
+    /// @throw DhcidComputeError If the computation of the DHCID failed.
+    static isc::dhcp_ddns::D2Dhcid computeDhcid(const Lease4Ptr& lease);
+
     /// @brief Selects a subnet for a given client's packet.
     ///
     /// @param question client's message
@@ -394,6 +538,12 @@ private:
     int hook_index_pkt4_receive_;
     int hook_index_subnet4_select_;
     int hook_index_pkt4_send_;
+
+protected:
+
+    /// Holds a list of @c isc::dhcp_ddns::NameChangeRequest objects which
+    /// are waiting for sending  to b10-dhcp-ddns module.
+    std::queue<isc::dhcp_ddns::NameChangeRequest> name_change_reqs_;
 };
 
 }; // namespace isc::dhcp

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

@@ -75,12 +75,14 @@ TESTS += dhcp4_unittests
 dhcp4_unittests_SOURCES = ../dhcp4_srv.h ../dhcp4_srv.cc ../ctrl_dhcp4_srv.cc
 dhcp4_unittests_SOURCES += ../dhcp4_log.h ../dhcp4_log.cc
 dhcp4_unittests_SOURCES += ../config_parser.cc ../config_parser.h
+dhcp4_unittests_SOURCES += dhcp4_test_utils.h
 dhcp4_unittests_SOURCES += dhcp4_unittests.cc
 dhcp4_unittests_SOURCES += dhcp4_srv_unittest.cc
 dhcp4_unittests_SOURCES += dhcp4_test_utils.cc dhcp4_test_utils.h
 dhcp4_unittests_SOURCES += wireshark.cc
 dhcp4_unittests_SOURCES += ctrl_dhcp4_srv_unittest.cc
 dhcp4_unittests_SOURCES += config_parser_unittest.cc
+dhcp4_unittests_SOURCES += fqdn_unittest.cc
 dhcp4_unittests_SOURCES += marker_file.cc
 nodist_dhcp4_unittests_SOURCES = ../dhcp4_messages.h ../dhcp4_messages.cc
 nodist_dhcp4_unittests_SOURCES += marker_file.h test_libraries.h
@@ -92,6 +94,7 @@ dhcp4_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/cc/libb10-cc.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/config/libb10-cfgclient.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libb10-dhcp++.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libb10-dhcp_ddns.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/libb10-dhcpsrv.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/log/libb10-log.la

+ 8 - 109
src/bin/dhcp4/tests/dhcp4_srv_unittest.cc

@@ -55,114 +55,12 @@ using namespace isc::dhcp;
 using namespace isc::data;
 using namespace isc::asiolink;
 using namespace isc::hooks;
+using namespace isc::dhcp::test;
 
-#if 0
 namespace {
 
-class NakedDhcpv4Srv: public Dhcpv4Srv {
-    // "Naked" DHCPv4 server, exposes internal fields
-public:
-
-    /// @brief Constructor.
-    ///
-    /// This constructor disables default modes of operation used by the
-    /// Dhcpv4Srv class:
-    /// - Send/receive broadcast messages through sockets on interfaces
-    /// which support broadcast traffic.
-    /// - Direct DHCPv4 traffic - communication with clients which do not
-    /// have IP address assigned yet.
-    ///
-    /// Enabling these modes requires root privilges so they must be
-    /// disabled for unit testing.
-    ///
-    /// Note, that disabling broadcast options on sockets does not impact
-    /// the operation of these tests because they use local loopback
-    /// interface which doesn't have broadcast capability anyway. It rather
-    /// prevents setting broadcast options on other (broadcast capable)
-    /// sockets which are opened on other interfaces in Dhcpv4Srv constructor.
-    ///
-    /// The Direct DHCPv4 Traffic capability can be disabled here because
-    /// it is tested with PktFilterLPFTest unittest. The tests which belong
-    /// to PktFilterLPFTest can be enabled on demand when root privileges can
-    /// be guaranteed.
-    ///
-    /// @param port port number to listen on; the default value 0 indicates
-    /// that sockets should not be opened.
-    NakedDhcpv4Srv(uint16_t port = 0)
-        : Dhcpv4Srv(port, "type=memfile", false, false) {
-    }
-
-    /// @brief fakes packet reception
-    /// @param timeout ignored
-    ///
-    /// The method receives all packets queued in receive queue, one after
-    /// another. Once the queue is empty, it initiates the shutdown procedure.
-    ///
-    /// See fake_received_ field for description
-    virtual Pkt4Ptr receivePacket(int /*timeout*/) {
-
-        // If there is anything prepared as fake incoming traffic, use it
-        if (!fake_received_.empty()) {
-            Pkt4Ptr pkt = fake_received_.front();
-            fake_received_.pop_front();
-            return (pkt);
-        }
-
-        // If not, just trigger shutdown and return immediately
-        shutdown();
-        return (Pkt4Ptr());
-    }
-
-    /// @brief fake packet sending
-    ///
-    /// Pretend to send a packet, but instead just store it in fake_send_ list
-    /// where test can later inspect server's response.
-    virtual void sendPacket(const Pkt4Ptr& pkt) {
-        fake_sent_.push_back(pkt);
-    }
-
-    /// @brief adds a packet to fake receive queue
-    ///
-    /// See fake_received_ field for description
-    void fakeReceive(const Pkt4Ptr& pkt) {
-        fake_received_.push_back(pkt);
-    }
-
-    virtual ~NakedDhcpv4Srv() {
-    }
-
-    /// @brief packets we pretend to receive
-    ///
-    /// Instead of setting up sockets on interfaces that change between OSes, it
-    /// is much easier to fake packet reception. This is a list of packets that
-    /// we pretend to have received. You can schedule new packets to be received
-    /// using fakeReceive() and NakedDhcpv4Srv::receivePacket() methods.
-    list<Pkt4Ptr> fake_received_;
-
-    list<Pkt4Ptr> fake_sent_;
-
-    using Dhcpv4Srv::adjustRemoteAddr;
-    using Dhcpv4Srv::processDiscover;
-    using Dhcpv4Srv::processRequest;
-    using Dhcpv4Srv::processRelease;
-    using Dhcpv4Srv::processDecline;
-    using Dhcpv4Srv::processInform;
-    using Dhcpv4Srv::getServerID;
-    using Dhcpv4Srv::loadServerID;
-    using Dhcpv4Srv::generateServerID;
-    using Dhcpv4Srv::writeServerID;
-    using Dhcpv4Srv::sanityCheck;
-    using Dhcpv4Srv::srvidToString;
-    using Dhcpv4Srv::unpackOptions;
-};
-#endif
-
 /// dummy server-id file location
-static const char* SRVID_FILE = "server-id-test.txt";
-
-namespace isc {
-namespace dhcp {
-namespace test {
+const char* SRVID_FILE = "server-id-test.txt";
 
 // Sanity check. Verifies that both Dhcpv4Srv and its derived
 // class NakedDhcpv4Srv can be instantiated and destroyed.
@@ -986,7 +884,7 @@ TEST_F(Dhcpv4SrvTest, ReleaseBasic) {
     // Generate client-id also duid_
     Pkt4Ptr rel = Pkt4Ptr(new Pkt4(DHCPRELEASE, 1234));
     rel->setRemoteAddr(addr);
-    rel->setYiaddr(addr);
+    rel->setCiaddr(addr);
     rel->addOption(clientid);
     rel->addOption(srv->getServerID());
     rel->setHWAddr(hw);
@@ -1049,7 +947,7 @@ TEST_F(Dhcpv4SrvTest, ReleaseReject) {
     // Generate client-id also duid_
     Pkt4Ptr rel = Pkt4Ptr(new Pkt4(DHCPRELEASE, 1234));
     rel->setRemoteAddr(addr);
-    rel->setYiaddr(addr);
+    rel->setCiaddr(addr);
     rel->addOption(clientid);
     rel->addOption(srv->getServerID());
     rel->setHWAddr(bogus_hw);
@@ -2734,7 +2632,7 @@ TEST_F(HooksDhcpv4SrvTest, lease4ReleaseSimple) {
     // Generate client-id also duid_
     Pkt4Ptr rel = Pkt4Ptr(new Pkt4(DHCPRELEASE, 1234));
     rel->setRemoteAddr(addr);
-    rel->setYiaddr(addr);
+    rel->setCiaddr(addr);
     rel->addOption(clientid);
     rel->addOption(srv_->getServerID());
     rel->setHWAddr(hw);
@@ -3027,7 +2925,8 @@ TEST_F(Dhcpv4SrvTest, vendorOptionsDocsisDefinitions) {
     ASSERT_EQ(0, rcode_);
 }
 
+}
 
-}; // end of isc::dhcp::test namespace
+    /*I}; // end of isc::dhcp::test namespace
 }; // end of isc::dhcp namespace
-}; // end of isc namespace
+}; // end of isc namespace */

+ 4 - 0
src/bin/dhcp4/tests/dhcp4_test_utils.h

@@ -357,6 +357,9 @@ public:
     using Dhcpv4Srv::processRelease;
     using Dhcpv4Srv::processDecline;
     using Dhcpv4Srv::processInform;
+    using Dhcpv4Srv::processClientName;
+    using Dhcpv4Srv::computeDhcid;
+    using Dhcpv4Srv::createNameChangeRequests;
     using Dhcpv4Srv::getServerID;
     using Dhcpv4Srv::loadServerID;
     using Dhcpv4Srv::generateServerID;
@@ -364,6 +367,7 @@ public:
     using Dhcpv4Srv::sanityCheck;
     using Dhcpv4Srv::srvidToString;
     using Dhcpv4Srv::unpackOptions;
+    using Dhcpv4Srv::name_change_reqs_;
 };
 
 }; // end of isc::dhcp::test namespace

+ 769 - 0
src/bin/dhcp4/tests/fqdn_unittest.cc

@@ -0,0 +1,769 @@
+// 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 <asiolink/io_address.h>
+#include <dhcp/option4_client_fqdn.h>
+#include <dhcp/option_int_array.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <dhcp_ddns/ncr_msg.h>
+
+#include <gtest/gtest.h>
+#include <boost/scoped_ptr.hpp>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::dhcp_ddns;
+
+namespace {
+
+class NameDhcpv4SrvTest : public Dhcpv4SrvTest {
+public:
+    NameDhcpv4SrvTest() : Dhcpv4SrvTest() {
+        srv_ = new NakedDhcpv4Srv(0);
+    }
+    virtual ~NameDhcpv4SrvTest() {
+        delete srv_;
+    }
+
+    // Create a lease to be used by various tests.
+    Lease4Ptr createLease(const isc::asiolink::IOAddress& addr,
+                          const std::string& hostname,
+                          const bool fqdn_fwd,
+                          const bool fqdn_rev) {
+        const uint8_t hwaddr[] = { 0, 1, 2, 3, 4, 5, 6 };
+        Lease4Ptr lease(new Lease4(addr, hwaddr, sizeof(hwaddr),
+                                   &generateClientId()->getData()[0],
+                                   generateClientId()->getData().size(),
+                                   100, 50, 75, time(NULL), subnet_->getID()));
+        // @todo Set this through the Lease4 constructor.
+        lease->hostname_ = hostname;
+        lease->fqdn_fwd_ = fqdn_fwd;
+        lease->fqdn_rev_ = fqdn_rev;
+
+        return (lease);
+    }
+
+    // Create an instance of the DHCPv4 Client FQDN Option.
+    Option4ClientFqdnPtr
+    createClientFqdn(const uint8_t flags,
+                     const std::string& fqdn_name,
+                     Option4ClientFqdn::DomainNameType fqdn_type) {
+        return (Option4ClientFqdnPtr(new Option4ClientFqdn(flags,
+                                                           Option4ClientFqdn::
+                                                           RCODE_CLIENT(),
+                                                           fqdn_name,
+                                                           fqdn_type)));
+   }
+
+    // Create an instance of the Hostname option.
+    OptionCustomPtr
+    createHostname(const std::string& hostname) {
+        OptionDefinition def("hostname", DHO_HOST_NAME, "string");
+        OptionCustomPtr opt_hostname(new OptionCustom(def, Option::V4));
+        opt_hostname->writeString(hostname);
+        return (opt_hostname);
+    }
+
+    // Generates partial hostname from the address. The format of the
+    // generated address is: host-A-B-C-D, where A.B.C.D is an IP
+    // address.
+    std::string generatedNameFromAddress(const IOAddress& addr) {
+        std::string gen_name = addr.toText();
+        std::replace(gen_name.begin(), gen_name.end(), '.', '-');
+        std::ostringstream hostname;
+        hostname << "host-" << gen_name;
+        return (hostname.str());
+    }
+
+    // Get the Client FQDN Option from the given message.
+    Option4ClientFqdnPtr getClientFqdnOption(const Pkt4Ptr& pkt) {
+        return (boost::dynamic_pointer_cast<
+                Option4ClientFqdn>(pkt->getOption(DHO_FQDN)));
+    }
+
+    // get the Hostname option from the given message.
+    OptionCustomPtr getHostnameOption(const Pkt4Ptr& pkt) {
+        return (boost::dynamic_pointer_cast<
+                OptionCustom>(pkt->getOption(DHO_HOST_NAME)));
+    }
+
+    // Create a message holding DHCPv4 Client FQDN Option.
+    Pkt4Ptr generatePktWithFqdn(const uint8_t msg_type,
+                                const uint8_t fqdn_flags,
+                                const std::string& fqdn_domain_name,
+                                Option4ClientFqdn::DomainNameType fqdn_type,
+                                const bool include_prl,
+                                const bool include_clientid = true) {
+        Pkt4Ptr pkt = Pkt4Ptr(new Pkt4(msg_type, 1234));
+        pkt->setRemoteAddr(IOAddress("192.0.2.3"));
+        // For DISCOVER we don't include server id, because client broadcasts
+        // the message to all servers.
+        if (msg_type != DHCPDISCOVER) {
+            pkt->addOption(srv_->getServerID());
+        }
+
+        if (include_clientid) {
+            pkt->addOption(generateClientId());
+        }
+
+        // Create Client FQDN Option with the specified flags and
+        // domain-name.
+        pkt->addOption(createClientFqdn(fqdn_flags, fqdn_domain_name,
+                                        fqdn_type));
+
+        // Control whether or not to request that server returns the FQDN
+        // option. Server may be configured to always return it or return
+        // only in case client requested it.
+        if (include_prl) {
+            OptionUint8ArrayPtr option_prl =
+                OptionUint8ArrayPtr(new OptionUint8Array(Option::V4,
+                                    DHO_DHCP_PARAMETER_REQUEST_LIST));
+            option_prl->addValue(DHO_FQDN);
+        }
+        return (pkt);
+    }
+
+    // Create a message holding a Hostname option.
+    Pkt4Ptr generatePktWithHostname(const uint8_t msg_type,
+                                    const std::string& hostname) {
+
+        Pkt4Ptr pkt = Pkt4Ptr(new Pkt4(msg_type, 1234));
+        pkt->setRemoteAddr(IOAddress("192.0.2.3"));
+        // For DISCOVER we don't include server id, because client broadcasts
+        // the message to all servers.
+        if (msg_type != DHCPDISCOVER) {
+            pkt->addOption(srv_->getServerID());
+        }
+
+        pkt->addOption(generateClientId());
+
+
+        // Create Client FQDN Option with the specified flags and
+        // domain-name.
+        pkt->addOption(createHostname(hostname));
+
+        return (pkt);
+
+    }
+
+    // Test that server generates the appropriate FQDN option in response to
+    // client's FQDN option.
+    void testProcessFqdn(const Pkt4Ptr& query, const uint8_t exp_flags,
+                         const std::string& exp_domain_name,
+                         Option4ClientFqdn::DomainNameType
+                         exp_domain_type = Option4ClientFqdn::FULL) {
+        ASSERT_TRUE(getClientFqdnOption(query));
+
+        Pkt4Ptr answer;
+        if (query->getType() == DHCPDISCOVER) {
+            answer.reset(new Pkt4(DHCPOFFER, 1234));
+
+        } else {
+            answer.reset(new Pkt4(DHCPACK, 1234));
+
+        }
+        ASSERT_NO_THROW(srv_->processClientName(query, answer));
+
+        Option4ClientFqdnPtr fqdn = getClientFqdnOption(answer);
+        ASSERT_TRUE(fqdn);
+
+        const bool flag_n = (exp_flags & Option4ClientFqdn::FLAG_N) != 0;
+        const bool flag_s = (exp_flags & Option4ClientFqdn::FLAG_S) != 0;
+        const bool flag_o = (exp_flags & Option4ClientFqdn::FLAG_O) != 0;
+        const bool flag_e = (exp_flags & Option4ClientFqdn::FLAG_E) != 0;
+
+        EXPECT_EQ(flag_n, fqdn->getFlag(Option4ClientFqdn::FLAG_N));
+        EXPECT_EQ(flag_s, fqdn->getFlag(Option4ClientFqdn::FLAG_S));
+        EXPECT_EQ(flag_o, fqdn->getFlag(Option4ClientFqdn::FLAG_O));
+        EXPECT_EQ(flag_e, fqdn->getFlag(Option4ClientFqdn::FLAG_E));
+
+        EXPECT_EQ(exp_domain_name, fqdn->getDomainName());
+        EXPECT_EQ(exp_domain_type, fqdn->getDomainNameType());
+
+    }
+
+    // Processes the Hostname option in the client's message and returns
+    // the hostname option which would be sent to the client. It will
+    // throw NULL pointer if the hostname option is not to be included
+    // in the response.
+    OptionCustomPtr processHostname(const Pkt4Ptr& query) {
+        if (!getHostnameOption(query)) {
+            ADD_FAILURE() << "Hostname option not carried in the query";
+        }
+
+        Pkt4Ptr answer;
+        if (query->getType() == DHCPDISCOVER) {
+            answer.reset(new Pkt4(DHCPOFFER, 1234));
+
+        } else {
+            answer.reset(new Pkt4(DHCPACK, 1234));
+
+        }
+        srv_->processClientName(query, answer);
+
+        OptionCustomPtr hostname = getHostnameOption(answer);
+        return (hostname);
+
+    }
+
+    // Test that the client message holding an FQDN is processed and the
+    // NameChangeRequests are generated.
+    void testProcessMessageWithFqdn(const uint8_t msg_type,
+                            const std::string& hostname) {
+        Pkt4Ptr req = generatePktWithFqdn(msg_type, Option4ClientFqdn::FLAG_S |
+                                          Option4ClientFqdn::FLAG_E, hostname,
+                                          Option4ClientFqdn::FULL, true);
+        Pkt4Ptr reply;
+        if (msg_type == DHCPDISCOVER) {
+            ASSERT_NO_THROW(reply = srv_->processDiscover(req));
+
+        } else if (msg_type == DHCPREQUEST) {
+            ASSERT_NO_THROW(reply = srv_->processRequest(req));
+
+        } else if (msg_type == DHCPRELEASE) {
+            ASSERT_NO_THROW(srv_->processRelease(req));
+            return;
+
+        } else {
+            return;
+        }
+
+        if (msg_type == DHCPDISCOVER) {
+            checkResponse(reply, DHCPOFFER, 1234);
+
+        } else {
+            checkResponse(reply, DHCPACK, 1234);
+        }
+
+    }
+
+    // Verify that NameChangeRequest holds valid values.
+    void verifyNameChangeRequest(const isc::dhcp_ddns::NameChangeType type,
+                                 const bool reverse, const bool forward,
+                                 const std::string& addr,
+                                 const std::string& fqdn,
+                                 const std::string& dhcid,
+                                 const time_t cltt,
+                                 const uint16_t len,
+                                 const bool not_strict_expire_check = false) {
+        NameChangeRequest ncr = srv_->name_change_reqs_.front();
+        EXPECT_EQ(type, ncr.getChangeType());
+        EXPECT_EQ(forward, ncr.isForwardChange());
+        EXPECT_EQ(reverse, ncr.isReverseChange());
+        EXPECT_EQ(addr, ncr.getIpAddress());
+        EXPECT_EQ(fqdn, ncr.getFqdn());
+        // Compare dhcid if it is not empty. In some cases, the DHCID is
+        // not known in advance and can't be compared.
+        if (!dhcid.empty()) {
+            EXPECT_EQ(dhcid, ncr.getDhcid().toStr());
+        }
+        // In some cases, the test doesn't have access to the last transmission
+        // time for the particular client. In such cases, the test can use the
+        // current time as cltt but the it may not check the lease expiration time
+        // for equality but rather check that the lease expiration time is not
+        // greater than the current time + lease lifetime.
+        if (not_strict_expire_check) {
+            EXPECT_GE(cltt + len, ncr.getLeaseExpiresOn());
+        } else {
+            EXPECT_EQ(cltt + len, ncr.getLeaseExpiresOn());
+        }
+        EXPECT_EQ(len, ncr.getLeaseLength());
+        EXPECT_EQ(isc::dhcp_ddns::ST_NEW, ncr.getStatus());
+        srv_->name_change_reqs_.pop();
+    }
+
+    NakedDhcpv4Srv* srv_;
+
+};
+
+// Test that the exception is thrown if lease pointer specified as the argument
+// of computeDhcid function is NULL.
+TEST_F(NameDhcpv4SrvTest, dhcidNullLease) {
+    Lease4Ptr lease;
+    EXPECT_THROW(srv_->computeDhcid(lease), isc::dhcp::DhcidComputeError);
+
+}
+
+// Test that the appropriate exception is thrown if the lease object used
+// to compute DHCID comprises wrong hostname.
+TEST_F(NameDhcpv4SrvTest, dhcidWrongHostname) {
+    // First, make sure that the lease with the correct hostname is accepted.
+    Lease4Ptr lease = createLease(IOAddress("192.0.2.3"),
+                                  "myhost.example.com.", true, true);
+    ASSERT_NO_THROW(srv_->computeDhcid(lease));
+
+    // Now, use the wrong hostname. It should result in the exception.
+    lease->hostname_ = "myhost...example.com.";
+    EXPECT_THROW(srv_->computeDhcid(lease), isc::dhcp::DhcidComputeError);
+
+    // Also, empty hostname is wrong.
+    lease->hostname_ = "";
+    EXPECT_THROW(srv_->computeDhcid(lease), isc::dhcp::DhcidComputeError);
+}
+
+// Test that the DHCID is computed correctly, when the lease holds
+// correct hostname and non-NULL client id.
+TEST_F(NameDhcpv4SrvTest, dhcidComputeFromClientId) {
+    Lease4Ptr lease = createLease(IOAddress("192.0.2.3"),
+                                  "myhost.example.com.",
+                                  true, true);
+    isc::dhcp_ddns::D2Dhcid dhcid;
+    ASSERT_NO_THROW(dhcid = srv_->computeDhcid(lease));
+
+    // Make sure that the computed DHCID is valid.
+    std::string dhcid_ref = "00010132E91AA355CFBB753C0F0497A5A9404"
+        "36965B68B6D438D98E680BF10B09F3BCF";
+    EXPECT_EQ(dhcid_ref, dhcid.toStr());
+}
+
+// Test that the DHCID is computed correctly, when the lease holds correct
+// hostname and NULL client id.
+TEST_F(NameDhcpv4SrvTest, dhcidComputeFromHWAddr) {
+    Lease4Ptr lease = createLease(IOAddress("192.0.2.3"),
+                                  "myhost.example.com.",
+                                  true, true);
+    lease->client_id_.reset();
+
+    isc::dhcp_ddns::D2Dhcid dhcid;
+    ASSERT_NO_THROW(dhcid = srv_->computeDhcid(lease));
+
+    // Make sure that the computed DHCID is valid.
+    std::string dhcid_ref = "0000012247F6DC4423C3E8627434A9D6868609"
+        "D88948F78018B215EDCAA30C0C135035";
+    EXPECT_EQ(dhcid_ref, dhcid.toStr());
+}
+
+
+// Test that server confirms to perform the forward and reverse DNS update,
+// when client asks for it.
+TEST_F(NameDhcpv4SrvTest, serverUpdateForwardFqdn) {
+    Pkt4Ptr query = generatePktWithFqdn(DHCPREQUEST,
+                                        Option4ClientFqdn::FLAG_E |
+                                        Option4ClientFqdn::FLAG_S,
+                                        "myhost.example.com.",
+                                        Option4ClientFqdn::FULL,
+                                        true);
+
+    testProcessFqdn(query,
+                    Option4ClientFqdn::FLAG_E | Option4ClientFqdn::FLAG_S,
+                    "myhost.example.com.");
+
+}
+
+// Test that server processes the Hostname option sent by a client and
+// responds with the Hostname option to confirm that the server has
+// taken responsibility for the update.
+TEST_F(NameDhcpv4SrvTest, serverUpdateHostname) {
+    Pkt4Ptr query;
+    ASSERT_NO_THROW(query = generatePktWithHostname(DHCPREQUEST,
+                                                    "myhost.example.com."));
+    OptionCustomPtr hostname;
+    ASSERT_NO_THROW(hostname = processHostname(query));
+
+    ASSERT_TRUE(hostname);
+    EXPECT_EQ("myhost.example.com.", hostname->readString());
+
+}
+
+// Test that the server skips processing of the empty Hostname option.
+TEST_F(NameDhcpv4SrvTest, serverUpdateEmptyHostname) {
+    Pkt4Ptr query;
+    ASSERT_NO_THROW(query = generatePktWithHostname(DHCPREQUEST, ""));
+    OptionCustomPtr hostname;
+    ASSERT_NO_THROW(hostname = processHostname(query));
+    EXPECT_FALSE(hostname);
+}
+
+// Test that the server skips processing of a wrong Hostname option.
+TEST_F(NameDhcpv4SrvTest, serverUpdateWrongHostname) {
+    Pkt4Ptr query;
+    ASSERT_NO_THROW(query = generatePktWithHostname(DHCPREQUEST,
+                                                    "abc..example.com"));
+    OptionCustomPtr hostname;
+    ASSERT_NO_THROW(hostname = processHostname(query));
+    EXPECT_FALSE(hostname);
+}
+
+
+// Test that server generates the fully qualified domain name for the client
+// if client supplies the partial name.
+TEST_F(NameDhcpv4SrvTest, serverUpdateForwardPartialNameFqdn) {
+    Pkt4Ptr query = generatePktWithFqdn(DHCPREQUEST,
+                                        Option4ClientFqdn::FLAG_E |
+                                        Option4ClientFqdn::FLAG_S,
+                                        "myhost",
+                                        Option4ClientFqdn::PARTIAL,
+                                        true);
+
+    testProcessFqdn(query,
+                    Option4ClientFqdn::FLAG_E | Option4ClientFqdn::FLAG_S,
+                    "myhost.example.com.");
+
+}
+
+// Test that server generates the fully qualified domain name for the client
+// if client supplies the unqualified name in the Hostname option.
+TEST_F(NameDhcpv4SrvTest, serverUpdateUnqualifiedHostname) {
+    Pkt4Ptr query;
+    ASSERT_NO_THROW(query = generatePktWithHostname(DHCPREQUEST, "myhost"));
+    OptionCustomPtr hostname;
+    ASSERT_NO_THROW(hostname =  processHostname(query));
+
+    ASSERT_TRUE(hostname);
+    EXPECT_EQ("myhost.example.com.", hostname->readString());
+
+}
+
+// Test that server sets empty domain-name in the FQDN option when client
+// supplied no domain-name. The domain-name is supposed to be set after the
+// lease is acquired. The domain-name is then generated from the IP address
+// assigned to a client.
+TEST_F(NameDhcpv4SrvTest, serverUpdateForwardNoNameFqdn) {
+    Pkt4Ptr query = generatePktWithFqdn(DHCPREQUEST,
+                                        Option4ClientFqdn::FLAG_E |
+                                        Option4ClientFqdn::FLAG_S,
+                                        "",
+                                        Option4ClientFqdn::PARTIAL,
+                                        true);
+
+    testProcessFqdn(query,
+                    Option4ClientFqdn::FLAG_E | Option4ClientFqdn::FLAG_S,
+                    "", Option4ClientFqdn::PARTIAL);
+
+}
+
+// Test server's response when client requests no DNS update.
+TEST_F(NameDhcpv4SrvTest, noUpdateFqdn) {
+    Pkt4Ptr query = generatePktWithFqdn(DHCPREQUEST,
+                                        Option4ClientFqdn::FLAG_E |
+                                        Option4ClientFqdn::FLAG_N,
+                                        "myhost.example.com.",
+                                        Option4ClientFqdn::FULL,
+                                        true);
+    testProcessFqdn(query, Option4ClientFqdn::FLAG_E |
+                    Option4ClientFqdn::FLAG_N,
+                    "myhost.example.com.");
+}
+
+// Test that server does not accept delegation of the forward DNS update
+// to a client.
+TEST_F(NameDhcpv4SrvTest, clientUpdateNotAllowedFqdn) {
+    Pkt4Ptr query = generatePktWithFqdn(DHCPREQUEST,
+                                        Option4ClientFqdn::FLAG_E,
+                                        "myhost.example.com.",
+                                        Option4ClientFqdn::FULL,
+                                        true);
+
+    testProcessFqdn(query, Option4ClientFqdn::FLAG_E |
+                    Option4ClientFqdn::FLAG_S | Option4ClientFqdn::FLAG_O,
+                    "myhost.example.com.");
+
+}
+
+// Test that exactly one NameChangeRequest is generated when the new lease
+// has been acquired (old lease is NULL).
+TEST_F(NameDhcpv4SrvTest, createNameChangeRequestsNewLease) {
+    Lease4Ptr lease = createLease(IOAddress("192.0.2.3"), "myhost.example.com.",
+                                  true, true);
+    Lease4Ptr old_lease;
+
+    ASSERT_NO_THROW(srv_->createNameChangeRequests(lease, old_lease));
+    ASSERT_EQ(1, srv_->name_change_reqs_.size());
+
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+                            "192.0.2.3", "myhost.example.com.",
+                            "00010132E91AA355CFBB753C0F0497A5A940436965"
+                            "B68B6D438D98E680BF10B09F3BCF",
+                            lease->cltt_, 100);
+}
+
+// Test that no NameChangeRequest is generated when a lease is renewed and
+// the FQDN data hasn't changed.
+TEST_F(NameDhcpv4SrvTest, createNameChangeRequestsRenewNoChange) {
+    Lease4Ptr lease = createLease(IOAddress("192.0.2.3"), "myhost.example.com.",
+                                  true, true);
+    Lease4Ptr old_lease = createLease(IOAddress("192.0.2.3"),
+                                      "myhost.example.com.", true, true);
+    old_lease->valid_lft_ += 100;
+
+    ASSERT_NO_THROW(srv_->createNameChangeRequests(lease, old_lease));
+    EXPECT_TRUE(srv_->name_change_reqs_.empty());
+}
+
+// Test that no NameChangeRequest is generated when forward and reverse
+// DNS update flags are not set in the lease.
+TEST_F(NameDhcpv4SrvTest, createNameChangeRequestsNoUpdate) {
+    Lease4Ptr lease1 = createLease(IOAddress("192.0.2.3"),
+                                   "lease1.example.com.",
+                                   true, true);
+    Lease4Ptr lease2 = createLease(IOAddress("192.0.2.3"),
+                                   "lease2.example.com.",
+                                   false, false);
+    ASSERT_NO_THROW(srv_->createNameChangeRequests(lease2, lease1));
+    EXPECT_EQ(1, srv_->name_change_reqs_.size());
+
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, true,
+                            "192.0.2.3", "lease1.example.com.",
+                            "0001013A5B311F5B9FB10DDF8E53689B874F25D"
+                            "62CC147C2FF237A64C90E5A597C9B7A",
+                            lease1->cltt_, 100);
+
+    lease2->hostname_ = "";
+    lease2->fqdn_rev_ = true;
+    lease2->fqdn_fwd_ = true;
+    ASSERT_NO_THROW(srv_->createNameChangeRequests(lease2, lease1));
+    EXPECT_EQ(1, srv_->name_change_reqs_.size());
+
+}
+
+// Test that two NameChangeRequests are generated when the lease is being
+// renewed and the new lease has updated FQDN data.
+TEST_F(NameDhcpv4SrvTest, createNameChangeRequestsRenew) {
+    Lease4Ptr lease1 = createLease(IOAddress("192.0.2.3"),
+                                   "lease1.example.com.",
+                                   true, true);
+    Lease4Ptr lease2 = createLease(IOAddress("192.0.2.3"),
+                                   "lease2.example.com.",
+                                   true, true);
+    ASSERT_NO_THROW(srv_->createNameChangeRequests(lease2, lease1));
+    ASSERT_EQ(2, srv_->name_change_reqs_.size());
+
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, true,
+                            "192.0.2.3", "lease1.example.com.",
+                            "0001013A5B311F5B9FB10DDF8E53689B874F25D"
+                            "62CC147C2FF237A64C90E5A597C9B7A",
+                            lease1->cltt_, 100);
+
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+                            "192.0.2.3", "lease2.example.com.",
+                            "000101F906D2BB752E1B2EECC5FF2BF434C0B2D"
+                            "D6D7F7BD873F4F280165DB8C9DBA7CB",
+                            lease2->cltt_, 100);
+
+}
+
+// This test verifies that exception is thrown when leases passed to the
+// createNameChangeRequests function do not match, i.e. they comprise
+// different IP addresses, client ids etc.
+TEST_F(NameDhcpv4SrvTest, createNameChangeRequestsLeaseMismatch) {
+    Lease4Ptr lease1 = createLease(IOAddress("192.0.2.3"),
+                                   "lease1.example.com.",
+                                   true, true);
+    Lease4Ptr lease2 = createLease(IOAddress("192.0.2.4"),
+                                   "lease2.example.com.",
+                                   true, true);
+    EXPECT_THROW(srv_->createNameChangeRequests(lease2, lease1),
+                 isc::Unexpected);
+}
+
+// Test that the OFFER message generated as a result of the DISCOVER message
+// processing will not result in generation of the NameChangeRequests.
+TEST_F(NameDhcpv4SrvTest, processDiscover) {
+    Pkt4Ptr req = generatePktWithFqdn(DHCPDISCOVER, Option4ClientFqdn::FLAG_S |
+                                      Option4ClientFqdn::FLAG_E,
+                                      "myhost.example.com.",
+                                      Option4ClientFqdn::FULL, true);
+
+    Pkt4Ptr reply;
+    ASSERT_NO_THROW(reply = srv_->processDiscover(req));
+
+    checkResponse(reply, DHCPOFFER, 1234);
+
+    EXPECT_TRUE(srv_->name_change_reqs_.empty());
+}
+
+// Test that server generates client's hostname from the IP address assigned
+// to it when DHCPv4 Client FQDN option specifies an empty domain-name.
+TEST_F(NameDhcpv4SrvTest, processRequestFqdnEmptyDomainName) {
+    Pkt4Ptr req = generatePktWithFqdn(DHCPREQUEST, Option4ClientFqdn::FLAG_S |
+                                      Option4ClientFqdn::FLAG_E,
+                                      "", Option4ClientFqdn::PARTIAL, true);
+
+    Pkt4Ptr reply;
+    ASSERT_NO_THROW(reply = srv_->processRequest(req));
+
+    checkResponse(reply, DHCPACK, 1234);
+
+    // Verify that there is one NameChangeRequest generated.
+    ASSERT_EQ(1, srv_->name_change_reqs_.size());
+    // The hostname is generated from the IP address acquired (yiaddr).
+    std::ostringstream hostname;
+    hostname << generatedNameFromAddress(reply->getYiaddr())
+             << ".example.com.";
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+                            reply->getYiaddr().toText(), hostname.str(),
+                            "", // empty DHCID forces that it is not checked
+                            time(NULL) + subnet_->getValid(),
+                            subnet_->getValid(), true);
+}
+
+// Test that server generates client's hostname from the IP address assigned
+// to it when Hostname option carries the top level domain-name.
+TEST_F(NameDhcpv4SrvTest, processRequestEmptyHostname) {
+    Pkt4Ptr req = generatePktWithHostname(DHCPREQUEST, ".");
+
+    Pkt4Ptr reply;
+    ASSERT_NO_THROW(reply = srv_->processRequest(req));
+
+    checkResponse(reply, DHCPACK, 1234);
+
+    // Verify that there is one NameChangeRequest generated.
+    ASSERT_EQ(1, srv_->name_change_reqs_.size());
+    // The hostname is generated from the IP address acquired (yiaddr).
+    std::ostringstream hostname;
+    hostname << generatedNameFromAddress(reply->getYiaddr()) << ".example.com.";
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+                            reply->getYiaddr().toText(), hostname.str(),
+                            "", // empty DHCID forces that it is not checked
+                            time(NULL), subnet_->getValid(), true);
+}
+
+// Test that client may send two requests, each carrying FQDN option with
+// a different domain-name. Server should use existing lease for the second
+// request but modify the DNS entries for the lease according to the contents
+// of the FQDN sent in the second request.
+TEST_F(NameDhcpv4SrvTest, processTwoRequestsFqdn) {
+    Pkt4Ptr req1 = generatePktWithFqdn(DHCPREQUEST, Option4ClientFqdn::FLAG_S |
+                                       Option4ClientFqdn::FLAG_E,
+                                       "myhost.example.com.",
+                                       Option4ClientFqdn::FULL, true);
+
+    Pkt4Ptr reply;
+    ASSERT_NO_THROW(reply = srv_->processRequest(req1));
+
+    checkResponse(reply, DHCPACK, 1234);
+
+    // Verify that there is one NameChangeRequest generated.
+    ASSERT_EQ(1, srv_->name_change_reqs_.size());
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+                            reply->getYiaddr().toText(), "myhost.example.com.",
+                            "00010132E91AA355CFBB753C0F0497A5A940436"
+                            "965B68B6D438D98E680BF10B09F3BCF",
+                            time(NULL), subnet_->getValid(), true);
+
+    // Create another Request message but with a different FQDN. Server
+    // should generate two NameChangeRequests: one to remove existing entry,
+    // another one to add new entry with updated domain-name.
+    Pkt4Ptr req2 = generatePktWithFqdn(DHCPREQUEST, Option4ClientFqdn::FLAG_S |
+                                       Option4ClientFqdn::FLAG_E,
+                                       "otherhost.example.com.",
+                                       Option4ClientFqdn::FULL, true);
+
+    ASSERT_NO_THROW(reply = srv_->processRequest(req2));
+
+    checkResponse(reply, DHCPACK, 1234);
+
+    // There should be two NameChangeRequests. Verify that they are valid.
+    ASSERT_EQ(2, srv_->name_change_reqs_.size());
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, true,
+                            reply->getYiaddr().toText(),
+                            "myhost.example.com.",
+                            "00010132E91AA355CFBB753C0F0497A5A940436"
+                            "965B68B6D438D98E680BF10B09F3BCF",
+                            time(NULL), subnet_->getValid(), true);
+
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+                            reply->getYiaddr().toText(),
+                            "otherhost.example.com.",
+                            "000101A5AEEA7498BD5AD9D3BF600E49FF39A7E3"
+                            "AFDCE8C3D0E53F35CC584DD63C89CA",
+                            time(NULL), subnet_->getValid(), true);
+}
+
+// Test that client may send two requests, each carrying Hostname option with
+// a different name. Server should use existing lease for the second request
+// but modify the DNS entries for the lease according to the contents of the
+// Hostname sent in the second request.
+TEST_F(NameDhcpv4SrvTest, processTwoRequestsHostname) {
+    Pkt4Ptr req1 = generatePktWithHostname(DHCPREQUEST, "myhost.example.com.");
+
+    Pkt4Ptr reply;
+    ASSERT_NO_THROW(reply = srv_->processRequest(req1));
+
+    checkResponse(reply, DHCPACK, 1234);
+
+    // Verify that there is one NameChangeRequest generated.
+    ASSERT_EQ(1, srv_->name_change_reqs_.size());
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+                            reply->getYiaddr().toText(), "myhost.example.com.",
+                            "00010132E91AA355CFBB753C0F0497A5A940436"
+                            "965B68B6D438D98E680BF10B09F3BCF",
+                            time(NULL), subnet_->getValid(), true);
+
+    // Create another Request message but with a different Hostname. Server
+    // should generate two NameChangeRequests: one to remove existing entry,
+    // another one to add new entry with updated domain-name.
+    Pkt4Ptr req2 = generatePktWithHostname(DHCPREQUEST, "otherhost");
+
+    ASSERT_NO_THROW(reply = srv_->processRequest(req2));
+
+    checkResponse(reply, DHCPACK, 1234);
+
+    // There should be two NameChangeRequests. Verify that they are valid.
+    ASSERT_EQ(2, srv_->name_change_reqs_.size());
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, true,
+                            reply->getYiaddr().toText(),
+                            "myhost.example.com.",
+                            "00010132E91AA355CFBB753C0F0497A5A940436"
+                            "965B68B6D438D98E680BF10B09F3BCF",
+                            time(NULL), subnet_->getValid(), true);
+
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+                            reply->getYiaddr().toText(),
+                            "otherhost.example.com.",
+                            "000101A5AEEA7498BD5AD9D3BF600E49FF39A7E3"
+                            "AFDCE8C3D0E53F35CC584DD63C89CA",
+                            time(NULL), subnet_->getValid(), true);
+}
+
+// Test that when the Release message is sent for the previously acquired
+// lease, then server genenerates a NameChangeRequest to remove the entries
+// corresponding to the lease being released.
+TEST_F(NameDhcpv4SrvTest, processRequestRelease) {
+    Pkt4Ptr req = generatePktWithFqdn(DHCPREQUEST, Option4ClientFqdn::FLAG_S |
+                                      Option4ClientFqdn::FLAG_E,
+                                      "myhost.example.com.",
+                                      Option4ClientFqdn::FULL, true);
+
+    Pkt4Ptr reply;
+    ASSERT_NO_THROW(reply = srv_->processRequest(req));
+
+    checkResponse(reply, DHCPACK, 1234);
+
+    // Verify that there is one NameChangeRequest generated.
+    ASSERT_EQ(1, srv_->name_change_reqs_.size());
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+                            reply->getYiaddr().toText(), "myhost.example.com.",
+                            "00010132E91AA355CFBB753C0F0497A5A940436"
+                            "965B68B6D438D98E680BF10B09F3BCF",
+                            time(NULL), subnet_->getValid(), true);
+
+    // Create a Release message.
+    Pkt4Ptr rel = Pkt4Ptr(new Pkt4(DHCPRELEASE, 1234));
+    rel->setCiaddr(reply->getYiaddr());
+    rel->setRemoteAddr(IOAddress("192.0.2.3"));
+    rel->addOption(generateClientId());
+    rel->addOption(srv_->getServerID());
+
+    ASSERT_NO_THROW(srv_->processRelease(rel));
+
+    // The lease has been removed, so there should be a NameChangeRequest to
+    // remove corresponding DNS entries.
+    ASSERT_EQ(1, srv_->name_change_reqs_.size());
+}
+
+} // end of anonymous namespace

+ 6 - 6
src/bin/dhcp6/dhcp6.dox

@@ -116,19 +116,19 @@ into the FIFO queue.
 @todo Currently the FIFO queue is not processed after the NameChangeRequests are
 generated and added to it. In the future implementation steps it is planned to create
 a code which will check if there are any outstanding requests in the queue and
-send them to the bind10-dhcp-ddns module when server is idle waiting for DHCP messages.
+send them to the b10-dhcp-ddns module when server is idle waiting for DHCP messages.
 
 In the simplest case, when client gets one address from the server, a DHCPv6 server
 may generate 0, 1 or 2 NameChangeRequests during single message processing. 
 Server generates no NameChangeRequests if it is not configured to update DNS
  or it rejects the DNS update for any other reason.
 
-Server may generate 1 NameChangeRequests in a situation when a client acquires a
+Server may generate 1 NameChangeRequest in a situation when a client acquires a
 new lease or it releases an existing lease. In the former case, the NameChangeRequest
-type is CHG_ADD, which indicates that the bind10-dhcp-ddns module should add a new DNS
+type is CHG_ADD, which indicates that the b10-dhcp-ddns module should add a new DNS
 binding for the client, and it is assumed that there is no DNS binding for this
 client already. In the latter case, the NameChangeRequest type is CHG_REMOVE to
-indicate to the bind10-dhcp-ddns module that the existing DNS binding should be removed
+indicate to the b10-dhcp-ddns module that the existing DNS binding should be removed
 from the DNS. The binding consists of the forward and reverse mapping.
 A server may only remove the mapping which it had added. Therefore, the lease database
 holds an information which updates (no update, reverse only update, forward only update,
@@ -140,7 +140,7 @@ Server may generate 2 NameChangeRequests in case the client is renewing a lease
 it already has a DNS binding for that lease. Note, that renewal may be triggered
 as a result of sending a RENEW message as well as the REQUEST message. In both cases
 DHCPv6 server will check if there is an existing lease for the client which has sent
-a message, and if there is it will check in the lease database if the DNS Updates had
+a message, and it will check in the lease database if the DNS Updates had
 been performed for this client. If the notion of client's FQDN changes comparing to
 the information stored in the lease database, the DHCPv6 has to remove an existing
 binding from the DNS and then add a new binding according to the new FQDN information
@@ -154,7 +154,7 @@ message being processed. That is 0, 1, 2 for the individual IA_NA. Generation of
 the distinct NameChangeRequests for each IADDR is not supported yet.
 
 The DHCPv6 Client FQDN Option comprises "NOS" flags which communicate to the
-server what updates (if any), client expects the server to perform. Server
+server what updates (if any) client expects the server to perform. Server
 may be configured to obey client's preference or do FQDN processing in a
 different way. If the server overrides client's preference it will communicate it
 by sending the DHCPv6 Client FQDN Option in its responses to a client, with

+ 2 - 2
src/bin/dhcp6/dhcp6_srv.cc

@@ -477,7 +477,7 @@ bool Dhcpv6Srv::run() {
                     .arg(e.what());
             }
 
-            // Send NameChangeRequests to the b10-dhcp_ddns module.
+            // Send NameChangeRequests to the b10-dhcp-ddns module.
             sendNameChangeRequests();
         }
     }
@@ -1215,7 +1215,7 @@ void
 Dhcpv6Srv::sendNameChangeRequests() {
     while (!name_change_reqs_.empty()) {
         // @todo Once next NameChangeRequest is picked from the queue
-        // we should send it to the bind10-dhcp_ddns module. Currently we
+        // we should send it to the b10-dhcp_ddns module. Currently we
         // just drop it.
         name_change_reqs_.pop();
     }

+ 1 - 1
src/bin/dhcp6/dhcp6_srv.h

@@ -434,7 +434,7 @@ protected:
     /// @brief Sends all outstanding NameChangeRequests to bind10-d2 module.
     ///
     /// The purpose of this function is to pick all outstanding
-    /// NameChangeRequests from the FIFO queue and send them to bind10-dhcp-ddns
+    /// NameChangeRequests from the FIFO queue and send them to b10-dhcp-ddns
     /// module.
     ///
     /// @todo Currently this function simply removes all requests from the

+ 0 - 1
src/lib/dhcp/option_custom.h

@@ -105,7 +105,6 @@ public:
     template<typename T>
     void addArrayDataField(const T value) {
         checkArrayType();
-
         OptionDataType data_type = definition_.getType();
         if (OptionDataTypeTraits<T>::type != data_type) {
             isc_throw(isc::dhcp::InvalidDataType,

+ 18 - 0
src/lib/dhcp/option_data_types.cc

@@ -227,6 +227,24 @@ OptionDataTypeUtil::writeFqdn(const std::string& fqdn,
     }
 }
 
+unsigned int
+OptionDataTypeUtil::getLabelCount(const std::string& text_name) {
+    // The isc::dns::Name class doesn't accept empty names. However, in some
+    // cases we may be dealing with empty names (e.g. sent by the DHCP clients).
+    // Empty names should not be sent as hostnames but if they are, for some
+    // reason, we don't want to throw an exception from this function. We
+    // rather want to signal empty name by returning 0 number of labels.
+    if (text_name.empty()) {
+        return (0);
+    }
+    try {
+        isc::dns::Name name(text_name);
+        return (name.getLabelCount());
+    } catch (const isc::Exception& ex) {
+        isc_throw(BadDataTypeCast, ex.what());
+    }
+}
+
 std::string
 OptionDataTypeUtil::readString(const std::vector<uint8_t>& buf) {
     std::string value;

+ 11 - 0
src/lib/dhcp/option_data_types.h

@@ -375,6 +375,17 @@ public:
                           std::vector<uint8_t>& buf,
                           const bool downcase = false);
 
+    /// @brief Return the number of labels in the Name.
+    ///
+    /// If the specified name is empty the 0 is returned.
+    ///
+    /// @param text_name A text representation of the name.
+    ///
+    /// @return A number of labels in the provided name or 0 if the
+    /// name string is empty.
+    /// @throw isc::dhcp::BadDataTypeCast if provided name is malformed.
+    static unsigned int getLabelCount(const std::string& text_name);
+
     /// @brief Read string value from a buffer.
     ///
     /// @param buf input buffer.

+ 15 - 1
src/lib/dhcp/tests/option_data_types_unittest.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-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
@@ -62,6 +62,20 @@ public:
     }
 };
 
+// The goal of this test is to verify that the getLabelCount returns the
+// correct number of labels in the domain name specified as a string
+// parameter.
+TEST_F(OptionDataTypesTest, getLabelCount) {
+    EXPECT_EQ(0, OptionDataTypeUtil::getLabelCount(""));
+    EXPECT_EQ(1, OptionDataTypeUtil::getLabelCount("."));
+    EXPECT_EQ(2, OptionDataTypeUtil::getLabelCount("example"));
+    EXPECT_EQ(3, OptionDataTypeUtil::getLabelCount("example.com"));
+    EXPECT_EQ(3, OptionDataTypeUtil::getLabelCount("example.com."));
+    EXPECT_EQ(4, OptionDataTypeUtil::getLabelCount("myhost.example.com"));
+    EXPECT_THROW(OptionDataTypeUtil::getLabelCount(".abc."),
+                 isc::dhcp::BadDataTypeCast);
+}
+
 // The goal of this test is to verify that an IPv4 address being
 // stored in a buffer (wire format) can be read into IOAddress
 // object.

+ 86 - 18
src/lib/dhcp_ddns/ncr_msg.cc

@@ -25,8 +25,23 @@
 namespace isc {
 namespace dhcp_ddns {
 
+
 /********************************* D2Dhcid ************************************/
 
+namespace {
+
+///
+/// @name Constants which define DHCID identifier-type
+//@{
+/// DHCID created from client's HW address.
+const uint8_t DHCID_ID_HWADDR   = 0x0;
+/// DHCID created from client identifier.
+const uint8_t DHCID_ID_CLIENTID = 0x1;
+/// DHCID created from DUID.
+const uint8_t DHCID_ID_DUID     = 0x2;
+
+}
+
 D2Dhcid::D2Dhcid() {
 }
 
@@ -34,6 +49,16 @@ D2Dhcid::D2Dhcid(const std::string& data) {
     fromStr(data);
 }
 
+D2Dhcid::D2Dhcid(const isc::dhcp::HWAddrPtr& hwaddr,
+                 const std::vector<uint8_t>& wire_fqdn) {
+    fromHWAddr(hwaddr, wire_fqdn);
+}
+
+D2Dhcid::D2Dhcid(const std::vector<uint8_t>& clientid_data,
+                 const std::vector<uint8_t>& wire_fqdn) {
+    fromClientId(clientid_data, wire_fqdn);
+}
+
 D2Dhcid::D2Dhcid(const isc::dhcp::DUID& duid,
                  const std::vector<uint8_t>& wire_fqdn) {
     fromDUID(duid, wire_fqdn);
@@ -56,33 +81,62 @@ D2Dhcid::toStr() const {
 }
 
 void
+D2Dhcid::fromClientId(const std::vector<uint8_t>& clientid_data,
+                      const std::vector<uint8_t>& wire_fqdn) {
+    createDigest(DHCID_ID_CLIENTID, clientid_data, wire_fqdn);
+}
+
+void
+D2Dhcid::fromHWAddr(const isc::dhcp::HWAddrPtr& hwaddr,
+                    const std::vector<uint8_t>& wire_fqdn) {
+    if (!hwaddr) {
+        isc_throw(isc::dhcp_ddns::DhcidRdataComputeError,
+                  "unable to compute DHCID from the HW address, "
+                  "NULL pointer has been specified");
+    } else if (hwaddr->hwaddr_.empty()) {
+        isc_throw(isc::dhcp_ddns::DhcidRdataComputeError,
+                  "unable to compute DHCID from the HW address, "
+                  "HW address is empty");
+    }
+    std::vector<uint8_t> hwaddr_data;
+    hwaddr_data.push_back(hwaddr->htype_);
+    hwaddr_data.insert(hwaddr_data.end(), hwaddr->hwaddr_.begin(),
+                       hwaddr->hwaddr_.end());
+    createDigest(DHCID_ID_HWADDR, hwaddr_data, wire_fqdn);
+}
+
+
+void
 D2Dhcid::fromDUID(const isc::dhcp::DUID& duid,
                   const std::vector<uint8_t>& wire_fqdn) {
-    // DHCID created from DUID starts with two bytes representing
-    // a type of the identifier. The value of 0x0002 indicates that
-    // DHCID has been created from DUID. The 3rd byte is equal to 1
-    // which indicates that the SHA-256 algorithm is used to create
-    // a DHCID digest. This value is called digest-type.
-    static uint8_t dhcid_header[] = { 0x00, 0x02, 0x01 };
 
+    createDigest(DHCID_ID_DUID, duid.getDuid(), wire_fqdn);
+}
+
+void
+D2Dhcid::createDigest(const uint8_t identifier_type,
+                      const std::vector<uint8_t>& identifier_data,
+                      const std::vector<uint8_t>& wire_fqdn) {
     // We get FQDN in the wire format, so we don't know if it is
     // valid. It is caller's responsibility to make sure it is in
     // the valid format. Here we just make sure it is not empty.
     if (wire_fqdn.empty()) {
-        isc_throw(isc::dhcp_ddns::NcrMessageError,
+        isc_throw(isc::dhcp_ddns::DhcidRdataComputeError,
                   "empty FQDN used to create DHCID");
     }
 
-    // Get the wire representation of the DUID.
-    std::vector<uint8_t> data = duid.getDuid();
-    // It should be DUID class responsibility to validate the DUID
-    // but let's be on the safe side here and make sure that empty
-    // DUID is not returned.
-    if (data.empty()) {
-        isc_throw(isc::dhcp_ddns::NcrMessageError,
+    // It is a responsibility of the classes which encapsulate client
+    // identifiers, e.g. DUID, to validate the client identifier data.
+    // But let's be on the safe side and at least check that it is not
+    // empty.
+    if (identifier_data.empty()) {
+        isc_throw(isc::dhcp_ddns::DhcidRdataComputeError,
                   "empty DUID used to create DHCID");
     }
 
+    // A data buffer will be used to compute the digest.
+    std::vector<uint8_t> data = identifier_data;
+
     // Append FQDN in the wire format.
     data.insert(data.end(), wire_fqdn.begin(), wire_fqdn.end());
 
@@ -100,14 +154,26 @@ D2Dhcid::fromDUID(const isc::dhcp::DUID& duid,
         secure = sha.process(static_cast<const Botan::byte*>(&data[0]),
                              data.size());
     } catch (const std::exception& ex) {
-        isc_throw(isc::dhcp_ddns::NcrMessageError,
+        isc_throw(isc::dhcp_ddns::DhcidRdataComputeError,
                   "error while generating DHCID from DUID: "
                   << ex.what());
     }
 
-    // The exception unsafe part is finished, so we can finally replace
-    // the contents of bytes_.
-    bytes_.assign(dhcid_header, dhcid_header + sizeof(dhcid_header));
+    // The DHCID RDATA has the following structure:
+    // 
+    //    < identifier-type > < digest-type > < digest >
+    //
+    // where identifier type 
+
+    // Let's allocate the space for the identifier-type (2 bytes) and
+    // digest-type (1 byte). This is 3 bytes all together.
+    bytes_.resize(3);
+    // Leave first byte 0 and set the second byte. Those two bytes
+    // form the identifier-type.
+    bytes_[1] = identifier_type;
+    // Third byte is always equal to 1, which specifies SHA-256 digest type.
+    bytes_[2] = 1;
+    // Now let's append the digest.
     bytes_.insert(bytes_.end(), secure.begin(), secure.end());
 }
 
@@ -117,6 +183,8 @@ operator<<(std::ostream& os, const D2Dhcid& dhcid) {
     return (os);
 }
 
+
+
 /**************************** NameChangeRequest ******************************/
 
 NameChangeRequest::NameChangeRequest()

+ 56 - 0
src/lib/dhcp_ddns/ncr_msg.h

@@ -22,6 +22,7 @@
 
 #include <cc/data.h>
 #include <dhcp/duid.h>
+#include <dhcp/hwaddr.h>
 #include <exceptions/exceptions.h>
 #include <util/buffer.h>
 #include <util/encode/hex.h>
@@ -40,6 +41,15 @@ public:
         isc::Exception(file, line, what) { };
 };
 
+/// @brief Exception thrown when there is an error occured during computation
+/// of the DHCID.
+class DhcidRdataComputeError : public isc::Exception {
+public:
+    DhcidRdataComputeError(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
+
 /// @brief Defines the types of DNS updates that can be requested.
 enum NameChangeType {
   CHG_ADD,
@@ -79,6 +89,22 @@ public:
     D2Dhcid(const std::string& data);
 
     /// @brief Constructor, creates an instance of the @c D2Dhcid from the
+    /// HW address.
+    ///
+    /// @param hwaddr A pointer to the object encapsulating HW address.
+    /// @param wire_fqdn A on-wire canonical representation of the FQDN.
+    D2Dhcid(const isc::dhcp::HWAddrPtr& hwaddr,
+            const std::vector<uint8_t>& wire_fqdn);
+
+    /// @brief Constructor, creates an instance of the @c D2Dhcid from the
+    /// client identifier carried in the Client Identifier option.
+    ///
+    /// @param clientid_data Holds the raw bytes representing client identifier.
+    /// @param wire_fqdn A on-wire canonical representation of the FQDN.
+    D2Dhcid(const std::vector<uint8_t>& clientid_data,
+            const std::vector<uint8_t>& wire_fqdn);
+
+    /// @brief Constructor, creates an instance of the @c D2Dhcid from the
     /// @c isc::dhcp::DUID.
     ///
     /// @param duid An object representing DUID.
@@ -101,6 +127,13 @@ public:
     /// or there is an odd number of digits.
     void fromStr(const std::string& data);
 
+    /// @brief Sets the DHCID value based on the Client Identifier.
+    ///
+    /// @param clientid_data Holds the raw bytes representing client identifier.
+    /// @param wire_fqdn A on-wire canonical representation of the FQDN.
+    void fromClientId(const std::vector<uint8_t>& clientid_data,
+                      const std::vector<uint8_t>& wire_fqdn);
+
     /// @brief Sets the DHCID value based on the DUID and FQDN.
     ///
     /// This function requires that the FQDN conforms to the section 3.5
@@ -112,6 +145,13 @@ public:
     void fromDUID(const isc::dhcp::DUID& duid,
                   const std::vector<uint8_t>& wire_fqdn);
 
+    /// @brief Sets the DHCID value based on the HW address and FQDN.
+    ///
+    /// @param hwaddr A pointer to the object encapsulating HW address.
+    /// @param wire_fqdn A on-wire canonical representation of the FQDN.
+    void fromHWAddr(const isc::dhcp::HWAddrPtr& hwaddr,
+                    const std::vector<uint8_t>& wire_fqdn);
+
     /// @brief Returns a reference to the DHCID byte vector.
     ///
     /// @return a reference to the vector.
@@ -135,6 +175,22 @@ public:
     }
 
 private:
+
+    /// @brief Creates the DHCID using specified indetifier.
+    ///
+    /// This function creates the DHCID RDATA as specified in RFC4701,
+    /// section 3.5.
+    ///
+    /// @param identifier_type is a less significant byte of the identifier-type
+    /// defined in RFC4701.
+    /// @param identifier_data A buffer holding client identifier raw data -
+    /// e.g. DUID, data carried in the Client Identifier option or client's
+    /// HW address.
+    /// @param A on-wire canonical representation of the FQDN.
+    void createDigest(const uint8_t identifier_type,
+                      const std::vector<uint8_t>& identifier_data,
+                      const std::vector<uint8_t>& wire_fqdn);
+
     /// @brief Storage for the DHCID value in unsigned bytes.
     std::vector<uint8_t> bytes_;
 };

+ 92 - 37
src/lib/dhcp_ddns/tests/ncr_unittests.cc

@@ -14,6 +14,7 @@
 
 #include <dhcp_ddns/ncr_msg.h>
 #include <dhcp/duid.h>
+#include <dhcp/hwaddr.h>
 #include <util/time_utilities.h>
 
 #include <gtest/gtest.h>
@@ -290,6 +291,26 @@ TEST(NameChangeRequestTest, dhcidTest) {
 
 }
 
+/// @brief Test fixture class for testing DHCID creation.
+class DhcidTest : public ::testing::Test {
+public:
+    /// @brief Constructor
+    DhcidTest() {
+        const uint8_t fqdn_data[] = {
+            6, 109, 121, 104, 111, 115, 116,     // myhost.
+            7, 101, 120, 97, 109, 112, 108, 101, // example.
+            3, 99, 111, 109, 0                   // com.
+        };
+        wire_fqdn_.assign(fqdn_data, fqdn_data + sizeof(fqdn_data));
+    }
+
+    /// @brief Destructor
+    virtual ~DhcidTest() {
+    }
+
+    std::vector<uint8_t> wire_fqdn_;
+};
+
 /// Tests that DHCID is correctly created from a DUID and FQDN. The final format
 /// of the DHCID is as follows:
 /// <identifier-type> <digest-type-code> <digest>
@@ -298,25 +319,15 @@ TEST(NameChangeRequestTest, dhcidTest) {
 /// - digest-type-code (1 octet) indicates SHA-256 hashing and is equal 0x1.
 /// - digest = SHA-256(<DUID> <FQDN>)
 /// Note: FQDN is given in the on-wire canonical format.
-TEST(NameChangeRequestTest, dhcidFromDUID) {
+TEST_F(DhcidTest, fromDUID) {
     D2Dhcid dhcid;
 
     // Create DUID.
     uint8_t duid_data[] = { 0, 1, 2, 3, 4, 5, 6 };
     DUID duid(duid_data, sizeof(duid_data));
 
-    // Create FQDN in on-wire format: myhost.example.com. It is encoded
-    // as a set of labels, each preceded by its length. The whole FQDN
-    // is zero-terminated.
-    const uint8_t fqdn_data[] = {
-        6, 109, 121, 104, 111, 115, 116,     // myhost.
-        7, 101, 120, 97, 109, 112, 108, 101, // example.
-        3, 99, 111, 109, 0                   // com.
-    };
-    std::vector<uint8_t> wire_fqdn(fqdn_data, fqdn_data + sizeof(fqdn_data));
-
     // Create DHCID.
-    ASSERT_NO_THROW(dhcid.fromDUID(duid, wire_fqdn));
+    ASSERT_NO_THROW(dhcid.fromDUID(duid, wire_fqdn_));
 
     // The reference DHCID (represented as string of hexadecimal digits)
     // has been calculated using one of the online calculators.
@@ -328,25 +339,15 @@ TEST(NameChangeRequestTest, dhcidFromDUID) {
 }
 
 // Test that DHCID is correctly created when the DUID has minimal length (1).
-TEST(NameChangeRequestTest, dhcidFromMinDUID) {
+TEST_F(DhcidTest, fromMinDUID) {
     D2Dhcid dhcid;
 
     // Create DUID.
     uint8_t duid_data[] = { 1 };
     DUID duid(duid_data, sizeof(duid_data));
 
-    // Create FQDN in on-wire format: myhost.example.com. It is encoded
-    // as a set of labels, each preceded by its length. The whole FQDN
-    // is zero-terminated.
-    const uint8_t fqdn_data[] = {
-        6, 109, 121, 104, 111, 115, 116,     // myhost.
-        7, 101, 120, 97, 109, 112, 108, 101, // example.
-        3, 99, 111, 109, 0                   // com.
-    };
-    std::vector<uint8_t> wire_fqdn(fqdn_data, fqdn_data + sizeof(fqdn_data));
-
     // Create DHCID.
-    ASSERT_NO_THROW(dhcid.fromDUID(duid, wire_fqdn));
+    ASSERT_NO_THROW(dhcid.fromDUID(duid, wire_fqdn_));
 
     // The reference DHCID (represented as string of hexadecimal digits)
     // has been calculated using one of the online calculators.
@@ -358,25 +359,15 @@ TEST(NameChangeRequestTest, dhcidFromMinDUID) {
 }
 
 // Test that DHCID is correctly created when the DUID has maximal length (128).
-TEST(NameChangeRequestTest, dhcidFromMaxDUID) {
+TEST_F(DhcidTest, fromMaxDUID) {
     D2Dhcid dhcid;
 
     // Create DUID.
     std::vector<uint8_t> duid_data(128, 1);
     DUID duid(&duid_data[0], duid_data.size());
 
-    // Create FQDN in on-wire format: myhost.example.com. It is encoded
-    // as a set of labels, each preceded by its length. The whole FQDN
-    // is zero-terminated.
-    const uint8_t fqdn_data[] = {
-        6, 109, 121, 104, 111, 115, 116,     // myhost.
-        7, 101, 120, 97, 109, 112, 108, 101, // example.
-        3, 99, 111, 109, 0                   // com.
-    };
-    std::vector<uint8_t> wire_fqdn(fqdn_data, fqdn_data + sizeof(fqdn_data));
-
     // Create DHCID.
-    ASSERT_NO_THROW(dhcid.fromDUID(duid, wire_fqdn));
+    ASSERT_NO_THROW(dhcid.fromDUID(duid, wire_fqdn_));
 
     // The reference DHCID (represented as string of hexadecimal digits)
     // has been calculated using one of the online calculators.
@@ -387,6 +378,71 @@ TEST(NameChangeRequestTest, dhcidFromMaxDUID) {
     EXPECT_EQ(dhcid_ref, dhcid.toStr());
 }
 
+// This test verifies that DHCID is properly computed from a buffer holding
+// client identifier data.
+TEST_F(DhcidTest, fromClientId) {
+    D2Dhcid dhcid;
+
+    // Create a buffer holding client id..
+    uint8_t clientid_data[] = { 0, 1, 2, 3, 4, 5, 6 };
+    std::vector<uint8_t> clientid(clientid_data,
+                                  clientid_data + sizeof(clientid_data));
+
+    // Create DHCID.
+    ASSERT_NO_THROW(dhcid.fromClientId(clientid, wire_fqdn_));
+
+    // The reference DHCID (represented as string of hexadecimal digits)
+    // has been calculated using one of the online calculators.
+    std::string dhcid_ref = "0001012191B7B21AF97E0E656DF887C5E2D"
+        "EF30E7758A207EDF4CCB2DE8CA37066021C";
+
+    // Make sure that the DHCID is valid.
+    EXPECT_EQ(dhcid_ref, dhcid.toStr());
+
+    // Make sure that the empty FQDN is not accepted.
+    std::vector<uint8_t> empty_wire_fqdn;
+    EXPECT_THROW(dhcid.fromClientId(clientid, empty_wire_fqdn),
+                 isc::dhcp_ddns::DhcidRdataComputeError);
+
+    // Make sure that the empty client identifier is not accepted.
+    clientid.clear();
+    EXPECT_THROW(dhcid.fromClientId(clientid, wire_fqdn_),
+                 isc::dhcp_ddns::DhcidRdataComputeError);
+
+
+}
+
+// This test verifies that DHCID is properly computed from a HW address.
+TEST_F(DhcidTest, fromHWAddr) {
+    D2Dhcid dhcid;
+
+    // Create a buffer holding client id..
+    uint8_t hwaddr_data[] = { 0, 1, 2, 3, 4, 5, 6 };
+    HWAddrPtr hwaddr(new HWAddr(hwaddr_data, sizeof(hwaddr_data),
+                                HTYPE_ETHER));
+
+    // Create DHCID.
+    ASSERT_NO_THROW(dhcid.fromHWAddr(hwaddr, wire_fqdn_));
+
+    // The reference DHCID (represented as string of hexadecimal digits)
+    // has been calculated using one of the online calculators.
+    std::string dhcid_ref = "0000012247F6DC4423C3E8627434A9D686860"
+        "9D88948F78018B215EDCAA30C0C135035";
+
+    // Make sure that the DHCID is valid.
+    EXPECT_EQ(dhcid_ref, dhcid.toStr());
+
+    // Make sure that the empty FQDN is not accepted.
+    std::vector<uint8_t> empty_wire_fqdn;
+    EXPECT_THROW(dhcid.fromHWAddr(hwaddr, empty_wire_fqdn),
+                 isc::dhcp_ddns::DhcidRdataComputeError);
+
+    // Make sure that the NULL HW address is not accepted.
+    hwaddr.reset();
+    EXPECT_THROW(dhcid.fromHWAddr(hwaddr, wire_fqdn_),
+                 isc::dhcp_ddns::DhcidRdataComputeError);
+}
+
 // test operator<< on D2Dhcid
 TEST(NameChangeRequestTest, leftShiftOperation) {
     const D2Dhcid dhcid("010203040A7F8E3D");
@@ -519,6 +575,5 @@ TEST(NameChangeRequestTest, toFromBufferTest) {
     ASSERT_EQ(final_str, msg_str);
 }
 
-
 } // end of anonymous namespace
 

+ 52 - 33
src/lib/dhcpsrv/lease.cc

@@ -74,6 +74,26 @@ Lease4::Lease4(const Lease4& other)
     }
 }
 
+bool
+Lease4::matches(const Lease4& other) const {
+    if ((client_id_ && !other.client_id_) ||
+        (!client_id_ && other.client_id_)) {
+        // One lease has client-id, but the other doesn't
+        return false;
+    }
+
+    if (client_id_ && other.client_id_ &&
+        *client_id_ != *other.client_id_) {
+        // Different client-ids
+        return false;
+    }
+
+    return (addr_ == other.addr_ &&
+            ext_ == other.ext_ &&
+            hwaddr_ == other.hwaddr_);
+
+}
+
 Lease4&
 Lease4::operator=(const Lease4& other) {
     if (this != &other) {
@@ -175,43 +195,42 @@ Lease4::operator==(const Lease4& other) const {
         return false;
     }
 
-    return (
-        addr_ == other.addr_ &&
-        ext_ == other.ext_ &&
-        hwaddr_ == other.hwaddr_ &&
-        t1_ == other.t1_ &&
-        t2_ == other.t2_ &&
-        valid_lft_ == other.valid_lft_ &&
-        cltt_ == other.cltt_ &&
-        subnet_id_ == other.subnet_id_ &&
-        fixed_ == other.fixed_ &&
-        hostname_ == other.hostname_ &&
-        fqdn_fwd_ == other.fqdn_fwd_ &&
-        fqdn_rev_ == other.fqdn_rev_ &&
-        comments_ == other.comments_
-    );
+    return (matches(other) &&
+            subnet_id_ == other.subnet_id_ &&
+            t1_ == other.t1_ &&
+            t2_ == other.t2_ &&
+            valid_lft_ == other.valid_lft_ &&
+            cltt_ == other.cltt_ &&
+            fixed_ == other.fixed_ &&
+            hostname_ == other.hostname_ &&
+            fqdn_fwd_ == other.fqdn_fwd_ &&
+            fqdn_rev_ == other.fqdn_rev_ &&
+            comments_ == other.comments_);
+}
+
+bool
+Lease6::matches(const Lease6& other) const {
+    return (addr_ == other.addr_ &&
+            type_ == other.type_ &&
+            prefixlen_ == other.prefixlen_ &&
+            iaid_ == other.iaid_ &&
+            *duid_ == *other.duid_);
 }
 
 bool
 Lease6::operator==(const Lease6& other) const {
-    return (
-        addr_ == other.addr_ &&
-        type_ == other.type_ &&
-        prefixlen_ == other.prefixlen_ &&
-        iaid_ == other.iaid_ &&
-        *duid_ == *other.duid_ &&
-        preferred_lft_ == other.preferred_lft_ &&
-        valid_lft_ == other.valid_lft_ &&
-        t1_ == other.t1_ &&
-        t2_ == other.t2_ &&
-        cltt_ == other.cltt_ &&
-        subnet_id_ == other.subnet_id_ &&
-        fixed_ == other.fixed_ &&
-        hostname_ == other.hostname_ &&
-        fqdn_fwd_ == other.fqdn_fwd_ &&
-        fqdn_rev_ == other.fqdn_rev_ &&
-        comments_ == other.comments_
-    );
+    return (matches(other) &&
+            preferred_lft_ == other.preferred_lft_ &&
+            valid_lft_ == other.valid_lft_ &&
+            t1_ == other.t1_ &&
+            t2_ == other.t2_ &&
+            cltt_ == other.cltt_ &&
+            subnet_id_ == other.subnet_id_ &&
+            fixed_ == other.fixed_ &&
+            hostname_ == other.hostname_ &&
+            fqdn_fwd_ == other.fqdn_fwd_ &&
+            fqdn_rev_ == other.fqdn_rev_ &&
+            comments_ == other.comments_);
 }
 
 } // namespace isc::dhcp

+ 28 - 0
src/lib/dhcpsrv/lease.h

@@ -209,6 +209,20 @@ struct Lease4 : public Lease {
     /// @param other the @c Lease4 object to be copied.
     Lease4(const Lease4& other);
 
+    /// @brief Check if two objects encapsulate the lease for the same
+    /// client.
+    ///
+    /// Checks if two @c Lease4 objects have the same address, client id,
+    /// HW address and ext_ value.  If these parameters match it is an
+    /// indication that both objects describe the lease for the same
+    /// client but apparently one is a result of renewal of the other. The
+    /// special case of the matching lease is the one that is equal to another.
+    ///
+    /// @param other A lease to compare with.
+    ///
+    /// @return true if the selected parameters of the two leases match.
+    bool matches(const Lease4& other) const;
+
     /// @brief Assignment operator.
     ///
     /// @param other the @c Lease4 object to be assigned.
@@ -320,6 +334,20 @@ struct Lease6 : public Lease {
         type_(TYPE_NA) {
     }
 
+    /// @brief Checks if two lease objects encapsulate the lease for the same
+    /// client.
+    ///
+    /// This function compares address, type, prefix length, IAID and DUID
+    /// parameters between two @c Lease6 objects. If these parameters match
+    /// it is an indication that both objects describe the lease for the same
+    /// client but apparently one is a result of renewal of the other. The
+    /// special case of the matching lease is the one that is equal to another.
+    ///
+    /// @param other A lease to compare to.
+    ///
+    /// @return true if selected parameters of the two leases match.
+    bool matches(const Lease6& other) const;
+
     /// @brief Compare two leases for equality
     ///
     /// @param other lease6 object with which to compare

+ 110 - 0
src/lib/dhcpsrv/tests/lease_mgr_unittest.cc

@@ -238,6 +238,17 @@ public:
 
 namespace {
 
+/// Hardware address used by different tests.
+const uint8_t HWADDR[] = {0x08, 0x00, 0x2b, 0x02, 0x3f, 0x4e};
+/// Client id used by different tests.
+const uint8_t CLIENTID[] = {0x17, 0x34, 0xe2, 0xff, 0x09, 0x92, 0x54};
+/// Valid lifetime value used by different tests.
+const uint32_t VALID_LIFETIME = 500;
+/// Subnet ID used by different tests.
+const uint32_t SUBNET_ID = 42;
+/// IAID value used by different tests.
+const uint32_t IAID = 7;
+
 /// @brief getParameter test
 ///
 /// This test checks if the LeaseMgr can be instantiated and that it
@@ -415,6 +426,52 @@ TEST(Lease4, operatorAssign) {
     EXPECT_FALSE(lease.client_id_ == copied_lease.client_id_);
 }
 
+// This test verifies that the matches() returns true if two leases differ
+// by values other than address, HW address, Client ID and ext_.
+TEST(Lease4, matches) {
+    // Create two leases which share the same address, HW address, client id
+    // and ext_ value.
+    const time_t current_time = time(NULL);
+    Lease4 lease1(IOAddress("192.0.2.3"), HWADDR, sizeof(HWADDR), CLIENTID,
+                  sizeof(CLIENTID), VALID_LIFETIME, current_time, 0, 0,
+                  SUBNET_ID);
+    lease1.hostname_ = "lease1.example.com.";
+    lease1.fqdn_fwd_ = true;
+    lease1.fqdn_rev_ = true;
+    Lease4 lease2(IOAddress("192.0.2.3"), HWADDR, sizeof(HWADDR), CLIENTID,
+                  sizeof(CLIENTID), VALID_LIFETIME + 10, current_time - 10,
+                  100, 200, SUBNET_ID);
+    lease2.hostname_ = "lease2.example.com.";
+    lease2.fqdn_fwd_ = false;
+    lease2.fqdn_rev_ = true;
+
+    // Leases should match.
+    EXPECT_TRUE(lease1.matches(lease2));
+    EXPECT_TRUE(lease2.matches(lease1));
+
+    // Change address, leases should not match anymore.
+    lease1.addr_ = IOAddress("192.0.2.4");
+    EXPECT_FALSE(lease1.matches(lease2));
+    lease1.addr_ = lease2.addr_;
+
+    // Change HW address, leases should not match.
+    lease1.hwaddr_[1] += 1;
+    EXPECT_FALSE(lease1.matches(lease2));
+    lease1.hwaddr_ = lease2.hwaddr_;
+
+    // Chanage client id, leases should not match.
+    std::vector<uint8_t> client_id = lease1.client_id_->getClientId();
+    client_id[1] += 1;
+    lease1.client_id_.reset(new ClientId(client_id));
+    EXPECT_FALSE(lease1.matches(lease2));
+    lease1.client_id_ = lease2.client_id_;
+
+    // Change ext_, leases should not match.
+    lease1.ext_ += 1;
+    EXPECT_FALSE(lease1.matches(lease2));
+    lease1.ext_ = lease2.ext_;
+}
+
 /// @brief Lease4 Equality Test
 ///
 /// Checks that the operator==() correctly compares two leases for equality.
@@ -641,6 +698,59 @@ TEST(Lease6, Lease6ConstructorWithFQDN) {
                                          subnet_id)), InvalidOperation);
 }
 
+// This test verifies that the matches() function returns true if two leases
+// differ by values other than address, type, prefix length, IAID and DUID.
+TEST(Lease6, matches) {
+
+    // Create two matching leases.
+    uint8_t llt[] = {0, 1, 2, 3, 4, 5, 6, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf};
+    DuidPtr duid(new DUID(llt, sizeof(llt)));
+
+    Lease6 lease1(Lease6::TYPE_NA, IOAddress("2001:db8:1::1"), duid,
+                  IAID, 100, 200, 50, 80,
+                  SUBNET_ID);
+    lease1.hostname_ = "lease1.example.com.";
+    lease1.fqdn_fwd_ = true;
+    lease1.fqdn_rev_ = true;
+    Lease6 lease2(Lease6::TYPE_NA, IOAddress("2001:db8:1::1"), duid,
+                  IAID, 200, 300, 90, 70,
+                  SUBNET_ID);
+    lease2.hostname_ = "lease1.example.com.";
+    lease2.fqdn_fwd_ = false;
+    lease2.fqdn_rev_ = true;
+
+    EXPECT_TRUE(lease1.matches(lease2));
+
+    // Modify each value used to match both leases, and make sure that
+    // leases don't match.
+
+    // Modify address.
+    lease1.addr_ = IOAddress("2001:db8:1::2");
+    EXPECT_FALSE(lease1.matches(lease2));
+    lease1.addr_ = lease2.addr_;
+
+    // Modify lease type.
+    lease1.type_ = Lease6::TYPE_TA;
+    EXPECT_FALSE(lease1.matches(lease2));
+    lease1.type_ = lease2.type_;
+
+    // Modify prefix length.
+    lease1.prefixlen_ += 1;
+    EXPECT_FALSE(lease1.matches(lease2));
+    lease1.prefixlen_ = lease2.prefixlen_;
+
+    // Modify IAID.
+    lease1.iaid_ += 1;
+    EXPECT_FALSE(lease1.matches(lease2));
+    lease1.iaid_ = lease2.iaid_;
+
+    // Modify DUID.
+    llt[1] += 1;
+    duid.reset(new DUID(llt, sizeof(llt)));
+    lease1.duid_ = duid;
+    EXPECT_FALSE(lease1.matches(lease2));
+    lease1.duid_ = lease2.duid_;
+}
 
 /// @brief Lease6 Equality Test
 ///