Browse Source

[3194] Merge branch 'master' into trac3194_1

Marcin Siodelski 11 years ago
parent
commit
faea98cbf4
63 changed files with 3512 additions and 110 deletions
  1. 45 0
      ChangeLog
  2. 5 0
      configure.ac
  3. 61 0
      doc/guide/bind10-guide.xml
  4. 2 0
      src/Makefile.am
  5. 1 1
      src/bin/auth/query.cc
  6. 16 3
      src/bin/bind10/init.py.in
  7. 7 0
      src/bin/bind10/init_messages.mes
  8. 26 4
      src/bin/dhcp4/config_parser.cc
  9. 15 2
      src/bin/dhcp4/dhcp4.spec
  10. 2 2
      src/bin/dhcp4/dhcp4_messages.mes
  11. 23 17
      src/bin/dhcp4/dhcp4_srv.cc
  12. 145 0
      src/bin/dhcp4/tests/config_parser_unittest.cc
  13. 130 0
      src/bin/dhcp4/tests/dhcp4_srv_unittest.cc
  14. 184 35
      src/bin/dhcp4/tests/dhcp4_test_utils.cc
  15. 59 3
      src/bin/dhcp4/tests/dhcp4_test_utils.h
  16. 1 1
      src/bin/dhcp6/ctrl_dhcp6_srv.cc
  17. 3 0
      src/bin/dhcp6/dhcp6_messages.mes
  18. 10 1
      src/bin/dhcp6/dhcp6_srv.cc
  19. 3 1
      src/bin/msgq/msgq.py.in
  20. 6 0
      src/bin/msgq/msgq_messages.mes
  21. 1 0
      src/hooks/Makefile.am
  22. 1 0
      src/hooks/dhcp/Makefile.am
  23. 65 0
      src/hooks/dhcp/user_chk/Makefile.am
  24. 113 0
      src/hooks/dhcp/user_chk/load_unload.cc
  25. 237 0
      src/hooks/dhcp/user_chk/subnet_select_co.cc
  26. 73 0
      src/hooks/dhcp/user_chk/tests/Makefile.am
  27. 27 0
      src/hooks/dhcp/user_chk/tests/run_unittests.cc
  28. 16 0
      src/hooks/dhcp/user_chk/tests/test_data_files_config.h.in
  29. 2 0
      src/hooks/dhcp/user_chk/tests/test_users_1.txt
  30. 2 0
      src/hooks/dhcp/user_chk/tests/test_users_err.txt
  31. 158 0
      src/hooks/dhcp/user_chk/tests/user_file_unittests.cc
  32. 216 0
      src/hooks/dhcp/user_chk/tests/user_registry_unittests.cc
  33. 96 0
      src/hooks/dhcp/user_chk/tests/user_unittests.cc
  34. 139 0
      src/hooks/dhcp/user_chk/tests/userid_unittests.cc
  35. 210 0
      src/hooks/dhcp/user_chk/user.cc
  36. 249 0
      src/hooks/dhcp/user_chk/user.h
  37. 18 0
      src/hooks/dhcp/user_chk/user_chk_log.cc
  38. 29 0
      src/hooks/dhcp/user_chk/user_chk_log.h
  39. 43 0
      src/hooks/dhcp/user_chk/user_chk_messages.mes
  40. 75 0
      src/hooks/dhcp/user_chk/user_data_source.h
  41. 159 0
      src/hooks/dhcp/user_chk/user_file.cc
  42. 135 0
      src/hooks/dhcp/user_chk/user_file.h
  43. 122 0
      src/hooks/dhcp/user_chk/user_registry.cc
  44. 128 0
      src/hooks/dhcp/user_chk/user_registry.h
  45. 25 0
      src/hooks/dhcp/user_chk/version.cc
  46. 1 1
      src/lib/datasrc/memory/zone_finder.cc
  47. 81 9
      src/lib/dhcp/iface_mgr.cc
  48. 24 0
      src/lib/dhcp/iface_mgr.h
  49. 4 2
      src/lib/dhcp/option_definition.cc
  50. 4 0
      src/lib/dhcp/pkt4.cc
  51. 1 0
      src/lib/dhcp/pkt4.h
  52. 17 15
      src/lib/dhcp/pkt6.cc
  53. 4 3
      src/lib/dhcp/pkt6.h
  54. 134 0
      src/lib/dhcp/tests/iface_mgr_unittest.cc
  55. 1 1
      src/lib/dhcp/tests/libdhcp++_unittest.cc
  56. 33 4
      src/lib/dhcpsrv/cfgmgr.cc
  57. 17 0
      src/lib/dhcpsrv/cfgmgr.h
  58. 1 0
      src/lib/dhcpsrv/dhcp_parsers.cc
  59. 14 1
      src/lib/dhcpsrv/subnet.cc
  60. 15 0
      src/lib/dhcpsrv/subnet.h
  61. 59 3
      src/lib/dhcpsrv/tests/cfgmgr_unittest.cc
  62. 18 0
      src/lib/dhcpsrv/tests/subnet_unittest.cc
  63. 1 1
      tests/tools/perfdhcp/perf_pkt6.cc

+ 45 - 0
ChangeLog

@@ -1,3 +1,48 @@
+698.	[bug]		muks
+	A bug was fixed in the interaction between b10-init and b10-msgq
+	that caused BIND 10 failures after repeated start/stop of
+	components.
+	(Trac #3094, git ed672a898d28d6249ff0c96df12384b0aee403c8
+
+697.	[func]	tmark
+	Implements "user_check" hooks shared library which supports subnet
+	selection based upon the contents of a list of known DHCP lease users
+	(i.e. clients).  Adds the following subdirectories to the bind10 src
+	directory for maintaining hooks shared libraries:
+		-bind10/src/hooks  - base directory for hooks shared libraries
+		-bind10/src/hooks/dhcp - base directory for all hooks libs pertaining
+		to DHCP(Kea)
+		-bind10/src/hooks/dhcp/user_check - directory containing the user_check
+		 hooks library
+	(Trac #3186, git f36aab92c85498f8511fbbe19fad5e3f787aef68)
+
+696.	[func]		tomek
+	b10-dhcp4: It is now possible to specify value of siaddr field
+	in DHCPv4 responses. It is used to point out to the next
+	server in the boot process (that typically is TFTP server).
+	(Trac #3191, git 541922b5300904a5de2eaeddc3666fc4b654ffba)
+
+695.	[func]		tomek
+	b10-dhcp6 is now able to listen on global IPv6 unicast addresses.
+	(Trac #3195, git 72e601f2a57ab70b25d50877c8e49242739d1c9f)
+
+694.	[bug]		tomek
+	b10-dhcp6 now handles exceptions better when processing initial
+	configuration. In particular, errors with socket binding do not
+	prevent b10-dhcp6 from establishing configuration session anymore.
+	(Trac #3195, git 72e601f2a57ab70b25d50877c8e49242739d1c9f)
+
+693.	[bug]		tomek
+	b10-dhcp6 now handles IPv6 interface enabling correctly.
+	(Trac #3195, git 72e601f2a57ab70b25d50877c8e49242739d1c9f)
+
+692.	[bug]		marcin
+	b10-dhcp4: Fix a bug whereby the Parameter Request List was not parsed
+	by the server and requested DHCPv4 options were not returned to the
+	client. Options are not sent back to the client if server failed to
+	assign a lease.
+	(Trac #3200, git 50d91e4c069c6de13680bfaaee3c56b68d6e4ab1)
+
 691.	[bug]		marcin
 	libdhcp++: Created definitions for standard DHCPv4 options:
 	tftp-server-name (66) and boot-file-name (67). Also, fixed definition

+ 5 - 0
configure.ac

@@ -1288,6 +1288,10 @@ AC_CONFIG_FILES([Makefile
                  src/bin/usermgr/Makefile
                  src/bin/usermgr/tests/Makefile
                  src/bin/tests/Makefile
+                 src/hooks/Makefile
+                 src/hooks/dhcp/Makefile
+                 src/hooks/dhcp/user_chk/Makefile
+                 src/hooks/dhcp/user_chk/tests/Makefile
                  src/lib/Makefile
                  src/lib/asiolink/Makefile
                  src/lib/asiolink/tests/Makefile
@@ -1468,6 +1472,7 @@ AC_OUTPUT([doc/version.ent
            src/bin/d2/spec_config.h.pre
            src/bin/d2/tests/test_data_files_config.h
            src/bin/tests/process_rename_test.py
+           src/hooks/dhcp/user_chk/tests/test_data_files_config.h
            src/lib/config/tests/data_def_unittests_config.h
            src/lib/dhcpsrv/tests/test_libraries.h
            src/lib/python/isc/config/tests/config_test

+ 61 - 0
doc/guide/bind10-guide.xml

@@ -4390,6 +4390,30 @@ Dhcp4/subnet4	[]	list	(default)
       </para>
     </section>
 
+    <section id="dhcp4-next-server">
+      <title>Next server (siaddr)</title>
+      <para>In some cases, clients want to obtain configuration from the TFTP server.
+      Although there is a dedicated option for it, some devices may use siaddr field
+      in the DHCPv4 packet for that purpose. That specific field can be configured
+      using next-server directive. It is possible to define it in global scope or
+      for a given subnet only. If both are defined, subnet value takes precedence.
+      The value in subnet can be set to 0.0.0.0, which means that next-server should
+      not be sent. It may also be set to empty string, which means the same as if
+      it was not defined at all - use global value.
+      </para>
+
+<screen>
+&gt; <userinput>config add Dhcp4/next-server</userinput>
+&gt; <userinput>config set Dhcp4/next-server "192.0.2.123"</userinput>
+&gt; <userinput>config commit</userinput>
+<userinput></userinput>
+&gt; <userinput>config add Dhcp4/subnet[0]/next-server</userinput>
+&gt; <userinput>config set Dhcp4/subnet[0]/next-server "192.0.2.234"</userinput>
+&gt; <userinput>config commit</userinput>
+</screen>
+
+    </section>
+
     <section id="dhcp4-std">
       <title>Supported Standards</title>
       <para>The following standards and draft standards are currently
@@ -4669,6 +4693,43 @@ Dhcp6/subnet6/	list
       </para>
     </section>
 
+    <section id="dhcp6-unicast">
+      <title>Unicast traffic support</title>
+      <para>
+        When DHCPv6 server starts up, by default it listens to the DHCP traffic
+        sent to multicast address ff02::1:2 on each interface that it is
+        configured to listen on (see <xref linkend="dhcp6-interface-selection"/>).
+        In some cases it is useful to configure a server to handle incoming
+        traffic sent to the global unicast addresses as well. The most common
+        reason for that is to have relays send their traffic to the server
+        directly. To configure server to listen on specific unicast address, a
+        notation to specify interfaces has been extended. Interface name can be
+        optionally followed by a slash, followed by global unicast address that
+        server should listen on. That will be done in addition to normal
+        link-local binding + listening on ff02::1:2 address. The sample commands
+        listed below show how to listen on 2001:db8::1 (a global address)
+        configured on the eth1 interface.
+      </para>
+      <para>
+        <screen>
+&gt; <userinput>config set Dhcp6/interfaces[0] eth1/2001:db8::1</userinput>
+&gt; <userinput>config commit</userinput></screen>
+        When configuration gets committed, the server will start to listen on
+        eth1 on link-local address, mutlicast group (ff02::1:2) and 2001:db8::1.
+      </para>
+      <para>
+        It is possible to mix interface names, wildcards and interface name/addresses
+        on the Dhcp6/interface list. It is not possible to specify more than one
+        unicast address on a given interface.
+      </para>
+      <para>
+        Care should be taken to specify proper unicast addresses. The server will
+        attempt to bind to those addresses specified, without any additional checks.
+        That approach is selected on purpose, so in the software can be used to
+        communicate over uncommon addresses if the administrator desires so.
+      </para>
+    </section>
+
     <section>
       <title>Subnet and Address Pool</title>
       <para>

+ 2 - 0
src/Makefile.am

@@ -1,4 +1,6 @@
 SUBDIRS = lib bin
+# @todo hooks lib could be a configurable switch
+SUBDIRS += hooks
 
 EXTRA_DIST = \
 	cppcheck-suppress.lst		\

+ 1 - 1
src/bin/auth/query.cc

@@ -255,7 +255,7 @@ Query::addWildcardNXRRSETProof(ZoneFinder& finder, ConstRRsetPtr nsec) {
     if (nsec->getRdataCount() == 0) {
         isc_throw(BadNSEC, "NSEC for WILDCARD_NXRRSET is empty");
     }
-    
+
     ConstZoneFinderContextPtr fcontext =
         finder.find(*qname_, RRType::NSEC(),
                     dnssec_opt_ | ZoneFinder::NO_WILDCARD);

+ 16 - 3
src/bin/bind10/init.py.in

@@ -337,6 +337,7 @@ class Init:
                 self.__propagate_component_config(new_config['components'])
             return isc.config.ccsession.create_answer(0)
         except Exception as e:
+            logger.error(BIND10_RECONFIGURE_ERROR, e)
             return isc.config.ccsession.create_answer(1, str(e))
 
     def get_processes(self):
@@ -597,6 +598,13 @@ class Init:
             process, the log_starting/log_started methods are not used.
         """
         logger.info(BIND10_STARTING_CC)
+
+        # Unsubscribe from the other CC session first, because we only
+        # monitor one and msgq expects all data sent to us to be read,
+        # or it will close its side of the socket.
+        if self.cc_session is not None:
+            self.cc_session.group_unsubscribe("Init")
+
         self.ccs = isc.config.ModuleCCSession(SPECFILE_LOCATION,
                                       self.config_handler,
                                       self.command_handler,
@@ -764,9 +772,14 @@ class Init:
         it might want to choose if it is for this one).
         """
         logger.info(BIND10_STOP_PROCESS, process)
-        self.cc_session.group_sendmsg(isc.config.ccsession.
-                                      create_command('shutdown', {'pid': pid}),
-                                      recipient, recipient)
+        try:
+            self.cc_session.group_sendmsg(isc.config.ccsession.
+                                          create_command('shutdown',
+                                                         {'pid': pid}),
+                                          recipient, recipient)
+        except:
+            logger.error(BIND10_COMPONENT_SHUTDOWN_ERROR, process)
+            raise
 
     def component_shutdown(self, exitcode=0):
         """

+ 7 - 0
src/bin/bind10/init_messages.mes

@@ -325,3 +325,10 @@ the configuration manager to start up.  The total length of time Init
 will wait for the configuration manager before reporting an error is
 set with the command line --wait switch, which has a default value of
 ten seconds.
+
+% BIND10_RECONFIGURE_ERROR Error applying new config: %1
+A new configuration was received, but there was an error doing the
+re-configuration.
+
+% BIND10_COMPONENT_SHUTDOWN_ERROR An error occured stopping component %1
+An attempt to gracefully shutdown a component failed.

+ 26 - 4
src/bin/dhcp4/config_parser.cc

@@ -199,7 +199,8 @@ protected:
             (config_id.compare("rebind-timer") == 0))  {
             parser = new Uint32Parser(config_id, uint32_values_);
         } else if ((config_id.compare("subnet") == 0) ||
-                   (config_id.compare("interface") == 0)) {
+                   (config_id.compare("interface") == 0) ||
+                   (config_id.compare("next-server") == 0)) {
             parser = new StringParser(config_id, string_values_);
         } else if (config_id.compare("pool") == 0) {
             parser = new Pool4Parser(config_id, pools_);
@@ -264,14 +265,34 @@ protected:
         Triplet<uint32_t> t2 = getParam("rebind-timer");
         Triplet<uint32_t> valid = getParam("valid-lifetime");
 
-        /// @todo: Convert this to logger once the parser is working reliably
         stringstream tmp;
         tmp << addr.toText() << "/" << (int)len
             << " with params t1=" << t1 << ", t2=" << t2 << ", valid=" << valid;
 
         LOG_INFO(dhcp4_logger, DHCP4_CONFIG_NEW_SUBNET).arg(tmp.str());
 
-        subnet_.reset(new Subnet4(addr, len, t1, t2, valid));
+        Subnet4Ptr subnet4(new Subnet4(addr, len, t1, t2, valid));
+        subnet_ = subnet4;
+
+        // Try global value first
+        try {
+            string next_server = globalContext()->string_values_->getParam("next-server");
+            if (!next_server.empty()) {
+                subnet4->setSiaddr(IOAddress(next_server));
+            }
+        } catch (const DhcpConfigError&) {
+            // Don't care. next_server is optional. We can live without it
+        }
+
+        // Try subnet specific value if it's available
+        try {
+            string next_server = string_values_->getParam("next-server");
+            if (!next_server.empty()) {
+                subnet4->setSiaddr(IOAddress(next_server));
+            }
+        } catch (const DhcpConfigError&) {
+            // Don't care. next_server is optional. We can live without it
+        }
     }
 };
 
@@ -366,7 +387,8 @@ DhcpConfigParser* createGlobalDhcp4ConfigParser(const std::string& config_id) {
     } else if (config_id.compare("option-def") == 0) {
         parser  = new OptionDefListParser(config_id,
                                           globalContext()->option_defs_);
-    } else if (config_id.compare("version") == 0) {
+    } else if ((config_id.compare("version") == 0) ||
+               (config_id.compare("next-server") == 0)) {
         parser  = new StringParser(config_id,
                                     globalContext()->string_values_);
     } else if (config_id.compare("lease-database") == 0) {

+ 15 - 2
src/bin/dhcp4/dhcp4.spec

@@ -16,7 +16,7 @@
           "item_default": ""
         }
       },
- 
+
       { "item_name": "interfaces",
         "item_type": "list",
         "item_optional": false,
@@ -48,6 +48,12 @@
         "item_default": 4000
       },
 
+      { "item_name": "next-server",
+        "item_type": "string",
+        "item_optional": true,
+        "item_default": ""
+      },
+
       { "item_name": "option-def",
         "item_type": "list",
         "item_optional": false,
@@ -218,6 +224,13 @@
                   "item_optional": false,
                   "item_default": 7200
                 },
+
+                { "item_name": "next-server",
+                  "item_type": "string",
+                  "item_optional": true,
+                  "item_default": "0.0.0.0"
+                },
+
                 { "item_name": "pool",
                   "item_type": "list",
                   "item_optional": false,
@@ -290,7 +303,7 @@
 
         {
             "command_name": "libreload",
-            "command_description": "Reloads the current hooks libraries.", 
+            "command_description": "Reloads the current hooks libraries.",
             "command_args": []
         }
 

+ 2 - 2
src/bin/dhcp4/dhcp4_messages.mes

@@ -118,7 +118,7 @@ a lease. It is up to the client to choose one server out of othe advertised
 and continue allocation with that server. This is a normal behavior and
 indicates successful operation.
 
-% DHCP4_LEASE_ADVERT_FAIL failed to advertise a lease for client client-id %1, hwaddr %2
+% DHCP4_LEASE_ADVERT_FAIL failed to advertise a lease for client client-id %1, hwaddr %2, client sent yiaddr %3
 This message indicates that the server has failed to offer a lease to
 the specified client after receiving a DISCOVER message from it. There are
 many possible reasons for such a failure.
@@ -128,7 +128,7 @@ This debug message indicates that the server successfully granted a lease
 in response to client's REQUEST message. This is a normal behavior and
 indicates successful operation.
 
-% DHCP4_LEASE_ALLOC_FAIL failed to grant a lease for client-id %1, hwaddr %2
+% DHCP4_LEASE_ALLOC_FAIL failed to grant a lease for client-id %1, hwaddr %2, client sent yiaddr %3
 This message indicates that the server failed to grant a lease to the
 specified client after receiving a REQUEST message from it.  There are many
 possible reasons for such a failure. Additional messages will indicate the

+ 23 - 17
src/bin/dhcp4/dhcp4_srv.cc

@@ -758,6 +758,13 @@ Dhcpv4Srv::assignLease(const Pkt4Ptr& question, Pkt4Ptr& answer) {
         return;
     }
 
+    // Set up siaddr. Perhaps assignLease is not the best place to call this
+    // as siaddr has nothing to do with a lease, but otherwise we would have
+    // to select subnet twice (performance hit) or update too many functions
+    // at once.
+    // @todo: move subnet selection to a common code
+    answer->setSiaddr(subnet->getSiaddr());
+
     LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL_DATA, DHCP4_SUBNET_SELECTED)
         .arg(subnet->toText());
 
@@ -811,13 +818,6 @@ Dhcpv4Srv::assignLease(const Pkt4Ptr& question, Pkt4Ptr& answer) {
         opt->setUint32(lease->valid_lft_);
         answer->addOption(opt);
 
-        // Router (type 3)
-        Subnet::OptionDescriptor opt_routers =
-            subnet->getOptionDescriptor("dhcp4", DHO_ROUTERS);
-        if (opt_routers.option) {
-            answer->addOption(opt_routers.option);
-        }
-
         // Subnet mask (type 1)
         answer->addOption(getNetmaskOption(subnet));
 
@@ -914,15 +914,18 @@ Dhcpv4Srv::processDiscover(Pkt4Ptr& discover) {
 
     copyDefaultFields(discover, offer);
     appendDefaultOptions(offer, DHCPOFFER);
-    appendRequestedOptions(discover, offer);
     appendRequestedVendorOptions(discover, offer);
 
     assignLease(discover, offer);
 
-    // There are a few basic options that we always want to
-    // include in the response. If client did not request
-    // them we append them for him.
-    appendBasicOptions(discover, offer);
+    // Adding any other options makes sense only when we got the lease.
+    if (offer->getYiaddr() != IOAddress("0.0.0.0")) {
+        appendRequestedOptions(discover, offer);
+        // There are a few basic options that we always want to
+        // include in the response. If client did not request
+        // them we append them for him.
+        appendBasicOptions(discover, offer);
+    }
 
     return (offer);
 }
@@ -938,7 +941,6 @@ Dhcpv4Srv::processRequest(Pkt4Ptr& request) {
 
     copyDefaultFields(request, ack);
     appendDefaultOptions(ack, DHCPACK);
-    appendRequestedOptions(request, ack);
     appendRequestedVendorOptions(request, ack);
 
     // Note that we treat REQUEST message uniformly, regardless if this is a
@@ -946,10 +948,14 @@ Dhcpv4Srv::processRequest(Pkt4Ptr& request) {
     // or even rebinding.
     assignLease(request, ack);
 
-    // There are a few basic options that we always want to
-    // include in the response. If client did not request
-    // them we append them for him.
-    appendBasicOptions(request, ack);
+    // Adding any other options makes sense only when we got the lease.
+    if (ack->getYiaddr() != IOAddress("0.0.0.0")) {
+        appendRequestedOptions(request, ack);
+        // There are a few basic options that we always want to
+        // include in the response. If client did not request
+        // them we append them for him.
+        appendBasicOptions(request, ack);
+    }
 
     return (ack);
 }

+ 145 - 0
src/bin/dhcp4/tests/config_parser_unittest.cc

@@ -412,6 +412,151 @@ TEST_F(Dhcp4ParserTest, subnetGlobalDefaults) {
     EXPECT_EQ(4000, subnet->getValid());
 }
 
+// Checks if the next-server defined as global parameter is taken into
+// consideration.
+TEST_F(Dhcp4ParserTest, nextServerGlobal) {
+
+    ConstElementPtr status;
+
+    string config = "{ \"interfaces\": [ \"*\" ],"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"next-server\": \"1.2.3.4\", "
+        "\"subnet4\": [ { "
+        "    \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+        "    \"subnet\": \"192.0.2.0/24\" } ],"
+        "\"valid-lifetime\": 4000 }";
+
+    ElementPtr json = Element::fromJSON(config);
+
+    EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json));
+
+    // check if returned status is OK
+    checkResult(status, 0);
+
+    // Now check if the configuration was indeed handled and we have
+    // expected pool configured.
+    Subnet4Ptr subnet = CfgMgr::instance().getSubnet4(IOAddress("192.0.2.200"));
+    ASSERT_TRUE(subnet);
+    EXPECT_EQ("1.2.3.4", subnet->getSiaddr().toText());
+}
+
+// Checks if the next-server defined as subnet parameter is taken into
+// consideration.
+TEST_F(Dhcp4ParserTest, nextServerSubnet) {
+
+    ConstElementPtr status;
+
+    string config = "{ \"interfaces\": [ \"*\" ],"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet4\": [ { "
+        "    \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+        "    \"next-server\": \"1.2.3.4\", "
+        "    \"subnet\": \"192.0.2.0/24\" } ],"
+        "\"valid-lifetime\": 4000 }";
+
+    ElementPtr json = Element::fromJSON(config);
+
+    EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json));
+
+    // check if returned status is OK
+    checkResult(status, 0);
+
+    // Now check if the configuration was indeed handled and we have
+    // expected pool configured.
+    Subnet4Ptr subnet = CfgMgr::instance().getSubnet4(IOAddress("192.0.2.200"));
+    ASSERT_TRUE(subnet);
+    EXPECT_EQ("1.2.3.4", subnet->getSiaddr().toText());
+}
+
+// Test checks several negative scenarios for next-server configuration: bogus
+// address, IPv6 adddress and empty string.
+TEST_F(Dhcp4ParserTest, nextServerNegative) {
+
+    ConstElementPtr status;
+
+    // Config with junk instead of next-server address
+    string config_bogus1 = "{ \"interfaces\": [ \"*\" ],"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet4\": [ { "
+        "    \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+        "    \"rebind-timer\": 2000, "
+        "    \"renew-timer\": 1000, "
+        "    \"next-server\": \"a.b.c.d\", "
+        "    \"subnet\": \"192.0.2.0/24\" } ],"
+        "\"valid-lifetime\": 4000 }";
+
+    // Config with IPv6 next server address
+    string config_bogus2 = "{ \"interfaces\": [ \"*\" ],"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet4\": [ { "
+        "    \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+        "    \"rebind-timer\": 2000, "
+        "    \"renew-timer\": 1000, "
+        "    \"next-server\": \"2001:db8::1\", "
+        "    \"subnet\": \"192.0.2.0/24\" } ],"
+        "\"valid-lifetime\": 4000 }";
+
+    // Config with empty next server address
+    string config_bogus3 = "{ \"interfaces\": [ \"*\" ],"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet4\": [ { "
+        "    \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+        "    \"rebind-timer\": 2000, "
+        "    \"renew-timer\": 1000, "
+        "    \"next-server\": \"\", "
+        "    \"subnet\": \"192.0.2.0/24\" } ],"
+        "\"valid-lifetime\": 4000 }";
+
+    ElementPtr json1 = Element::fromJSON(config_bogus1);
+    ElementPtr json2 = Element::fromJSON(config_bogus2);
+    ElementPtr json3 = Element::fromJSON(config_bogus3);
+
+    // check if returned status is always a failure
+    EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json1));
+    checkResult(status, 1);
+
+    EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json2));
+    checkResult(status, 1);
+
+    EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json3));
+    checkResult(status, 0);
+}
+
+// Checks if the next-server defined as global value is overridden by subnet
+// specific value.
+TEST_F(Dhcp4ParserTest, nextServerOverride) {
+
+    ConstElementPtr status;
+
+    string config = "{ \"interfaces\": [ \"*\" ],"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"next-server\": \"192.0.0.1\", "
+        "\"subnet4\": [ { "
+        "    \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+        "    \"next-server\": \"1.2.3.4\", "
+        "    \"subnet\": \"192.0.2.0/24\" } ],"
+        "\"valid-lifetime\": 4000 }";
+
+    ElementPtr json = Element::fromJSON(config);
+
+    EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json));
+
+    // check if returned status is OK
+    checkResult(status, 0);
+
+    // Now check if the configuration was indeed handled and we have
+    // expected pool configured.
+    Subnet4Ptr subnet = CfgMgr::instance().getSubnet4(IOAddress("192.0.2.200"));
+    ASSERT_TRUE(subnet);
+    EXPECT_EQ("1.2.3.4", subnet->getSiaddr().toText());
+}
+
 // This test checks if it is possible to override global values
 // on a per subnet basis.
 TEST_F(Dhcp4ParserTest, subnetLocal) {

+ 130 - 0
src/bin/dhcp4/tests/dhcp4_srv_unittest.cc

@@ -1377,6 +1377,136 @@ TEST_F(Dhcpv4SrvTest, unpackOptions) {
     EXPECT_EQ(0x0, option_bar->getValue());
 }
 
+// Checks whether the server uses default (0.0.0.0) siaddr value, unless
+// explicitly specified
+TEST_F(Dhcpv4SrvTest, siaddrDefault) {
+    boost::scoped_ptr<NakedDhcpv4Srv> srv;
+    ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
+    IOAddress hint("192.0.2.107");
+
+    Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+    dis->setRemoteAddr(IOAddress("192.0.2.1"));
+    OptionPtr clientid = generateClientId();
+    dis->addOption(clientid);
+    dis->setYiaddr(hint);
+
+    // Pass it to the server and get an offer
+    Pkt4Ptr offer = srv->processDiscover(dis);
+    ASSERT_TRUE(offer);
+
+    // Check if we get response at all
+    checkResponse(offer, DHCPOFFER, 1234);
+
+    // Verify that it is 0.0.0.0
+    EXPECT_EQ("0.0.0.0", offer->getSiaddr().toText());
+}
+
+// Checks whether the server uses specified siaddr value
+TEST_F(Dhcpv4SrvTest, siaddr) {
+    boost::scoped_ptr<NakedDhcpv4Srv> srv;
+    ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
+    subnet_->setSiaddr(IOAddress("192.0.2.123"));
+
+    Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+    dis->setRemoteAddr(IOAddress("192.0.2.1"));
+    OptionPtr clientid = generateClientId();
+    dis->addOption(clientid);
+
+    // Pass it to the server and get an offer
+    Pkt4Ptr offer = srv->processDiscover(dis);
+    ASSERT_TRUE(offer);
+
+    // Check if we get response at all
+    checkResponse(offer, DHCPOFFER, 1234);
+
+    // Verify that its value is proper
+    EXPECT_EQ("192.0.2.123", offer->getSiaddr().toText());
+}
+
+// Checks if the next-server defined as global value is overridden by subnet
+// specific value and returned in server messages. There's also similar test for
+// checking parser only configuration, see Dhcp4ParserTest.nextServerOverride in
+// config_parser_unittest.cc.
+TEST_F(Dhcpv4SrvTest, nextServerOverride) {
+
+    NakedDhcpv4Srv srv(0);
+
+    ConstElementPtr status;
+
+    string config = "{ \"interfaces\": [ \"*\" ],"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"next-server\": \"192.0.0.1\", "
+        "\"subnet4\": [ { "
+        "    \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+        "    \"next-server\": \"1.2.3.4\", "
+        "    \"subnet\": \"192.0.2.0/24\" } ],"
+        "\"valid-lifetime\": 4000 }";
+
+    ElementPtr json = Element::fromJSON(config);
+
+    EXPECT_NO_THROW(status = configureDhcp4Server(srv, json));
+
+    // check if returned status is OK
+    ASSERT_TRUE(status);
+    comment_ = config::parseAnswer(rcode_, status);
+    ASSERT_EQ(0, rcode_);
+
+    Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+    dis->setRemoteAddr(IOAddress("192.0.2.1"));
+    OptionPtr clientid = generateClientId();
+    dis->addOption(clientid);
+
+    // Pass it to the server and get an offer
+    Pkt4Ptr offer = srv.processDiscover(dis);
+    ASSERT_TRUE(offer);
+    EXPECT_EQ(DHCPOFFER, offer->getType());
+
+    EXPECT_EQ("1.2.3.4", offer->getSiaddr().toText());
+}
+
+// Checks if the next-server defined as global value is used in responses
+// when there is no specific value defined in subnet and returned to the client
+// properly. There's also similar test for checking parser only configuration,
+// see Dhcp4ParserTest.nextServerGlobal in config_parser_unittest.cc.
+TEST_F(Dhcpv4SrvTest, nextServerGlobal) {
+
+    NakedDhcpv4Srv srv(0);
+
+    ConstElementPtr status;
+
+    string config = "{ \"interfaces\": [ \"*\" ],"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"next-server\": \"192.0.0.1\", "
+        "\"subnet4\": [ { "
+        "    \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+        "    \"subnet\": \"192.0.2.0/24\" } ],"
+        "\"valid-lifetime\": 4000 }";
+
+    ElementPtr json = Element::fromJSON(config);
+
+    EXPECT_NO_THROW(status = configureDhcp4Server(srv, json));
+
+    // check if returned status is OK
+    ASSERT_TRUE(status);
+    comment_ = config::parseAnswer(rcode_, status);
+    ASSERT_EQ(0, rcode_);
+
+    Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+    dis->setRemoteAddr(IOAddress("192.0.2.1"));
+    OptionPtr clientid = generateClientId();
+    dis->addOption(clientid);
+
+    // Pass it to the server and get an offer
+    Pkt4Ptr offer = srv.processDiscover(dis);
+    ASSERT_TRUE(offer);
+    EXPECT_EQ(DHCPOFFER, offer->getType());
+
+    EXPECT_EQ("192.0.0.1", offer->getSiaddr().toText());
+}
+
+
 // a dummy MAC address
 const uint8_t dummyMacAddr[] = {0, 1, 2, 3, 4, 5};
 

+ 184 - 35
src/bin/dhcp4/tests/dhcp4_test_utils.cc

@@ -22,6 +22,7 @@
 #include <dhcp/option_custom.h>
 #include <dhcp/iface_mgr.h>
 #include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/lease.h>
 #include <dhcpsrv/lease_mgr.h>
 #include <dhcpsrv/lease_mgr_factory.h>
 
@@ -123,37 +124,101 @@ void Dhcpv4SrvTest::messageCheck(const Pkt4Ptr& q, const Pkt4Ptr& a) {
     EXPECT_TRUE(q->getLocalHWAddr() == a->getLocalHWAddr());
     EXPECT_TRUE(q->getRemoteHWAddr() == a->getRemoteHWAddr());
 
-    // Check that bare minimum of required options are there.
-    // We don't check options requested by a client. Those
-    // are checked elsewhere.
-    EXPECT_TRUE(a->getOption(DHO_SUBNET_MASK));
-    EXPECT_TRUE(a->getOption(DHO_ROUTERS));
+    // Check that the server identifier is present in the response.
+    // Presence (or absence) of other options is checked elsewhere.
     EXPECT_TRUE(a->getOption(DHO_DHCP_SERVER_IDENTIFIER));
-    EXPECT_TRUE(a->getOption(DHO_DHCP_LEASE_TIME));
-    EXPECT_TRUE(a->getOption(DHO_SUBNET_MASK));
-    EXPECT_TRUE(a->getOption(DHO_DOMAIN_NAME));
-    EXPECT_TRUE(a->getOption(DHO_DOMAIN_NAME_SERVERS));
 
     // Check that something is offered
     EXPECT_TRUE(a->getYiaddr().toText() != "0.0.0.0");
 }
 
-void Dhcpv4SrvTest::optionsCheck(const Pkt4Ptr& pkt) {
-    // Check that the requested and configured options are returned
-    // in the ACK message.
-    EXPECT_TRUE(pkt->getOption(DHO_DOMAIN_NAME))
-        << "domain-name not present in the response";
-    EXPECT_TRUE(pkt->getOption(DHO_DOMAIN_NAME_SERVERS))
-        << "dns-servers not present in the response";
-    EXPECT_TRUE(pkt->getOption(DHO_LOG_SERVERS))
-        << "log-servers not present in the response";
-    EXPECT_TRUE(pkt->getOption(DHO_COOKIE_SERVERS))
-        << "cookie-servers not present in the response";
-    // Check that the requested but not configured options are not
-    // returned in the ACK message.
-    EXPECT_FALSE(pkt->getOption(DHO_LPR_SERVERS))
-        << "domain-name present in the response but it is"
-        << " expected not to be present";
+::testing::AssertionResult
+Dhcpv4SrvTest::basicOptionsPresent(const Pkt4Ptr& pkt) {
+    std::ostringstream errmsg;
+    errmsg << "option missing in the response";
+    if (!pkt->getOption(DHO_DOMAIN_NAME)) {
+        return (::testing::AssertionFailure(::testing::Message()
+                                            << "domain-name " << errmsg));
+
+    } else if (!pkt->getOption(DHO_DOMAIN_NAME_SERVERS)) {
+        return (::testing::AssertionFailure(::testing::Message()
+                                            << "dns-servers " << errmsg));
+
+    } else if (!pkt->getOption(DHO_SUBNET_MASK)) {
+        return (::testing::AssertionFailure(::testing::Message()
+                                            << "subnet-mask " << errmsg));
+
+    } else if (!pkt->getOption(DHO_ROUTERS)) {
+        return (::testing::AssertionFailure(::testing::Message() << "routers "
+                                            << errmsg));
+
+    } else if (!pkt->getOption(DHO_DHCP_LEASE_TIME)) {
+        return (::testing::AssertionFailure(::testing::Message() <<
+                                            "dhcp-lease-time " << errmsg));
+
+    }
+    return (::testing::AssertionSuccess());
+
+}
+
+::testing::AssertionResult
+Dhcpv4SrvTest::noBasicOptions(const Pkt4Ptr& pkt) {
+    std::ostringstream errmsg;
+    errmsg << "option present in the response";
+    if (pkt->getOption(DHO_DOMAIN_NAME)) {
+        return (::testing::AssertionFailure(::testing::Message()
+                                            << "domain-name " << errmsg));
+
+    } else if (pkt->getOption(DHO_DOMAIN_NAME_SERVERS)) {
+        return (::testing::AssertionFailure(::testing::Message()
+                                            << "dns-servers " << errmsg));
+
+    } else if (pkt->getOption(DHO_SUBNET_MASK)) {
+        return (::testing::AssertionFailure(::testing::Message()
+                                            << "subnet-mask " << errmsg));
+
+    } else if (pkt->getOption(DHO_ROUTERS)) {
+        return (::testing::AssertionFailure(::testing::Message() << "routers "
+                                            << errmsg));
+
+    } else if (pkt->getOption(DHO_DHCP_LEASE_TIME)) {
+        return (::testing::AssertionFailure(::testing::Message()
+                                            << "dhcp-lease-time " << errmsg));
+
+    }
+    return (::testing::AssertionSuccess());
+}
+
+::testing::AssertionResult
+Dhcpv4SrvTest::requestedOptionsPresent(const Pkt4Ptr& pkt) {
+    std::ostringstream errmsg;
+    errmsg << "option missing in the response";
+    if (!pkt->getOption(DHO_LOG_SERVERS)) {
+        return (::testing::AssertionFailure(::testing::Message()
+                                            << "log-servers " << errmsg));
+
+    } else if (!pkt->getOption(DHO_COOKIE_SERVERS)) {
+        return (::testing::AssertionFailure(::testing::Message()
+                                            << "cookie-servers " << errmsg));
+
+    }
+    return (::testing::AssertionSuccess());
+}
+
+::testing::AssertionResult
+Dhcpv4SrvTest::noRequestedOptions(const Pkt4Ptr& pkt) {
+    std::ostringstream errmsg;
+    errmsg << "option present in the response";
+    if (pkt->getOption(DHO_LOG_SERVERS)) {
+        return (::testing::AssertionFailure(::testing::Message()
+                                            << "log-servers " << errmsg));
+
+    } else if (pkt->getOption(DHO_COOKIE_SERVERS)) {
+        return (::testing::AssertionFailure(::testing::Message()
+                                            << "cookie-servers " << errmsg));
+
+    }
+    return (::testing::AssertionSuccess());
 }
 
 OptionPtr Dhcpv4SrvTest::generateClientId(size_t size /*= 4*/) {
@@ -274,6 +339,48 @@ void Dhcpv4SrvTest::checkClientId(const Pkt4Ptr& rsp, const OptionPtr& expected_
     EXPECT_TRUE(expected_clientid->getData() == opt->getData());
 }
 
+::testing::AssertionResult
+Dhcpv4SrvTest::createPacketFromBuffer(const Pkt4Ptr& src_pkt,
+                                      Pkt4Ptr& dst_pkt) {
+    // Create on-wire format of the packet. If pack() has been called
+    // on this instance of the packet already, the next call to pack()
+    // should remove all contents of the output buffer.
+    try {
+        src_pkt->pack();
+    } catch (const Exception& ex) {
+        return (::testing::AssertionFailure(::testing::Message()
+                                            << "Failed to parse source packet: "
+                                            << ex.what()));
+    }
+    // Get the output buffer from the source packet.
+    const util::OutputBuffer& buf = src_pkt->getBuffer();
+    // Create a copy of the packet using the output buffer from the source
+    // packet.
+    try {
+        dst_pkt.reset(new Pkt4(static_cast<const uint8_t*>(buf.getData()),
+                               buf.getLength()));
+    } catch (const Exception& ex) {
+        return (::testing::AssertionFailure(::testing::Message()
+                                            << "Failed to create a"
+                                            " destination packet from"
+                                            " the buffer: "
+                                            << ex.what()));
+    }
+
+    try {
+        // Parse the new packet and return to the caller.
+        dst_pkt->unpack();
+    } catch (const Exception& ex) {
+        return (::testing::AssertionFailure(::testing::Message()
+                                            << "Failed to parse a"
+                                            << " destination packet: "
+                                            << ex.what()));
+    }
+
+    return (::testing::AssertionSuccess());
+}
+
+
 void Dhcpv4SrvTest::testDiscoverRequest(const uint8_t msg_type) {
     // Create an instance of the tested class.
     boost::scoped_ptr<NakedDhcpv4Srv> srv(new NakedDhcpv4Srv(0));
@@ -318,17 +425,22 @@ void Dhcpv4SrvTest::testDiscoverRequest(const uint8_t msg_type) {
     // are returned when requested.
     configureRequestedOptions();
 
+    // Create a copy of the original packet by parsing its wire format.
+    // This simulates the real life scenario when we process the packet
+    // which was parsed from its wire format.
+    Pkt4Ptr received;
+    ASSERT_TRUE(createPacketFromBuffer(req, received));
     if (msg_type == DHCPDISCOVER) {
         ASSERT_NO_THROW(
-            rsp = srv->processDiscover(req);
-            );
+            rsp = srv->processDiscover(received);
+        );
 
         // Should return OFFER
         ASSERT_TRUE(rsp);
         EXPECT_EQ(DHCPOFFER, rsp->getType());
 
     } else {
-        ASSERT_NO_THROW(rsp = srv->processRequest(req););
+        ASSERT_NO_THROW(rsp = srv->processRequest(received));
 
         // Should return ACK
         ASSERT_TRUE(rsp);
@@ -336,27 +448,30 @@ void Dhcpv4SrvTest::testDiscoverRequest(const uint8_t msg_type) {
 
     }
 
-    messageCheck(req, rsp);
+    messageCheck(received, rsp);
 
+    // Basic options should be present when we got the lease.
+    EXPECT_TRUE(basicOptionsPresent(rsp));
     // We did not request any options so these should not be present
     // in the RSP.
-    EXPECT_FALSE(rsp->getOption(DHO_LOG_SERVERS));
-    EXPECT_FALSE(rsp->getOption(DHO_COOKIE_SERVERS));
-    EXPECT_FALSE(rsp->getOption(DHO_LPR_SERVERS));
+    EXPECT_TRUE(noRequestedOptions(rsp));
 
     // Repeat the test but request some options.
     // Add 'Parameter Request List' option.
     addPrlOption(req);
 
+    ASSERT_TRUE(createPacketFromBuffer(req, received));
+    ASSERT_TRUE(received->getOption(DHO_DHCP_PARAMETER_REQUEST_LIST));
+
     if (msg_type == DHCPDISCOVER) {
-        ASSERT_NO_THROW(rsp = srv->processDiscover(req););
+        ASSERT_NO_THROW(rsp = srv->processDiscover(received));
 
         // Should return non-NULL packet.
         ASSERT_TRUE(rsp);
         EXPECT_EQ(DHCPOFFER, rsp->getType());
 
     } else {
-        ASSERT_NO_THROW(rsp = srv->processRequest(req););
+        ASSERT_NO_THROW(rsp = srv->processRequest(received));
 
         // Should return non-NULL packet.
         ASSERT_TRUE(rsp);
@@ -364,7 +479,41 @@ void Dhcpv4SrvTest::testDiscoverRequest(const uint8_t msg_type) {
     }
 
     // Check that the requested options are returned.
-    optionsCheck(rsp);
+    EXPECT_TRUE(basicOptionsPresent(rsp));
+    EXPECT_TRUE(requestedOptionsPresent(rsp));
+
+    // The following part of the test will test that the NAK is sent when
+    // there is no address pool configured. In the same time, we expect
+    // that the requested options are not included in NAK message, but that
+    // they are only included when yiaddr is set to non-zero value.
+    ASSERT_NO_THROW(subnet_->delPools(Lease::TYPE_V4));
+
+    // There has been a lease allocated for the particular client. So,
+    // even though we deleted the subnet, the client would get the
+    // existing lease (not a NAK). Therefore, we have to change the chaddr
+    // in the packet so as the existing lease is not returned.
+    req->setHWAddr(1, 6, std::vector<uint8_t>(2, 6));
+    ASSERT_TRUE(createPacketFromBuffer(req, received));
+    ASSERT_TRUE(received->getOption(DHO_DHCP_PARAMETER_REQUEST_LIST));
+
+    if (msg_type == DHCPDISCOVER) {
+        ASSERT_NO_THROW(rsp = srv->processDiscover(received));
+        // Should return non-NULL packet.
+        ASSERT_TRUE(rsp);
+    } else {
+        ASSERT_NO_THROW(rsp = srv->processRequest(received));
+        // Should return non-NULL packet.
+        ASSERT_TRUE(rsp);
+    }
+    // We should get the NAK packet with yiaddr set to 0.
+    EXPECT_EQ(DHCPNAK, rsp->getType());
+    ASSERT_EQ("0.0.0.0", rsp->getYiaddr().toText());
+
+    // Make sure that none of the requested options is returned in NAK.
+    // Also options such as Routers or Subnet Mask should not be there,
+    // because lease hasn't been acquired.
+    EXPECT_TRUE(noRequestedOptions(rsp));
+    EXPECT_TRUE(noBasicOptions(rsp));
 }
 
 /// @brief This function cleans up after the test.

+ 59 - 3
src/bin/dhcp4/tests/dhcp4_test_utils.h

@@ -109,10 +109,32 @@ public:
     /// @param a answer (server's message)
     void messageCheck(const Pkt4Ptr& q, const Pkt4Ptr& a);
 
-    /// @brief Check that requested options are present.
+    /// @brief Check that certain basic (always added when lease is acquired)
+    /// are present in a message.
     ///
-    /// @param pkt packet to be checked.
-    void optionsCheck(const Pkt4Ptr& pkt);
+    /// @param pkt A message to be checked.
+    /// @return Assertion result which indicates whether test passed or failed.
+    ::testing::AssertionResult basicOptionsPresent(const Pkt4Ptr& pkt);
+
+    /// @brief Check that certain basic (always added when lease is acquired)
+    /// are not present.
+    ///
+    /// @param pkt A packet to be checked.
+    /// @return Assertion result which indicates whether test passed or failed.
+    ::testing::AssertionResult noBasicOptions(const Pkt4Ptr& pkt);
+
+    /// @brief Check that certain requested options are present in the message.
+    ///
+    /// @param pkt A message to be checked.
+    /// @return Assertion result which indicates whether test passed or failed.
+    ::testing::AssertionResult requestedOptionsPresent(const Pkt4Ptr& pkt);
+
+    /// @brief Check that certain options (requested with PRL option)
+    /// are not present.
+    ///
+    /// @param pkt A packet to be checked.
+    /// @return Assertion result which indicates whether test passed or failed.
+    ::testing::AssertionResult noRequestedOptions(const Pkt4Ptr& pkt);
 
     /// @brief generates client-id option
     ///
@@ -183,6 +205,32 @@ public:
     /// @return relayed DISCOVER
     Pkt4Ptr captureRelayedDiscover();
 
+    /// @brief Create packet from output buffer of another packet.
+    ///
+    /// This function creates a packet using an output buffer from another
+    /// packet. This imitates reception of a packet from the wire. The
+    /// unpack function is then called to parse packet contents and to
+    /// create a collection of the options carried by this packet.
+    ///
+    /// This function is useful for unit tests which verify that the received
+    /// packet is parsed correctly. In those cases it is usually inappropriate
+    /// to create an instance of the packet, add options, set packet
+    /// fields and use such packet as an input to processDiscover or
+    /// processRequest function. This is because, such a packet has certain
+    /// options already initialized and there is no way to verify that they
+    /// have been initialized when packet instance was created or wire buffer
+    /// processing. By creating a packet from the buffer we guarantee that the
+    /// new packet is entirely initialized during wire data parsing.
+    ///
+    /// @param src_pkt A source packet, to be copied.
+    /// @param [out] dst_pkt A destination packet.
+    ///
+    /// @return assertion result indicating if a function completed with
+    /// success or failure.
+    static ::testing::AssertionResult
+    createPacketFromBuffer(const Pkt4Ptr& src_pkt,
+                           Pkt4Ptr& dst_pkt);
+
     /// @brief generates a DHCPv4 packet based on provided hex string
     ///
     /// @return created packet
@@ -190,6 +238,14 @@ public:
 
     /// @brief Tests if Discover or Request message is processed correctly
     ///
+    /// This test verifies that the Parameter Request List option is handled
+    /// correctly, i.e. it checks that certain options are present in the
+    /// server's response when they are requested and that they are not present
+    /// when they are not requested or NAK occurs.
+    ///
+    /// @todo We need an additional test for PRL option using real traffic
+    /// capture.
+    ///
     /// @param msg_type DHCPDISCOVER or DHCPREQUEST
     void testDiscoverRequest(const uint8_t msg_type);
 

+ 1 - 1
src/bin/dhcp6/ctrl_dhcp6_srv.cc

@@ -216,7 +216,7 @@ void ControlledDhcpv6Srv::establishSession() {
         // reopen sockets according to new configuration.
         openActiveSockets(getPort());
 
-    } catch (const DhcpConfigError& ex) {
+    } catch (const std::exception& ex) {
         LOG_ERROR(dhcp6_logger, DHCP6_CONFIG_LOAD_FAIL).arg(ex.what());
 
     }

+ 3 - 0
src/bin/dhcp6/dhcp6_messages.mes

@@ -450,6 +450,9 @@ This debug message indicates that a shutdown of the IPv6 server has
 been requested via a call to the 'shutdown' method of the core Dhcpv6Srv
 object.
 
+% DHCP6_SOCKET_UNICAST server is about to open socket on address %1 on interface %2
+This is a debug message that inform that a unicast socket will be opened.
+
 % DHCP6_SRV_CONSTRUCT_ERROR error creating Dhcpv6Srv object, reason: %1
 This error message indicates that during startup, the construction of a
 core component within the IPv6 DHCP server (the Dhcpv6 server object)

+ 10 - 1
src/bin/dhcp6/dhcp6_srv.cc

@@ -2284,7 +2284,7 @@ Dhcpv6Srv::openActiveSockets(const uint16_t port) {
                       << " trying to reopen sockets after reconfiguration");
         }
         if (CfgMgr::instance().isActiveIface(iface->getName())) {
-            iface_ptr->inactive4_ = false;
+            iface_ptr->inactive6_ = false;
             LOG_INFO(dhcp6_logger, DHCP6_ACTIVATE_INTERFACE)
                 .arg(iface->getFullName());
 
@@ -2297,6 +2297,15 @@ Dhcpv6Srv::openActiveSockets(const uint16_t port) {
             iface_ptr->inactive6_ = true;
 
         }
+
+        iface_ptr->clearUnicasts();
+
+        const IOAddress* unicast = CfgMgr::instance().getUnicast(iface->getName());
+        if (unicast) {
+            LOG_INFO(dhcp6_logger, DHCP6_SOCKET_UNICAST).arg(unicast->toText())
+                .arg(iface->getName());
+            iface_ptr->addUnicast(*unicast);
+        }
     }
     // Let's reopen active sockets. openSockets6 will check internally whether
     // sockets are marked active or inactive.

+ 3 - 1
src/bin/msgq/msgq.py.in

@@ -525,7 +525,9 @@ class MsgQ:
             # Append it to buffer (but check the data go away)
             if fileno in self.sendbuffs:
                 (last_sent, buff) = self.sendbuffs[fileno]
-                if now - last_sent > 0.1:
+                tdelta = now - last_sent
+                if tdelta > 0.1:
+                    logger.error(MSGQ_SOCKET_TIMEOUT_ERROR, fileno, tdelta)
                     self.kill_socket(fileno, sock)
                     return False
                 buff += msg

+ 6 - 0
src/bin/msgq/msgq_messages.mes

@@ -142,3 +142,9 @@ data structure.
 % MSGQ_SUBS_NEW_TARGET Creating new target for subscription to group '%1' for instance '%2'
 Debug message. Creating a new subscription. Also creating a new data structure
 to hold it.
+
+% MSGQ_SOCKET_TIMEOUT_ERROR Killing socket %1 because timeout exceeded (%2)
+Outgoing data was queued up on a socket connected to msgq, but the other
+side is not reading it. It could be deadlocked, or may not be monitoring
+it. Both cases are programming errors and should be corrected. The socket
+is closed on the msgq side.

+ 1 - 0
src/hooks/Makefile.am

@@ -0,0 +1 @@
+SUBDIRS = dhcp

+ 1 - 0
src/hooks/dhcp/Makefile.am

@@ -0,0 +1 @@
+SUBDIRS = user_chk

+ 65 - 0
src/hooks/dhcp/user_chk/Makefile.am

@@ -0,0 +1,65 @@
+SUBDIRS = . tests
+
+AM_CPPFLAGS  = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
+AM_CPPFLAGS += $(BOOST_INCLUDES)
+AM_CXXFLAGS  = $(B10_CXXFLAGS)
+
+# Some versions of GCC warn about some versions of Boost regarding
+# missing initializer for members in its posix_time.
+# https://svn.boost.org/trac/boost/ticket/3477
+# But older GCC compilers don't have the flag.
+AM_CXXFLAGS += $(WARNING_NO_MISSING_FIELD_INITIALIZERS_CFLAG)
+
+# Until logging in dynamically loaded libraries is fixed,
+# Define rule to build logging source files from message file
+# user_chk_messages.h user_chk_messages.cc: s-messages
+
+# Until logging in dynamically loaded libraries is fixed, exclude these.
+# s-messages: user_chk_messages.mes
+#	$(top_builddir)/src/lib/log/compiler/message $(top_srcdir)/src/hooks/dhcp/user_chk/user_chk_messages.mes
+#	touch $@
+
+# Tell automake that the message files are built as part of the build process
+# (so that they are built before the main library is built).
+# BUILT_SOURCES = user_chk_messages.h user_chk_messages.cc
+BUILT_SOURCES =
+
+# Ensure that the message file is included in the distribution
+EXTRA_DIST =
+
+# Get rid of generated message files on a clean
+#CLEANFILES = *.gcno *.gcda user_chk_messages.h user_chk_messages.cc s-messages
+CLEANFILES = *.gcno *.gcda
+
+lib_LTLIBRARIES = libdhcp_user_chk.la
+libdhcp_user_chk_la_SOURCES  =
+libdhcp_user_chk_la_SOURCES += load_unload.cc
+libdhcp_user_chk_la_SOURCES += subnet_select_co.cc
+libdhcp_user_chk_la_SOURCES += user.cc user.h
+# Until logging in dynamically loaded libraries is fixed, exclude these.
+#libdhcp_user_chk_la_SOURCES += user_chk_log.cc user_chk_log.h
+libdhcp_user_chk_la_SOURCES += user_data_source.h
+libdhcp_user_chk_la_SOURCES += user_file.cc user_file.h
+libdhcp_user_chk_la_SOURCES += user_registry.cc user_registry.h
+libdhcp_user_chk_la_SOURCES += version.cc
+
+# Until logging in dynamically loaded libraries is fixed, exclude these.
+#nodist_libdhcp_user_chk_la_SOURCES = user_chk_messages.cc user_chk_messages.h
+
+libdhcp_user_chk_la_CXXFLAGS = $(AM_CXXFLAGS)
+libdhcp_user_chk_la_CPPFLAGS = $(AM_CPPFLAGS) $(LOG4CPLUS_INCLUDES)
+libdhcp_user_chk_la_LDFLAGS  = $(AM_LDFLAGS)
+libdhcp_user_chk_la_LDFLAGS  += -avoid-version -export-dynamic -module
+libdhcp_user_chk_la_LIBADD  =
+libdhcp_user_chk_la_LIBADD  += $(top_builddir)/src/lib/hooks/libb10-hooks.la
+libdhcp_user_chk_la_LIBADD  += $(top_builddir)/src/lib/log/libb10-log.la
+libdhcp_user_chk_la_LIBADD  += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
+libdhcp_user_chk_la_LIBADD  += $(top_builddir)/src/lib/util/libb10-util.la
+libdhcp_user_chk_la_LIBADD  += $(top_builddir)/src/lib/util/threads/libb10-threads.la
+
+
+if USE_CLANGPP
+# Disable unused parameter warning caused by some of the
+# Boost headers when compiling with clang.
+libdhcp_user_chk_la_CXXFLAGS += -Wno-unused-parameter
+endif

+ 113 - 0
src/hooks/dhcp/user_chk/load_unload.cc

@@ -0,0 +1,113 @@
+// 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.
+
+/// @file This file defines the load and unload hooks library functions.
+
+#include <hooks/hooks.h>
+#include <user_registry.h>
+#include <user_file.h>
+
+#include <iostream>
+#include <fstream>
+#include <errno.h>
+
+using namespace isc::hooks;
+
+/// @brief Pointer to the registry instance.
+UserRegistryPtr user_registry;
+
+/// @brief Output filestream for recording user check outcomes.
+std::fstream user_chk_output;
+
+/// @brief User registry input file name.
+/// @todo Hard-coded for now, this should be configurable.
+const char* registry_fname = "/tmp/user_chk_registry.txt";
+
+/// @brief User check outcome file name.
+/// @todo Hard-coded for now, this should be configurable.
+const char* user_chk_output_fname = "/tmp/user_chk_outcome.txt";
+
+// Functions accessed by the hooks framework use C linkage to avoid the name
+// mangling that accompanies use of the C++ compiler as well as to avoid
+// issues related to namespaces.
+extern "C" {
+
+/// @brief Called by the Hooks library manager when the library is loaded.
+///
+/// Instantiates the UserRegistry and opens the outcome file. Failure in
+/// either results in a failed return code.
+///
+/// @param unused library handle parameter required by Hooks API.
+///
+/// @return Returns 0 upon success, non-zero upon failure.
+int load(LibraryHandle&) {
+    // non-zero indicates an error.
+    int ret_val = 0;
+    try {
+        // Instantiate the registry.
+        user_registry.reset(new UserRegistry());
+
+        // Create the data source.
+        UserDataSourcePtr user_file(new UserFile(registry_fname));
+
+        // Set the registry's data source
+        user_registry->setSource(user_file);
+
+        // Do an initial load of the registry.
+        user_registry->refresh();
+
+        // Open up the output file for user_chk results.
+        user_chk_output.open(user_chk_output_fname,
+                     std::fstream::out | std::fstream::app);
+
+        if (!user_chk_output) {
+            // Grab the system error message.
+            const char* errmsg = strerror(errno);
+            isc_throw(isc::Unexpected, "Cannot open output file: "
+                                       << user_chk_output_fname
+                                       << " reason: " << errmsg);
+        }
+    }
+    catch (const std::exception& ex) {
+        // Log the error and return failure.
+        std::cout << "DHCP UserCheckHook could not be loaded: "
+                  << ex.what() << std::endl;
+        ret_val = 1;
+    }
+
+    return (ret_val);
+}
+
+/// @brief Called by the Hooks library manager when the library is unloaded.
+///
+/// Destroys the UserRegistry and closes the outcome file.
+///
+/// @return Always returns 0.
+int unload() {
+    try {
+        user_registry.reset();
+        if (user_chk_output.is_open()) {
+            user_chk_output.close();
+        }
+    } catch (const std::exception& ex) {
+        // On the off chance something goes awry, catch it and log it.
+        // @todo Not sure if we should return a non-zero result or not.
+        std::cout << "DHCP UserCheckHook could not be unloaded: "
+                  << ex.what() << std::endl;
+    }
+
+    return (0);
+}
+
+}

+ 237 - 0
src/hooks/dhcp/user_chk/subnet_select_co.cc

@@ -0,0 +1,237 @@
+// 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.
+
+/// @file Defines the subnet4_select and subnet6_select callout functions.
+
+#include <hooks/hooks.h>
+#include <dhcp/pkt4.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/pkt6.h>
+#include <dhcpsrv/subnet.h>
+#include <user_registry.h>
+
+#include <fstream>
+#include <string>
+
+using namespace isc::dhcp;
+using namespace isc::hooks;
+using namespace std;
+
+extern UserRegistryPtr user_registry;
+extern std::fstream user_chk_output;
+extern const char* registry_fname;
+extern const char* user_chk_output_fname;
+
+// Functions accessed by the hooks framework use C linkage to avoid the name
+// mangling that accompanies use of the C++ compiler as well as to avoid
+// issues related to namespaces.
+extern "C" {
+
+/// @brief Adds an entry to the end of the user check outcome file.
+///
+/// Each user entry is written in an ini-like format, with one name-value pair
+/// per line as follows:
+///
+/// id_type=<id type>
+/// client=<id str>
+/// subnet=<subnet str>
+/// registered=<is registered>"
+///
+/// where:
+/// <id type> text label of the id type: "HW_ADDR" or "DUID"
+/// <id str> user's id formatted as either isc::dhcp::Hwaddr.toText() or
+/// isc::dhcp::DUID.toText()
+/// <subnet str> selected subnet formatted as isc::dhcp::Subnet4::toText() or
+/// isc::dhcp::Subnet6::toText() as appropriate.
+/// <is registered> "yes" or "no"
+///
+/// Sample IPv4 entry would like this:
+///
+/// @code
+/// id_type=DUID
+/// client=00:01:00:01:19:ef:e6:3b:00:0c:01:02:03:04
+/// subnet=2001:db8:2::/64
+/// registered=yes
+/// id_type=duid
+/// @endcode
+///
+/// Sample IPv4 entry would like this:
+///
+/// @code
+/// id_type=DUID
+/// id_type=HW_ADDR
+/// client=hwtype=1 00:0c:01:02:03:05
+/// subnet=152.77.5.0/24
+/// registered=no
+/// @endcode
+///
+/// @param id_type_str text label identify the id type
+/// @param id_val_str text representation of the user id
+/// @param subnet_str text representation  of the selected subnet
+/// @param registered boolean indicating if the user is registered or not
+void generate_output_record(const std::string& id_type_str,
+                            const std::string& id_val_str,
+                            const std::string& subnet_str,
+                            const bool& registered)
+{
+    user_chk_output << "id_type=" << id_type_str << std::endl
+                    << "client=" << id_val_str << std::endl
+                    << "subnet=" << subnet_str << std::endl
+                    << "registered=" << (registered ? "yes" : "no")
+                    << std::endl;
+
+    // @todo Flush is here to ensure output is immediate for demo purposes.
+    // Performance would generally dictate not using it.
+    flush(user_chk_output);
+}
+
+/// @brief  This callout is called at the "subnet4_select" hook.
+///
+/// This function searches the UserRegistry for the client indicated by the
+/// inbound IPv4 DHCP packet. If the client is found in the registry output
+/// the generate outcome record and return.
+///
+/// If the client is not found in the registry replace the selected subnet
+/// with the restricted access subnet, then generate the outcome record and
+/// return.  By convention, it is assumed that last subnet in the list of
+/// available subnets is the restricted access subnet.
+///
+/// @param handle CalloutHandle which provides access to context.
+///
+/// @return 0 upon success, non-zero otherwise.
+int subnet4_select(CalloutHandle& handle) {
+    if (!user_registry) {
+        std::cout << "DHCP UserCheckHook : subnet4_select UserRegistry is null"
+                  << std::endl;
+        return (1);
+    }
+
+    try {
+        // Get subnet collection. If it's empty just bail nothing to do.
+        const isc::dhcp::Subnet4Collection *subnets = NULL;
+        handle.getArgument("subnet4collection", subnets);
+        if (subnets->size() == 0) {
+            return 0;
+        }
+
+        // Refresh the registry.
+        user_registry->refresh();
+
+        // Get the HWAddress as the user identifier.
+        Pkt4Ptr query;
+        handle.getArgument("query4", query);
+        HWAddrPtr hwaddr = query->getHWAddr();
+
+        // Look for the user.
+        UserPtr registered_user = user_registry->findUser(*hwaddr);
+        if (registered_user) {
+            // User is in the registry, so leave the pre-selected subnet alone.
+            Subnet4Ptr subnet;
+            handle.getArgument("subnet4", subnet);
+            // Add the outcome entry to the output file.
+            generate_output_record(UserId::HW_ADDRESS_STR, hwaddr->toText(),
+                                   subnet->toText(), true);
+        } else {
+            // User is not in the registry, so assign them to the last subnet
+            // in the collection.  By convention we are assuming this is the
+            // restricted subnet.
+            Subnet4Ptr subnet = subnets->back();
+            handle.setArgument("subnet4", subnet);
+            // Add the outcome entry to the output file.
+            generate_output_record(UserId::HW_ADDRESS_STR, hwaddr->toText(),
+                                   subnet->toText(), false);
+        }
+    } catch (const std::exception& ex) {
+        std::cout << "DHCP UserCheckHook : subnet6_select unexpected error: "
+                  << ex.what() << std::endl;
+        return (1);
+    }
+
+    return (0);
+}
+
+/// @brief  This callout is called at the "subnet6_select" hook.
+///
+/// This function searches the UserRegistry for the client indicated by the
+/// inbound IPv6 DHCP packet. If the client is found in the registry generate
+/// the outcome record and return.
+///
+/// If the client is not found in the registry replace the selected subnet
+/// with the restricted access subnet, then generate the outcome record and
+/// return.  By convention, it is assumed that last subnet in the list of
+/// available subnets is the restricted access subnet.
+///
+/// @param handle CalloutHandle which provides access to context.
+///
+/// @return 0 upon success, non-zero otherwise.
+int subnet6_select(CalloutHandle& handle) {
+    if (!user_registry) {
+        std::cout << "DHCP UserCheckHook : subnet6_select UserRegistry is null"
+                  << std::endl;
+        return (1);
+    }
+
+    try {
+        // Get subnet collection. If it's empty just bail nothing to do.
+        const isc::dhcp::Subnet6Collection *subnets = NULL;
+        handle.getArgument("subnet6collection", subnets);
+        if (subnets->size() == 0) {
+            return 0;
+        }
+
+        // Refresh the registry.
+        user_registry->refresh();
+
+        // Get the HWAddress as the user identifier.
+        Pkt6Ptr query;
+        handle.getArgument("query6", query);
+
+        DuidPtr duid;
+        OptionPtr opt_duid = query->getOption(D6O_CLIENTID);
+        if (!opt_duid) {
+            std::cout << "DHCP6 query is missing DUID" << std::endl;
+            return (1);
+        }
+
+        duid = DuidPtr(new DUID(opt_duid->getData()));
+
+        // Look for the user.
+        UserPtr registered_user = user_registry->findUser(*duid);
+        if (registered_user) {
+            // User is in the registry, so leave the pre-selected subnet alone.
+            Subnet6Ptr subnet;
+            handle.getArgument("subnet6", subnet);
+            // Add the outcome entry to the output file.
+            generate_output_record(UserId::DUID_STR, duid->toText(),
+                                   subnet->toText(), true);
+        } else {
+            // User is not in the registry, so assign them to the last subnet
+            // in the collection.  By convention we are assuming this is the
+            // restricted subnet.
+            Subnet6Ptr subnet = subnets->back();
+            handle.setArgument("subnet6", subnet);
+            // Add the outcome entry to the output file.
+            generate_output_record(UserId::DUID_STR, duid->toText(),
+                                   subnet->toText(), false);
+        }
+    } catch (const std::exception& ex) {
+        std::cout << "DHCP UserCheckHook : subnet6_select unexpected error: "
+                  << ex.what() << std::endl;
+        return (1);
+    }
+
+    return (0);
+}
+
+}

+ 73 - 0
src/hooks/dhcp/user_chk/tests/Makefile.am

@@ -0,0 +1,73 @@
+SUBDIRS = .
+
+AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
+AM_CPPFLAGS += -I$(top_builddir)/src/hooks/dhcp/user_chk -I$(top_srcdir)/src/hooks/dhcp/user_chk
+AM_CPPFLAGS += $(BOOST_INCLUDES) $(BOTAN_INCLUDES)
+AM_CPPFLAGS += -DTEST_DATA_BUILDDIR=\"$(abs_top_srcdir)/src/hooks/dhcp/user_chk/tests\"
+AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\"
+
+AM_CXXFLAGS = $(B10_CXXFLAGS)
+
+# Some versions of GCC warn about some versions of Boost regarding
+# missing initializer for members in its posix_time.
+# https://svn.boost.org/trac/boost/ticket/3477
+# But older GCC compilers don't have the flag.
+AM_CXXFLAGS += $(WARNING_NO_MISSING_FIELD_INITIALIZERS_CFLAG)
+
+USER_CHK_LIB = $(top_builddir)/src/hooks/dhcp/user_chk/libdhcp_user_chk.la
+
+if USE_STATIC_LINK
+AM_LDFLAGS = -static
+endif
+
+# Unit test data files need to get installed.
+EXTRA_DIST = test_users_1.txt test_users_err.txt
+
+CLEANFILES = *.gcno *.gcda
+
+TESTS_ENVIRONMENT = \
+	$(LIBTOOL) --mode=execute $(VALGRIND_COMMAND)
+
+TESTS =
+if HAVE_GTEST
+TESTS += libdhcp_user_chk_unittests
+
+libdhcp_user_chk_unittests_SOURCES  = 
+libdhcp_user_chk_unittests_SOURCES += ../load_unload.cc
+libdhcp_user_chk_unittests_SOURCES += ../subnet_select_co.cc
+libdhcp_user_chk_unittests_SOURCES += ../version.cc
+libdhcp_user_chk_unittests_SOURCES += ../user.cc ../user.h
+# Until logging in dynamically loaded libraries is fixed, exclude these.
+#libdhcp_user_chk_unittests_SOURCES += ../user_chk_log.cc ../user_chk_log.h
+#libdhcp_user_chk_unittests_SOURCES += ../user_chk_messages.cc ../user_chk_messages.h
+libdhcp_user_chk_unittests_SOURCES += ../user_data_source.h
+libdhcp_user_chk_unittests_SOURCES += ../user_file.cc ../user_file.h
+libdhcp_user_chk_unittests_SOURCES += ../user_registry.cc ../user_registry.h
+libdhcp_user_chk_unittests_SOURCES += run_unittests.cc
+libdhcp_user_chk_unittests_SOURCES += userid_unittests.cc
+libdhcp_user_chk_unittests_SOURCES += user_unittests.cc
+libdhcp_user_chk_unittests_SOURCES += user_registry_unittests.cc
+libdhcp_user_chk_unittests_SOURCES += user_file_unittests.cc
+
+libdhcp_user_chk_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) $(LOG4CPLUS_INCLUDES)
+
+libdhcp_user_chk_unittests_LDFLAGS  = $(AM_LDFLAGS)  $(GTEST_LDFLAGS)
+
+libdhcp_user_chk_unittests_CXXFLAGS = $(AM_CXXFLAGS)
+if USE_CLANGPP
+# This is to workaround unused variables tcout and tcerr in
+# log4cplus's streams.h and unused parameters from some of the
+# Boost headers.
+libdhcp_user_chk_unittests_CXXFLAGS += -Wno-unused-parameter
+endif
+
+libdhcp_user_chk_unittests_LDADD = $(top_builddir)/src/lib/log/libb10-log.la
+libdhcp_user_chk_unittests_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
+libdhcp_user_chk_unittests_LDADD += $(top_builddir)/src/lib/hooks/libb10-hooks.la
+libdhcp_user_chk_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libb10-dhcp++.la
+libdhcp_user_chk_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
+libdhcp_user_chk_unittests_LDADD += $(top_builddir)/src/lib/cc/libb10-cc.la
+libdhcp_user_chk_unittests_LDADD += ${BOTAN_LIBS} ${BOTAN_RPATH}
+libdhcp_user_chk_unittests_LDADD += $(GTEST_LDADD)
+endif
+noinst_PROGRAMS = $(TESTS)

+ 27 - 0
src/hooks/dhcp/user_chk/tests/run_unittests.cc

@@ -0,0 +1,27 @@
+// 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 <log/logger_support.h>
+
+#include <gtest/gtest.h>
+
+int
+main(int argc, char* argv[]) {
+    ::testing::InitGoogleTest(&argc, argv);
+    isc::log::initLogger();
+
+    int result = RUN_ALL_TESTS();
+
+    return (result);
+}

+ 16 - 0
src/hooks/dhcp/user_chk/tests/test_data_files_config.h.in

@@ -0,0 +1,16 @@
+// 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.
+//
+/// @brief Path to local dir so tests can locate test data files 
+#define USER_CHK_TEST_DIR "@abs_top_srcdir@/src/hooks/dhcp/user_chk/tests"

+ 2 - 0
src/hooks/dhcp/user_chk/tests/test_users_1.txt

@@ -0,0 +1,2 @@
+{ "type" : "HW_ADDR", "id" : "01AC00F03344", "opt1" : "true" }
+{ "type" : "DUID", "id" : "225060de0a0b", "opt1" : "true" }

+ 2 - 0
src/hooks/dhcp/user_chk/tests/test_users_err.txt

@@ -0,0 +1,2 @@
+{ "type" : "DUID", "id" : "777777777777", "opt1" : "true" }
+{ "type" : "BOGUS", "id" : "01AC00F03344", "opt1" : "true" }

+ 158 - 0
src/hooks/dhcp/user_chk/tests/user_file_unittests.cc

@@ -0,0 +1,158 @@
+// 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 <exceptions/exceptions.h>
+#include <test_data_files_config.h>
+#include <user_file.h>
+
+#include <boost/function.hpp>
+#include <boost/bind.hpp>
+#include <boost/shared_ptr.hpp>
+#include <gtest/gtest.h>
+
+using namespace std;
+
+namespace {
+
+/// @brief Convenience method for reliably building test file path names.
+///
+/// Function prefixes the given file name with a path to unit tests directory
+/// so we can reliably find test data files.
+///
+/// @param name base file name of the test file
+std::string testFilePath(const std::string& name) {
+    return (std::string(USER_CHK_TEST_DIR) + "/" + name);
+}
+
+/// @brief Tests the UserFile constructor.
+TEST(UserFile, construction) {
+    // Verify that a UserFile with no file name is rejected.
+    ASSERT_THROW(UserFile(""), UserFileError);
+
+    // Verify that a UserFile with a non-blank file name is accepted.
+    ASSERT_NO_THROW(UserFile("someName"));
+}
+
+/// @brief Tests opening and closing UserFile
+TEST(UserFile, openFile) {
+    UserFilePtr user_file;
+
+    // Construct a user file that refers to a non existant file.
+    ASSERT_NO_THROW(user_file.reset(new UserFile("NoSuchFile")));
+    EXPECT_FALSE(user_file->isOpen());
+
+    // Verify a non-existant file fails to open
+    ASSERT_THROW(user_file->open(), UserFileError);
+    EXPECT_FALSE(user_file->isOpen());
+
+    // Construct a user file that should exist.
+    ASSERT_NO_THROW(user_file.reset(new UserFile
+                                   (testFilePath("test_users_1.txt"))));
+    // File should not be open.
+    EXPECT_FALSE(user_file->isOpen());
+
+    // Verify that we can open it.
+    ASSERT_NO_THROW(user_file->open());
+    EXPECT_TRUE(user_file->isOpen());
+
+    // Verify that we cannot open an already open file.
+    ASSERT_THROW(user_file->open(), UserFileError);
+
+    // Verifyt we can close it.
+    ASSERT_NO_THROW(user_file->close());
+    EXPECT_FALSE(user_file->isOpen());
+
+    // Verify that we can reopen it.
+    ASSERT_NO_THROW(user_file->open());
+    EXPECT_TRUE(user_file->isOpen());
+}
+
+
+/// @brief Tests makeUser with invalid user strings
+TEST(UserFile, makeUser) {
+    const char* invalid_strs[]= {
+        // Missinge type element.
+        "{ \"id\" : \"01AC00F03344\" }",
+        // Invalid id type string value.
+        "{ \"type\" : \"BOGUS\", \"id\" : \"01AC00F03344\"}",
+        // Non-string id type
+        "{ \"type\" : 1, \"id\" : \"01AC00F03344\"}",
+        // Missing id element.
+        "{ \"type\" : \"HW_ADDR\" }",
+        // Odd number of digits in id value.
+        "{ \"type\" : \"HW_ADDR\", \"id\" : \"1AC00F03344\"}",
+        // Invalid characters in id value.
+        "{ \"type\" : \"HW_ADDR\", \"id\" : \"THIS IS BAD!\"}",
+        // Empty id value.
+        "{ \"type\" : \"HW_ADDR\", \"id\" : \"\"}",
+        // Non-string id.
+        "{ \"type\" : \"HW_ADDR\", \"id\" : 01AC00F03344 }",
+        // Option with non-string value
+        "{ \"type\" : \"HW_ADDR\", \"id\" : \"01AC00F03344\", \"opt\" : 4 }",
+        NULL
+        };
+
+    // Create a UseFile to work with.
+    UserFilePtr user_file;
+    ASSERT_NO_THROW(user_file.reset(new UserFile("noFile")));
+
+    // Iterate over the list of invalid user strings and verify
+    // each one fails.
+    const char** tmp = invalid_strs;;
+    while (*tmp) {
+        EXPECT_THROW(user_file->makeUser(*tmp), UserFileError)
+                     << "Invalid str not caught: ["
+                     <<  *tmp << "]" << std::endl;
+        ++tmp;
+    }
+}
+
+/// @brief Test reading from UserFile
+TEST(UserFile, readFile) {
+    UserFilePtr user_file;
+
+    // Construct and then open a known file.
+    ASSERT_NO_THROW(user_file.reset(new UserFile
+                                    (testFilePath("test_users_1.txt"))));
+    ASSERT_NO_THROW(user_file->open());
+    EXPECT_TRUE(user_file->isOpen());
+
+    // File should contain two valid users. Read and verify each.
+    UserPtr user;
+    int i = 0;
+    do {
+        ASSERT_NO_THROW(user = user_file->readNextUser());
+        switch (i++) {
+            case 0:
+                EXPECT_EQ(UserId::HW_ADDRESS, user->getUserId().getType());
+                EXPECT_EQ("01ac00f03344", user->getUserId().toText());
+                EXPECT_EQ("true", user->getProperty("opt1"));
+                break;
+            case 1:
+                EXPECT_EQ(UserId::DUID, user->getUserId().getType());
+                EXPECT_EQ("225060de0a0b", user->getUserId().toText());
+                EXPECT_EQ("true", user->getProperty("opt1"));
+                break;
+            default:
+                // Third time around, we are at EOF User should be null.
+                ASSERT_FALSE(user);
+                break;
+        }
+    } while (user);
+
+
+    ASSERT_NO_THROW(user_file->close());
+}
+
+} // end of anonymous namespace

+ 216 - 0
src/hooks/dhcp/user_chk/tests/user_registry_unittests.cc

@@ -0,0 +1,216 @@
+// 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 <dhcp/hwaddr.h>
+#include <exceptions/exceptions.h>
+#include <user_registry.h>
+#include <user_file.h>
+#include <test_data_files_config.h>
+
+#include <boost/function.hpp>
+#include <boost/bind.hpp>
+#include <boost/shared_ptr.hpp>
+#include <gtest/gtest.h>
+
+using namespace std;
+
+namespace {
+
+/// @brief Convenience method for reliably building test file path names.
+///
+/// Function prefixes the given file name with a path to unit tests directory
+/// so we can reliably find test data files.
+///
+/// @param name base file name of the test file
+std::string testFilePath(const std::string& name) {
+    return (std::string(USER_CHK_TEST_DIR) + "/" + name);
+}
+
+/// @brief Tests UserRegistry construction.
+TEST(UserRegistry, constructor) {
+    // Currently there is only the default constructor which does not throw.
+    UserRegistryPtr reg;
+    ASSERT_NO_THROW(reg.reset(new UserRegistry()));
+}
+
+/// @brief Tests mechanics of adding, finding, removing Users.
+TEST(UserRegistry, userBasics) {
+    // Create an empty registry.
+    UserRegistryPtr reg;
+    ASSERT_NO_THROW(reg.reset(new UserRegistry()));
+
+    // Verify that a blank user cannot be added.
+    UserPtr user;
+    ASSERT_THROW(reg->addUser(user), UserRegistryError);
+
+    // Make a new id and user.
+    UserIdPtr id;
+    ASSERT_NO_THROW(id.reset(new UserId(UserId::HW_ADDRESS, "01010101")));
+    ASSERT_NO_THROW(user.reset(new User(*id)));
+
+    // Verify that we can add a user.
+    ASSERT_NO_THROW(reg->addUser(user));
+
+    // Verify that the user can be found.
+    UserPtr found_user;
+    ASSERT_NO_THROW(found_user = reg->findUser(*id));
+    EXPECT_TRUE(found_user);
+    EXPECT_EQ(found_user->getUserId(), *id);
+
+    // Verify that searching for a non-existant user returns empty user pointer.
+    UserIdPtr id2;
+    ASSERT_NO_THROW(id2.reset(new UserId(UserId::HW_ADDRESS, "02020202")));
+    ASSERT_NO_THROW(found_user = reg->findUser(*id2));
+    EXPECT_FALSE(found_user);
+
+    // Verify that the user can be deleted.
+    ASSERT_NO_THROW(reg->removeUser(*id));
+    ASSERT_NO_THROW(found_user = reg->findUser(*id));
+    EXPECT_FALSE(found_user);
+}
+
+/// @brief Tests finding users by isc::dhcp::HWaddr instance.
+TEST(UserRegistry, findByHWAddr) {
+    // Create the registry.
+    UserRegistryPtr reg;
+    ASSERT_NO_THROW(reg.reset(new UserRegistry()));
+
+    // Make a new user and add it.
+    UserPtr user;
+    ASSERT_NO_THROW(user.reset(new User(UserId::HW_ADDRESS, "01010101")));
+    ASSERT_NO_THROW(reg->addUser(user));
+
+    // Make a HWAddr instance using the same id value.
+    isc::dhcp::HWAddr hwaddr(user->getUserId().getId(), isc::dhcp::HTYPE_ETHER);
+
+    // Verify user can be found by HWAddr.
+    UserPtr found_user;
+    ASSERT_NO_THROW(found_user = reg->findUser(hwaddr));
+    EXPECT_TRUE(found_user);
+    EXPECT_EQ(found_user->getUserId(), user->getUserId());
+}
+
+/// @brief Tests finding users by isc::dhcp::DUID instance.
+TEST(UserRegistry, findByDUID) {
+    // Create the registry.
+    UserRegistryPtr reg;
+    ASSERT_NO_THROW(reg.reset(new UserRegistry()));
+
+    // Make a new user and add it.
+    UserPtr user;
+    ASSERT_NO_THROW(user.reset(new User(UserId::DUID, "01010101")));
+    ASSERT_NO_THROW(reg->addUser(user));
+
+    // Make a DUID instance using the same id value.
+    isc::dhcp::DUID duid(user->getUserId().getId());
+
+    // Verify user can be found by DUID.
+    UserPtr found_user;
+    ASSERT_NO_THROW(found_user = reg->findUser(duid));
+    EXPECT_TRUE(found_user);
+    EXPECT_EQ(found_user->getUserId(), user->getUserId());
+}
+
+/// @brief Tests mixing users of different types.
+TEST(UserRegistry, oneOfEach) {
+    // Create the registry.
+    UserRegistryPtr reg;
+    ASSERT_NO_THROW(reg.reset(new UserRegistry()));
+
+    // Make user ids.
+    UserIdPtr idh, idd;
+    ASSERT_NO_THROW(idh.reset(new UserId(UserId::HW_ADDRESS, "01010101")));
+    ASSERT_NO_THROW(idd.reset(new UserId(UserId::DUID, "03030303")));
+
+    // Make and add HW_ADDRESS user.
+    UserPtr user;
+    user.reset(new User(*idh));
+    ASSERT_NO_THROW(reg->addUser(user));
+
+    // Make and add DUID user.
+    user.reset(new User(*idd));
+    ASSERT_NO_THROW(reg->addUser(user));
+
+    // Verify we can find both.
+    UserPtr found_user;
+    ASSERT_NO_THROW(found_user = reg->findUser(*idh));
+    ASSERT_NO_THROW(found_user = reg->findUser(*idd));
+}
+
+/// @brief Tests loading the registry from a file.
+TEST(UserRegistry, refreshFromFile) {
+    // Create the registry.
+    UserRegistryPtr reg;
+    ASSERT_NO_THROW(reg.reset(new UserRegistry()));
+
+    UserDataSourcePtr user_file;
+
+    // Verify that data source cannot be set to null source.
+    ASSERT_THROW(reg->setSource(user_file), UserRegistryError);
+
+    // Create the data source.
+    ASSERT_NO_THROW(user_file.reset(new UserFile
+                                    (testFilePath("test_users_1.txt"))));
+
+    // Set the registry's data source and refresh the registry.
+    ASSERT_NO_THROW(reg->setSource(user_file));
+    ASSERT_NO_THROW(reg->refresh());
+
+    // Verify we can find all the expected users.
+    UserIdPtr id;
+    ASSERT_NO_THROW(id.reset(new UserId(UserId::HW_ADDRESS, "01ac00f03344")));
+    EXPECT_TRUE(reg->findUser(*id));
+
+    ASSERT_NO_THROW(id.reset(new UserId(UserId::DUID, "225060de0a0b")));
+    EXPECT_TRUE(reg->findUser(*id));
+}
+
+/// @brief Tests preservation of registry upon refresh failure.
+TEST(UserRegistry, refreshFail) {
+    // Create the registry.
+    UserRegistryPtr reg;
+    ASSERT_NO_THROW(reg.reset(new UserRegistry()));
+
+    // Create the data source.
+    UserDataSourcePtr user_file;
+    ASSERT_NO_THROW(user_file.reset(new UserFile
+                                    (testFilePath("test_users_1.txt"))));
+
+    // Set the registry's data source and refresh the registry.
+    ASSERT_NO_THROW(reg->setSource(user_file));
+    ASSERT_NO_THROW(reg->refresh());
+
+    // Make user ids of expected users.
+    UserIdPtr id1, id2;
+    ASSERT_NO_THROW(id1.reset(new UserId(UserId::HW_ADDRESS, "01ac00f03344")));
+    ASSERT_NO_THROW(id2.reset(new UserId(UserId::DUID, "225060de0a0b")));
+
+    // Verify we can find all the expected users.
+    EXPECT_TRUE(reg->findUser(*id1));
+    EXPECT_TRUE(reg->findUser(*id2));
+
+    // Replace original data source with a new one containing an invalid entry.
+    ASSERT_NO_THROW(user_file.reset(new UserFile
+                                    (testFilePath("test_users_err.txt"))));
+    ASSERT_NO_THROW(reg->setSource(user_file));
+
+    // Refresh should throw due to invalid data.
+    EXPECT_THROW(reg->refresh(), UserRegistryError);
+
+    // Verify we can still find all the original users.
+    EXPECT_TRUE(reg->findUser(*id1));
+    EXPECT_TRUE(reg->findUser(*id2));
+}
+
+} // end of anonymous namespace

+ 96 - 0
src/hooks/dhcp/user_chk/tests/user_unittests.cc

@@ -0,0 +1,96 @@
+// 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 <exceptions/exceptions.h>
+#include <user.h>
+
+#include <boost/function.hpp>
+#include <boost/bind.hpp>
+#include <boost/shared_ptr.hpp>
+#include <gtest/gtest.h>
+
+using namespace std;
+
+namespace {
+
+/// @brief Tests User construction variants.
+/// Note, since all constructors accept or rely on UserId, invalid id
+/// variants are tested under UserId not here.
+TEST(UserTest, construction) {
+    std::string test_address("01FF02AC030B0709");
+
+    // Create a user id.
+    UserIdPtr id;
+    ASSERT_NO_THROW(id.reset(new UserId(UserId::HW_ADDRESS, test_address)));
+
+    // Verify construction from a UserId.
+    UserPtr user;
+    ASSERT_NO_THROW(user.reset(new User(*id)));
+
+    // Verify construction from an id type and an address vector.
+    ASSERT_NO_THROW(user.reset(new User(UserId::HW_ADDRESS, id->getId())));
+    ASSERT_NO_THROW(user.reset(new User(UserId::DUID, id->getId())));
+
+    // Verify construction from an id type and an address string.
+    ASSERT_NO_THROW(user.reset(new User(UserId::HW_ADDRESS, test_address)));
+    ASSERT_NO_THROW(user.reset(new User(UserId::DUID, test_address)));
+}
+
+/// @brief Tests property map fundamentals.
+TEST(UserTest, properties) {
+    // Create a user.
+    UserPtr user;
+    ASSERT_NO_THROW(user.reset(new User(UserId::DUID, "01020304050607")));
+
+    // Verify that we can add and retrieve a property.
+    std::string value = "";
+    EXPECT_NO_THROW(user->setProperty("one","1"));
+    EXPECT_NO_THROW(value = user->getProperty("one"));
+    EXPECT_EQ("1", value);
+
+    // Verify that we can update and then retrieve the property.
+    EXPECT_NO_THROW(user->setProperty("one","1.0"));
+    EXPECT_NO_THROW(value = user->getProperty("one"));
+    EXPECT_EQ("1.0", value);
+
+    // Verify that we can remove and then NOT find the property.
+    EXPECT_NO_THROW(user->delProperty("one"));
+    EXPECT_NO_THROW(value = user->getProperty("one"));
+    EXPECT_TRUE(value.empty());
+
+    // Verify that a blank property name is NOT permitted.
+    EXPECT_THROW(user->setProperty("", "blah"), isc::BadValue);
+
+    // Verify that a blank property value IS permitted.
+    EXPECT_NO_THROW(user->setProperty("one", ""));
+    EXPECT_NO_THROW(value = user->getProperty("one"));
+    EXPECT_TRUE(value.empty());
+
+    PropertyMap map;
+    map["one"]="1.0";
+    map["two"]="2.0";
+
+    ASSERT_NO_THROW(user->setProperties(map));
+
+    EXPECT_NO_THROW(value = user->getProperty("one"));
+    EXPECT_EQ("1.0", value);
+
+    EXPECT_NO_THROW(value = user->getProperty("two"));
+    EXPECT_EQ("2.0", value);
+
+    const PropertyMap& map2 = user->getProperties();
+    EXPECT_TRUE(map2 == map);
+}
+
+} // end of anonymous namespace

+ 139 - 0
src/hooks/dhcp/user_chk/tests/userid_unittests.cc

@@ -0,0 +1,139 @@
+// 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 <exceptions/exceptions.h>
+#include <user.h>
+
+#include <boost/function.hpp>
+#include <boost/bind.hpp>
+#include <boost/shared_ptr.hpp>
+#include <gtest/gtest.h>
+
+using namespace std;
+
+namespace {
+
+/// @brief Test invalid constructors.
+TEST(UserIdTest, invalidConstructors) {
+    // Verify that constructor does not allow empty id vector.
+    std::vector<uint8_t> empty_bytes;
+    ASSERT_THROW(UserId(UserId::HW_ADDRESS, empty_bytes), isc::BadValue);
+    ASSERT_THROW(UserId(UserId::DUID, empty_bytes), isc::BadValue);
+
+    // Verify that constructor does not allow empty id string.
+    ASSERT_THROW(UserId(UserId::HW_ADDRESS, ""), isc::BadValue);
+    ASSERT_THROW(UserId(UserId::DUID, ""), isc::BadValue);
+}
+
+/// @brief Test making and using HW_ADDRESS type UserIds
+TEST(UserIdTest, hwAddress_type) {
+    // Verify text label look up for HW_ADDRESS enum.
+    EXPECT_EQ(std::string(UserId::HW_ADDRESS_STR),
+              UserId::lookupTypeStr(UserId::HW_ADDRESS));
+
+    // Verify enum look up for HW_ADDRESS text label.
+    EXPECT_EQ(UserId::HW_ADDRESS,
+              UserId::lookupType(UserId::HW_ADDRESS_STR));
+
+    // Build a test address vector.
+    uint8_t tmp[] = { 0x01, 0xFF, 0x02, 0xAC, 0x03, 0x0B, 0x07, 0x08 };
+    std::vector<uint8_t> bytes(tmp, tmp + (sizeof(tmp)/sizeof(uint8_t)));
+
+    // Verify construction from an HW_ADDRESS id type and address vector.
+    UserIdPtr id;
+    ASSERT_NO_THROW(id.reset(new UserId(UserId::HW_ADDRESS, bytes)));
+    // Verify that the id can be fetched.
+    EXPECT_EQ(id->getType(), UserId::HW_ADDRESS);
+    EXPECT_TRUE(bytes == id->getId());
+
+    // Check relational oeprators when a == b.
+    UserIdPtr id2;
+    ASSERT_NO_THROW(id2.reset(new UserId(UserId::HW_ADDRESS, id->toText())));
+    EXPECT_TRUE(*id == *id2);
+    EXPECT_FALSE(*id != *id2);
+    EXPECT_FALSE(*id < *id2);
+
+    // Check relational oeprators when a < b.
+    ASSERT_NO_THROW(id2.reset(new UserId(UserId::HW_ADDRESS,
+                                         "01FF02AC030B0709")));
+    EXPECT_FALSE(*id == *id2);
+    EXPECT_TRUE(*id != *id2);
+    EXPECT_TRUE(*id < *id2);
+
+    // Check relational oeprators when a > b.
+    ASSERT_NO_THROW(id2.reset(new UserId(UserId::HW_ADDRESS,
+                                         "01FF02AC030B0707")));
+    EXPECT_FALSE(*id == *id2);
+    EXPECT_TRUE(*id != *id2);
+    EXPECT_FALSE(*id < *id2);
+}
+
+/// @brief Test making and using DUID type UserIds
+TEST(UserIdTest, duid_type) {
+    // Verify text label look up for DUID enum.
+    EXPECT_EQ(std::string(UserId::DUID_STR),
+              UserId::lookupTypeStr(UserId::DUID));
+
+    // Verify enum look up for DUID text label.
+    EXPECT_EQ(UserId::DUID,
+              UserId::lookupType(UserId::DUID_STR));
+
+    // Build a test DUID vector.
+    uint8_t tmp[] = { 0x01, 0xFF, 0x02, 0xAC, 0x03, 0x0B, 0x07, 0x08 };
+    std::vector<uint8_t> bytes(tmp, tmp + (sizeof(tmp)/sizeof(uint8_t)));
+
+    // Verify construction from an DUID id type and address vector.
+    UserIdPtr id;
+    ASSERT_NO_THROW(id.reset(new UserId(UserId::DUID, bytes)));
+    // Verify that the id can be fetched.
+    EXPECT_EQ(id->getType(), UserId::DUID);
+    EXPECT_TRUE(bytes == id->getId());
+
+    // Check relational oeprators when a == b.
+    UserIdPtr id2;
+    ASSERT_NO_THROW(id2.reset(new UserId(UserId::DUID, id->toText())));
+    EXPECT_TRUE(*id == *id2);
+    EXPECT_FALSE(*id != *id2);
+    EXPECT_FALSE(*id < *id2);
+
+    // Check relational oeprators when a < b.
+    ASSERT_NO_THROW(id2.reset(new UserId(UserId::DUID, "01FF02AC030B0709")));
+    EXPECT_FALSE(*id == *id2);
+    EXPECT_TRUE(*id != *id2);
+    EXPECT_TRUE(*id < *id2);
+
+    // Check relational oeprators when a > b.
+    ASSERT_NO_THROW(id2.reset(new UserId(UserId::DUID, "01FF02AC030B0707")));
+    EXPECT_FALSE(*id == *id2);
+    EXPECT_TRUE(*id != *id2);
+    EXPECT_FALSE(*id < *id2);
+}
+
+/// @brief Tests that UserIds of different types compare correctly.
+TEST(UserIdTest, mixed_type_compare) {
+    UserIdPtr hw, duid;
+    // Create UserIds with different types, but same id data.
+    ASSERT_NO_THROW(hw.reset(new UserId(UserId::HW_ADDRESS,
+                                        "01FF02AC030B0709")));
+    ASSERT_NO_THROW(duid.reset(new UserId(UserId::DUID,
+                                          "01FF02AC030B0709")));
+
+    // Verify that UserIdType influences logical comparators.
+    EXPECT_FALSE(*hw == *duid);
+    EXPECT_TRUE(*hw != *duid);
+    EXPECT_TRUE(*hw < *duid);
+}
+
+
+} // end of anonymous namespace

+ 210 - 0
src/hooks/dhcp/user_chk/user.cc

@@ -0,0 +1,210 @@
+// 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 <dhcp/hwaddr.h>
+#include <dhcp/duid.h>
+#include <exceptions/exceptions.h>
+#include <util/encode/hex.h>
+
+#include <user.h>
+
+#include <iomanip>
+#include <sstream>
+
+//********************************* UserId ******************************
+
+const char* UserId::HW_ADDRESS_STR = "HW_ADDR";
+const char* UserId::DUID_STR = "DUID";
+
+UserId::UserId(UserIdType id_type, const std::vector<uint8_t>& id)
+    : id_type_(id_type), id_(id) {
+    if (id.size() == 0) {
+        isc_throw(isc::BadValue, "UserId id may not be blank");
+    }
+}
+
+UserId::UserId(UserIdType id_type, const std::string & id_str) :
+    id_type_(id_type) {
+    if (id_str.empty()) {
+        isc_throw(isc::BadValue, "UserId id string may not be blank");
+    }
+
+    // Convert the id string to vector.
+    // Input is expected to be 2-digits per bytes, no delimiters.
+    std::vector<uint8_t> addr_bytes;
+    isc::util::encode::decodeHex(id_str, addr_bytes);
+
+    // Attempt to instantiate the appropriate id class to leverage validation.
+    switch (id_type) {
+        case HW_ADDRESS: {
+            isc::dhcp::HWAddr hwaddr(addr_bytes, isc::dhcp::HTYPE_ETHER);
+            break;
+            }
+        case DUID: {
+            isc::dhcp::DUID duid(addr_bytes);
+            break;
+            }
+        default:
+            isc_throw (isc::BadValue, "Invalid id_type: " << id_type);
+            break;
+    }
+
+    // It's a valid id.
+    id_ = addr_bytes;
+}
+
+UserId::~UserId() {
+}
+
+const std::vector<uint8_t>&
+UserId::getId() const {
+    return (id_);
+}
+
+UserId::UserIdType
+UserId::getType() const {
+    return (id_type_);
+}
+
+std::string
+UserId::toText(char delim_char) const {
+    std::stringstream tmp;
+    tmp << std::hex;
+    bool delim = false;
+    for (std::vector<uint8_t>::const_iterator it = id_.begin();
+         it != id_.end(); ++it) {
+        if (delim_char && delim) {
+            tmp << delim_char;
+        }
+
+        tmp << std::setw(2) << std::setfill('0')
+            << static_cast<unsigned int>(*it);
+        delim = true;
+    }
+
+    return (tmp.str());
+}
+
+bool
+UserId::operator ==(const UserId & other) const {
+    return ((this->id_type_ == other.id_type_) && (this->id_ == other.id_));
+}
+
+bool
+UserId::operator !=(const UserId & other) const {
+    return (!(*this == other));
+}
+
+bool
+UserId::operator <(const UserId & other) const {
+    return ((this->id_type_ < other.id_type_) ||
+            ((this->id_type_ == other.id_type_) && (this->id_ < other.id_)));
+}
+
+std::string
+UserId::lookupTypeStr(UserIdType type) {
+    const char* tmp = NULL;
+    switch (type) {
+        case HW_ADDRESS:
+            tmp = HW_ADDRESS_STR;
+            break;
+        case DUID:
+            tmp = DUID_STR;
+            break;
+        default:
+            isc_throw(isc::BadValue, "Invalid UserIdType:" << type);
+            break;
+    }
+
+    return (std::string(tmp));
+}
+
+UserId::UserIdType
+UserId::lookupType(const std::string& type_str) {
+    if (type_str.compare(HW_ADDRESS_STR) == 0) {
+        return (HW_ADDRESS);
+    } else if (type_str.compare(DUID_STR) == 0) {
+        return (DUID);
+    }
+
+    isc_throw(isc::BadValue, "Invalid UserIdType string:" << type_str);
+}
+
+std::ostream&
+operator<<(std::ostream& os, const UserId& user_id) {
+    std::string tmp = UserId::lookupTypeStr(user_id.getType());
+    os << tmp << "=" << user_id.toText();
+    return (os);
+}
+
+//********************************* User ******************************
+
+User::User(const UserId& user_id) : user_id_(user_id) {
+}
+
+User::User(UserId::UserIdType id_type, const std::vector<uint8_t>& id)
+    : user_id_(id_type, id) {
+}
+
+User::User(UserId::UserIdType id_type, const std::string& id_str)
+    : user_id_(id_type, id_str) {
+}
+
+User::~User() {
+}
+
+const PropertyMap&
+User::getProperties() const {
+    return (properties_);
+}
+
+void
+User::setProperties(const PropertyMap& properties) {
+    properties_ = properties;
+}
+
+void User::setProperty(const std::string& name, const std::string& value) {
+    if (name.empty()) {
+        isc_throw (isc::BadValue, "User property name cannot be blank");
+    }
+
+    // Note that if the property exists its value will be updated.
+    properties_[name]=value;
+}
+
+std::string
+User::getProperty(const std::string& name) const {
+    PropertyMap::const_iterator it = properties_.find(name);
+    if (it != properties_.end()) {
+        return ((*it).second);
+    }
+
+    // By returning an empty string rather than throwing, we allow the
+    // flexibility of defaulting to blank if not specified.  Let the caller
+    // decide if that is valid or not.
+    return ("");
+}
+
+void
+User::delProperty(const std::string & name) {
+    PropertyMap::iterator it = properties_.find(name);
+    if (it != properties_.end()) {
+        properties_.erase(it);
+    }
+}
+
+const UserId&
+User::getUserId() const {
+    return (user_id_);
+}

+ 249 - 0
src/hooks/dhcp/user_chk/user.h

@@ -0,0 +1,249 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef USER_H
+#define USER_H
+
+#include <boost/shared_ptr.hpp>
+
+#include <map>
+#include <stdint.h>
+#include <vector>
+
+/// @file user.h This file defines classes: UserId and User.
+/// @brief These classes are used to describe and recognize DHCP lease
+/// clients.
+
+/// @brief Encapsulates a unique identifier for a DHCP client.
+/// This class provides a generic wrapper around the information used to
+/// uniquely identify the requester in a DHCP request packet.  It provides
+/// the necessary operators such that it can be used as a key within STL
+/// containers such as maps.  It supports both IPv4 and IPv6 clients.
+class UserId {
+public:
+    /// @brief Defines the supported types of user ids.
+    // Use explicit values to ensure consistent numeric ordering for key
+    // comparisons.
+    enum UserIdType {
+        /// @brief Hardware addresses (MAC) are used for IPv4 clients.
+        HW_ADDRESS = 0,
+        /// @brief DUIDs are used for IPv6 clients.
+        DUID = 1
+    };
+
+    /// @brief Defines the text label hardware address id type.
+    static const char* HW_ADDRESS_STR;
+    /// @brief Define the text label DUID id type.
+    static const char* DUID_STR;
+
+    /// @brief Constructor
+    ///
+    /// Constructs a UserId from an id type and id vector.
+    ///
+    /// @param id_type The type of user id contained in vector
+    /// @param id a vector of unsigned bytes containing the id
+    ///
+    /// @throw isc::BadValue if the vector is empty.
+    UserId(UserIdType id_type, const std::vector<uint8_t>& id);
+
+    /// @brief Constructor
+    ///
+    /// Constructs a UserId from an id type and id string.
+    ///
+    /// @param id_type The type of user id contained in string.
+    /// The string is expected to contain an even number of hex digits
+    /// without delimiters.
+    ///
+    /// @param id a vector of unsigned bytes containing the id
+    ///
+    /// @throw isc::BadValue if the string is empty, contains non
+    /// valid hex digits, or an odd number of hex digits.
+    UserId(UserIdType id_type, const std::string& id_str);
+
+    /// @brief Destructor.
+    ~UserId();
+
+    /// @brief Returns a const reference to the actual id value
+    const std::vector<uint8_t>& getId() const;
+
+    /// @brief Returns the user id type
+    UserIdType getType() const;
+
+    /// @brief Returns textual representation of the id
+    ///
+    /// Outputs a string of hex digits representing the id with an
+    /// optional delimiter between digit pairs (i.e. bytes).
+    ///
+    /// Without a delimiter:
+    ///   "0c220F"
+    ///
+    /// with colon as a delimiter:
+    ///   "0c:22:0F"
+    ///
+    /// @param delim_char The delimiter to place in between
+    /// "bytes". It defaults to none.
+    ///  (e.g. 00010203ff)
+    std::string toText(char delim_char=0x0) const;
+
+    /// @brief Compares two UserIds for equality
+    bool operator ==(const UserId & other) const;
+
+    /// @brief Compares two UserIds for inequality
+    bool operator !=(const UserId & other) const;
+
+    /// @brief Performs less than comparison of two UserIds
+    bool operator <(const UserId & other) const;
+
+    /// @brief Returns the text label for a given id type
+    ///
+    /// @param type The id type value for which the label is desired
+    ///
+    /// @throw isc::BadValue if type is not valid.
+    static std::string lookupTypeStr(UserIdType type);
+
+    /// @brief Returns the id type for a given text label
+    ///
+    /// @param type_str The text label for which the id value is desired
+    ///
+    /// @throw isc::BadValue if type_str is not a valid text label.
+    static UserIdType lookupType(const std::string& type_str);
+
+private:
+    /// @brief The type of id value
+    UserIdType id_type_;
+
+    /// @brief The id value
+    std::vector<uint8_t> id_;
+
+};
+
+/// @brief Outputs the UserId contents in a string to the given stream.
+///
+/// The output string has the form "<type>=<id>" where:
+///
+/// <type> is the text label returned by UserId::lookupTypeStr()
+/// <id> is the output of UserId::toText() without a delimiter.
+///
+/// Examples:
+///       HW_ADDR=0c0e0a01ff06
+///       DUID=0001000119efe63b000c01020306
+///
+/// @param os output stream to which to write
+/// @param user_id source object to output
+std::ostream&
+operator<<(std::ostream& os, const UserId& user_id);
+
+/// @brief Defines a smart pointer to UserId
+typedef boost::shared_ptr<UserId> UserIdPtr;
+
+/// @brief Defines a map of string values keyed by string labels.
+typedef std::map<std::string, std::string> PropertyMap;
+
+/// @brief Represents a unique DHCP user
+/// This class is used to represent a specific DHCP user who is identified by a
+/// unique id and who possesses a set of properties.
+class User {
+public:
+    /// @brief Constructor
+    ///
+    /// Constructs a new User from a given id with an empty set of properties.
+    ///
+    /// @param user_id Id to assign to the user
+    ///
+    /// @throw isc::BadValue if user id is blank.
+    User(const UserId & user_id);
+
+    /// @brief Constructor
+    ///
+    /// Constructs a new User from a given id type and vector containing the
+    /// id data with an empty set of properties.
+    ///
+    /// @param user_id Type of id contained in the id vector
+    /// @param id Vector of data representing the user's id
+    ///
+    /// @throw isc::BadValue if user id vector is empty.
+    User(UserId::UserIdType id_type, const std::vector<uint8_t>& id);
+
+    /// @brief Constructor
+    ///
+    /// Constructs a new User from a given id type and string containing the
+    /// id data with an empty set of properties.
+    ///
+    /// @param user_id Type of id contained in the id vector
+    /// @param id string of hex digits representing the user's id
+    ///
+    /// @throw isc::BadValue if user id string is empty or invalid
+    User(UserId::UserIdType id_type, const std::string& id_str);
+
+    /// @brief Destructor
+    ~User();
+
+    /// @brief Returns a reference to the map of properties.
+    ///
+    /// Note that this reference can go out of scope and should not be
+    /// relied upon other than for momentary use.
+    const PropertyMap& getProperties() const;
+
+    /// @brief Sets the user's properties from a given property map
+    ///
+    /// Replaces the contents of the user's property map with the given
+    /// property map.
+    ///
+    /// @param properties property map to assign to the user
+    void setProperties(const PropertyMap& properties);
+
+    /// @brief Sets a given property to the given value
+    ///
+    /// Adds or updates the given property to the given value.
+    ///
+    /// @param name string by which the property is identified (keyed)
+    /// @param value string data to associate with the property
+    ///
+    /// @throw isc::BadValue if name is blank.
+    void setProperty(const std::string& name, const std::string& value);
+
+    /// @brief Fetches the string value for a given property name.
+    ///
+    /// @param name property name to fetch
+    ///
+    /// @return Returns the string value for the given name or an empty string
+    /// if the property is not found in the property map.
+    std::string getProperty(const std::string& name) const;
+
+    /// @brief Removes the given property from the property map.
+    ///
+    /// Removes the given property from the map if found. If not, no harm no
+    /// foul.
+    ///
+    /// @param name property name to remove
+    void delProperty(const std::string& name);
+
+    /// @brief Returns the user's id.
+    ///
+    /// Note that this reference can go out of scope and should not be
+    /// relied upon other than for momentary use.
+    const UserId& getUserId() const;
+
+private:
+    /// @brief The user's id.
+    UserId user_id_;
+
+    /// @brief The user's property map.
+    PropertyMap properties_;
+};
+
+/// @brief Defines a smart pointer to a User.
+typedef boost::shared_ptr<User> UserPtr;
+
+#endif

+ 18 - 0
src/hooks/dhcp/user_chk/user_chk_log.cc

@@ -0,0 +1,18 @@
+// 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.
+
+/// Defines the logger used by the user check hooks library.
+#include <user_chk_log.h>
+
+isc::log::Logger user_chk_logger("user_chk");

+ 29 - 0
src/hooks/dhcp/user_chk/user_chk_log.h

@@ -0,0 +1,29 @@
+// Copyright (C) 2013  Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef USER_CHK_LOG_H
+#define USER_CHK_LOG_H
+
+#include <log/message_initializer.h>
+#include <log/macros.h>
+#include <user_chk_messages.h>
+
+/// @brief User Check Logger
+///
+/// Define the logger used to log messages.  We could define it in multiple
+/// modules, but defining in a single module and linking to it saves time and
+/// space.
+extern isc::log::Logger user_chk_logger;
+
+#endif // USER_CHK_LOG_H

+ 43 - 0
src/hooks/dhcp/user_chk/user_chk_messages.mes

@@ -0,0 +1,43 @@
+# 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.
+
+% USER_CHK_HOOK_LOAD_ERROR DHCP UserCheckHook could not be loaded: %1
+This is an error message issued when the DHCP UserCheckHook could not be loaded.
+The exact cause should be explained in the log message.  User subnet selection
+will revert to default processing.
+
+% USER_CHK_HOOK_UNLOAD_ERROR DHCP UserCheckHook an error occurred unloading the library: %1
+This is an error message issued when an error occurs while unloading the
+UserCheckHook library.  This is unlikely to occur and normal operations of the
+library will likely resume when it is next loaded.
+
+% USER_CHK_SUBNET4_SELECT_ERROR DHCP UserCheckHook an unexpected error occured in subnet4_select callout: %1
+This is an error message issued when the DHCP UserCheckHook subnet4_select hook
+encounters an unexpected error.  The message should contain a more detailed
+explanation.
+
+% USER_CHK_SUBNET4_SELECT_REGISTRY_NULL DHCP UserCheckHook UserRegistry has not been created.
+This is an error message issued when the DHCP UserCheckHook subnet4_select hook
+has been invoked but the UserRegistry has not been created.  This is a
+programmatic error and should not occur.
+
+% USER_CHK_SUBNET6_SELECT_ERROR DHCP UserCheckHook an unexpected error occured in subnet6_select callout: %1
+This is an error message issued when the DHCP UserCheckHook subnet6_select hook
+encounters an unexpected error.  The message should contain a more detailed
+explanation.
+
+% USER_CHK_SUBNET6_SELECT_REGISTRY_NULL DHCP UserCheckHook UserRegistry has not been created.
+This is an error message issued when the DHCP UserCheckHook subnet6_select hook
+has been invoked but the UserRegistry has not been created.  This is a
+programmatic error and should not occur.

+ 75 - 0
src/hooks/dhcp/user_chk/user_data_source.h

@@ -0,0 +1,75 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+#ifndef _USER_DATA_SOURCE_H
+#define _USER_DATA_SOURCE_H
+
+/// @file user_data_source.h Defines the base class, UserDataSource.
+#include <exceptions/exceptions.h>
+#include <user.h>
+
+/// @brief Thrown if UserDataSource encounters an error
+class UserDataSourceError : public isc::Exception {
+public:
+    UserDataSourceError(const char* file, size_t line,
+                               const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
+/// @brief Defines an interface for reading user data into a registry.
+/// This is an abstract class which defines the interface for reading Users
+/// from an IO source such as a file.
+class UserDataSource {
+public:
+    /// @brief Constructor.
+    UserDataSource() {};
+
+    /// @brief Virtual Destructor.
+    virtual ~UserDataSource() {};
+
+    /// @brief Opens the data source.
+    ///
+    /// Prepares the data source for reading.  Upon successful completion the
+    /// data source is ready to read from the beginning of its content.
+    ///
+    /// @throw UserDataSourceError if the source fails to open.
+    virtual void open() = 0;
+
+    /// @brief Fetches the next user from the data source.
+    ///
+    /// Reads the next User from the data source and returns it.  If no more
+    /// data is available it should return an empty (null) user.
+    ///
+    /// @throw UserDataSourceError if an error occurs.
+    virtual UserPtr readNextUser() = 0;
+
+    /// @brief Closes that data source.
+    ///
+    /// Closes the data source.
+    ///
+    /// This method must not throw exceptions.
+    virtual void close() = 0;
+
+    /// @brief Returns true if the data source is open.
+    ///
+    /// This method should return true once the data source has been
+    /// successfully opened and until it has been closed.
+    ///
+    /// It is assumed to be exception safe.
+    virtual bool isOpen() const = 0;
+};
+
+/// @brief Defines a smart pointer to a UserDataSource.
+typedef boost::shared_ptr<UserDataSource> UserDataSourcePtr;
+
+#endif

+ 159 - 0
src/hooks/dhcp/user_chk/user_file.cc

@@ -0,0 +1,159 @@
+// 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 <cc/data.h>
+#include <user.h>
+#include <user_file.h>
+
+#include <boost/foreach.hpp>
+#include <errno.h>
+#include <iostream>
+
+UserFile::UserFile(const std::string& fname) : fname_(fname), file_() {
+    if (fname_.empty()) {
+        isc_throw(UserFileError, "file name cannot be blank");
+    }
+}
+
+UserFile::~UserFile(){
+    close();
+};
+
+void
+UserFile::open() {
+    if (isOpen()) {
+        isc_throw (UserFileError, "file is already open");
+    }
+
+    file_.open(fname_.c_str(), std::ifstream::in);
+    int sav_error = errno;
+    if (!file_.is_open()) {
+        isc_throw(UserFileError, "cannot open file:" << fname_
+                                 << " reason: " << strerror(sav_error));
+    }
+}
+
+UserPtr
+UserFile::readNextUser() {
+    if (!isOpen()) {
+        isc_throw (UserFileError, "cannot read, file is not open");
+    }
+
+    if (file_.good()) {
+        char buf[USER_ENTRY_MAX_LEN];
+
+        // Get the next line.
+        file_.getline(buf, sizeof(buf));
+
+        // We got something, try to make a user out of it.
+        if (file_.gcount() > 0) {
+            return(makeUser(buf));
+        }
+    }
+
+    // Returns an empty user on EOF.
+    return (UserPtr());
+}
+
+UserPtr
+UserFile::makeUser(const std::string& user_string) {
+    // This method leverages the existing JSON parsing provided by isc::data
+    // library.  Should this prove to be a performance issue, it may be that
+    // lighter weight solution would be appropriate.
+
+    // Turn the string of JSON text into an Element set.
+    isc::data::ElementPtr elements;
+    try {
+        elements = isc::data::Element::fromJSON(user_string);
+    } catch (isc::data::JSONError& ex) {
+        isc_throw(UserFileError,
+                  "UserFile entry is malformed JSON: " << ex.what());
+    }
+
+    // Get a map of the Elements, keyed by element name.
+    isc::data::ConstElementPtr element;
+    PropertyMap properties;
+    std::string id_type_str;
+    std::string id_str;
+
+    // Iterate over the elements, saving of "type" and "id" to their
+    // respective locals.  Anything else is assumed to be an option so
+    // add it to the local property map.
+    std::pair<std::string, isc::data::ConstElementPtr> element_pair;
+    BOOST_FOREACH (element_pair, elements->mapValue()) {
+        // Get the element's label.
+        std::string label = element_pair.first;
+
+        // Currently everything must be a string.
+        if (element_pair.second->getType() != isc::data::Element::string) {
+            isc_throw (UserFileError, "UserFile entry: " << user_string
+                       << "has non-string value for : " << label);
+        }
+
+        std::string value = element_pair.second->stringValue();
+
+        if (label == "type") {
+            id_type_str = value;
+        } else if (label == "id") {
+            id_str = value;
+        } else {
+            // JSON parsing reduces any duplicates to the last value parsed,
+            // so we will never see duplicates here.
+            properties[label]=value;
+        }
+    }
+
+    // First we attempt to translate the id type.
+    UserId::UserIdType id_type;
+    try {
+        id_type = UserId::lookupType(id_type_str);
+    } catch (const std::exception& ex) {
+        isc_throw (UserFileError, "UserFile entry has invalid type: "
+                                  << user_string << " " << ex.what());
+    }
+
+    // Id type is valid, so attempt to make the user based on that and
+    // the value we have for "id".
+    UserPtr user;
+    try {
+        user.reset(new User(id_type, id_str));
+    } catch (const std::exception& ex) {
+        isc_throw (UserFileError, "UserFile cannot create user form entry: "
+                                  << user_string << " " << ex.what());
+    }
+
+    // We have a new User, so add in the properties and return it.
+    user->setProperties(properties);
+    return (user);
+}
+
+bool
+UserFile::isOpen() const {
+    return (file_.is_open());
+}
+
+void
+UserFile::close() {
+    try {
+        if (file_.is_open()) {
+            file_.close();
+        }
+    } catch (const std::exception& ex) {
+        // Highly unlikely to occur but let's at least spit out an error.
+        // Beyond that we swallow it for tidiness.
+        std::cout << "UserFile unexpected error closing the file: "
+                  << fname_ << " : " << ex.what() << std::endl;
+    }
+}
+

+ 135 - 0
src/hooks/dhcp/user_chk/user_file.h

@@ -0,0 +1,135 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+#ifndef _USER_FILE_H
+#define _USER_FILE_H
+
+/// @file user_file.h Defines the class, UserFile, which implements the UserDataSource interface for text files.
+
+#include <user_data_source.h>
+#include <user.h>
+
+#include <boost/shared_ptr.hpp>
+#include <fstream>
+#include <string>
+
+using namespace std;
+
+/// @brief Thrown a UserFile encounters an error.
+/// Note that it derives from UserDataSourceError to comply with the interface.
+class UserFileError : public UserDataSourceError {
+public:
+    UserFileError(const char* file, size_t line,
+                               const char* what) :
+        UserDataSourceError(file, line, what) { };
+};
+
+/// @brief Provides a UserDataSource implementation for JSON text files.
+/// This class allows a text file of JSON entries to be treated as a source of
+/// User entries.  The format of the file is one user entry per line, where
+/// each line contains a JSON string as follows:
+///
+/// { "type" : "<user type>", "id" : "<user_id>" (options)  }
+///
+/// where:
+///
+/// <user_type>  text label of the id type: "HW_ADDR" or "DUID"
+/// <user_id>  the user's id as a string of hex digits without delimiters
+/// (options) zero or more string elements as name-value pairs, separated by
+/// commas: "opt1" : "val1",  "other_opt", "77" ...
+///
+/// Each entry must have a valid entry for "type" and a valid entry or "id".
+///
+/// If an entry contains duplicate option names, that option will be assigend
+/// the last value found. This is typical JSON behavior.
+/// Currently, only string option values (i.e. enclosed in quotes) are
+/// supported.
+///
+/// Example file entries might look like this:
+/// @code
+///
+/// { "type" : "HW_ADDR", "id" : "01AC00F03344", "opt1" : "true" }
+/// { "type" : "DUID", "id" : "225060de0a0b", "opt1" : "false" }
+///
+/// @endcode
+class UserFile : public UserDataSource {
+public:
+    /// @brief Maximum length of a single user entry.
+    /// This value is somewhat arbitrary. 4K seems reasonably large.  If it
+    /// goes beyond this, then a flat file is not likely the way to go.
+    static const size_t USER_ENTRY_MAX_LEN = 4096;
+
+    /// @brief Constructor
+    ///
+    /// Create a UserFile for the given file name without opening the file.
+    /// @param fname pathname to the input file.
+    ///
+    /// @throw UserFileError if given file name is empty.
+    UserFile(const std::string& fname);
+
+    /// @brief Destructor.
+    ////
+    /// The destructor does call the close method.
+    virtual ~UserFile();
+
+    /// @brief Opens the input file for reading.
+    ///
+    /// Upon successful completion, the file is opened and positioned to start
+    /// reading from the beginning of the file.
+    ///
+    /// @throw UserFileError if the file cannot be opened.
+    virtual void open();
+
+    /// @brief Fetches the next user from the file.
+    ///
+    /// Reads the next user entry from the file and attempts to create a
+    /// new User from the text therein.  If there is no more data to be read
+    /// it returns an empty UserPtr.
+    ///
+    /// @return A UserPtr pointing to the new User or an empty pointer on EOF.
+    ///
+    /// @throw UserFileError if an error occurs while reading.
+    virtual UserPtr readNextUser();
+
+    /// @brief Closes the underlying file.
+    ///
+    /// Method is exception safe.
+    virtual void close();
+
+    /// @brief Returns true if the file is open.
+    ///
+    /// @return True if the underlying file is open, false otherwise.
+    virtual bool isOpen() const;
+
+    /// @brief Creates a new User instance from JSON text.
+    ///
+    /// @param user_string string the JSON text for a user entry.
+    ///
+    /// @return A pointer to the newly created User instance.
+    ///
+    /// @throw UserFileError if the entry is invalid.
+    UserPtr makeUser(const std::string& user_string);
+
+private:
+    /// @brief Pathname of the input text file.
+    string fname_;
+
+    /// @brief Input file stream.
+    std::ifstream file_;
+
+};
+
+/// @brief Defines a smart pointer to a UserFile.
+typedef boost::shared_ptr<UserFile> UserFilePtr;
+
+#endif

+ 122 - 0
src/hooks/dhcp/user_chk/user_registry.cc

@@ -0,0 +1,122 @@
+// 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 <user_registry.h>
+#include <user.h>
+
+UserRegistry::UserRegistry() {
+}
+
+UserRegistry::~UserRegistry(){
+}
+
+void
+UserRegistry::addUser(UserPtr& user) {
+    if (!user) {
+        isc_throw (UserRegistryError, "UserRegistry cannot add blank user");
+    }
+
+    UserPtr found_user;
+    if ((found_user = findUser(user->getUserId()))) {
+        isc_throw (UserRegistryError, "UserRegistry duplicate user: "
+                   << user->getUserId());
+    }
+
+    users_[user->getUserId()] = user;
+}
+
+const UserPtr&
+UserRegistry::findUser(const UserId& id) const {
+    static UserPtr empty;
+    UserMap::const_iterator it = users_.find(id);
+    if (it != users_.end()) {
+        const UserPtr tmp = (*it).second;
+        return ((*it).second);
+    }
+
+    return empty;
+}
+
+void
+UserRegistry::removeUser(const UserId& id) {
+    static UserPtr empty;
+    UserMap::iterator it = users_.find(id);
+    if (it != users_.end()) {
+        users_.erase(it);
+    }
+}
+
+const UserPtr&
+UserRegistry::findUser(const isc::dhcp::HWAddr& hwaddr) const {
+    UserId id(UserId::HW_ADDRESS, hwaddr.hwaddr_);
+    return (findUser(id));
+}
+
+const UserPtr&
+UserRegistry::findUser(const isc::dhcp::DUID& duid) const {
+    UserId id(UserId::DUID, duid.getDuid());
+    return (findUser(id));
+}
+
+void UserRegistry::refresh() {
+    if (!source_) {
+        isc_throw(UserRegistryError,
+                  "UserRegistry: cannot refresh, no data source");
+    }
+
+    // If the source isn't open, open it.
+    if (!source_->isOpen()) {
+        source_->open();
+    }
+
+    // Make a copy in case something goes wrong midstream.
+    UserMap backup(users_);
+
+    // Empty the registry then read users from source until source is empty.
+    clearall();
+    try {
+        UserPtr user;
+        while ((user = source_->readNextUser())) {
+            addUser(user);
+        }
+    } catch (const std::exception& ex) {
+        // Source was compromsised so restore registry from backup.
+        users_ = backup;
+        // Close the source.
+        source_->close();
+        isc_throw (UserRegistryError, "UserRegistry: refresh failed during read"
+                   << ex.what());
+    }
+
+    // Close the source.
+    source_->close();
+}
+
+void UserRegistry::clearall() {
+    users_.clear();
+}
+
+void UserRegistry::setSource(UserDataSourcePtr& source) {
+    if (!source) {
+        isc_throw (UserRegistryError,
+                   "UserRegistry: data source cannot be set to null");
+    }
+
+    source_ = source;
+}
+
+const UserDataSourcePtr& UserRegistry::getSource() {
+    return (source_);
+}
+

+ 128 - 0
src/hooks/dhcp/user_chk/user_registry.h

@@ -0,0 +1,128 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+#ifndef _USER_REGISTRY_H
+#define _USER_REGISTRY_H
+
+/// @file user_registry.h Defines the class, UserRegistry.
+
+#include <dhcp/hwaddr.h>
+#include <dhcp/duid.h>
+#include <exceptions/exceptions.h>
+#include <user.h>
+#include <user_data_source.h>
+
+#include <string>
+
+using namespace std;
+
+/// @brief Thrown UserRegistry encounters an error
+class UserRegistryError : public isc::Exception {
+public:
+    UserRegistryError(const char* file, size_t line,
+                               const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
+/// @brief Defines a map of unique Users keyed by UserId.
+typedef std::map<UserId,UserPtr> UserMap;
+
+/// @brief Embodies an update-able, searchable list of unique users
+/// This class provides the means to create and maintain a searchable list
+/// of unique users. List entries are pointers to instances of User, keyed
+/// by their UserIds.
+/// Users may be added and removed from the list individually or the list
+/// may be updated by loading it from a data source, such as a file.
+class UserRegistry {
+public:
+    /// @brief Constructor
+    ///
+    /// Creates a new registry with an empty list of users and no data source.
+    UserRegistry();
+
+    /// @brief Destructor
+    ~UserRegistry();
+
+    /// @brief Adds a given user to the registry.
+    ///
+    /// @param user A pointer to the user to add
+    ///
+    /// @throw UserRegistryError if the user is null or if the user already
+    /// exists in the registry.
+    void addUser(UserPtr& user);
+
+    /// @brief Finds a user in the registry by user id
+    ///
+    /// @param id The user id for which to search
+    ///
+    /// @return A pointer to the user if found or an null pointer if not.
+    const UserPtr& findUser(const UserId& id) const;
+
+    /// @brief Removes a user from the registry by user id
+    ///
+    /// Removes the user entry if found, if not simply return.
+    ///
+    /// @param id The user id of the user to remove
+    void removeUser(const UserId&  id);
+
+    /// @brief Finds a user in the registry by hardware address
+    ///
+    /// @param hwaddr The hardware address for which to search
+    ///
+    /// @return A pointer to the user if found or an null pointer if not.
+    const UserPtr& findUser(const isc::dhcp::HWAddr& hwaddr) const;
+
+    /// @brief Finds a user in the registry by DUID
+    ///
+    /// @param duid The DUID for which to search
+    ///
+    /// @return A pointer to the user if found or an null pointer if not.
+    const UserPtr& findUser(const isc::dhcp::DUID& duid) const;
+
+    /// @brief Updates the registry from its data source.
+    ///
+    /// This method will replace the contents of the registry with new content
+    /// read from its data source.  It will attempt to open the source and
+    /// then add users from the source to the registry until the source is
+    /// exhausted.  If an error occurs accessing the source the registry
+    /// contents will be restored to that of before the call to refresh.
+    ///
+    /// @throw UserRegistryError if the data source has not been set (is null)
+    /// or if an error occurs accessing the data source.
+    void refresh();
+
+    /// @brief Removes all entries from the registry.
+    void clearall();
+
+    /// @brief Returns a reference to the data source.
+    const UserDataSourcePtr& getSource();
+
+    /// @brief Sets the data source to the given value.
+    ///
+    /// @param source reference to the data source to use.
+    ///
+    /// @throw UserRegistryError if new source value is null.
+    void setSource(UserDataSourcePtr& source);
+
+private:
+    /// @brief The registry of users.
+    UserMap users_;
+
+    /// @brief The current data source of users.
+    UserDataSourcePtr source_;
+};
+
+/// @brief Define a smart pointer to a UserRegistry.
+typedef boost::shared_ptr<UserRegistry> UserRegistryPtr;
+
+#endif

+ 25 - 0
src/hooks/dhcp/user_chk/version.cc

@@ -0,0 +1,25 @@
+// 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.
+// version.cc
+
+#include <hooks/hooks.h>
+
+extern "C" {
+
+/// @brief Version function required by Hooks API for compatibility checks.
+int version() {
+    return (BIND10_HOOKS_VERSION);
+}
+
+}

+ 1 - 1
src/lib/datasrc/memory/zone_finder.cc

@@ -772,7 +772,7 @@ InMemoryZoneFinder::findAll(const isc::dns::Name& name,
 // the case of CNAME can be eliminated (these should be guaranteed at the load
 // or update time, but even if they miss a corner case and allows a CNAME to
 // be added at origin, the zone is broken anyway, so we'd just let this
-// method return garbage, too).  As a result, there can be only too cases
+// method return garbage, too).  As a result, there can be only two cases
 // for the result codes: SUCCESS if the requested type of RR exists; NXRRSET
 // otherwise.  Due to its simplicity we implement it separately, rather than
 // sharing the code with findInternal.

+ 81 - 9
src/lib/dhcp/iface_mgr.cc

@@ -177,6 +177,17 @@ IfaceMgr::IfaceMgr()
     }
 }
 
+void Iface::addUnicast(const isc::asiolink::IOAddress& addr) {
+    for (Iface::AddressCollection::const_iterator i = unicasts_.begin();
+         i != unicasts_.end(); ++i) {
+        if (*i == addr) {
+            isc_throw(BadValue, "Address " << addr.toText()
+                      << " already defined on the " << name_ << " interface.");
+        }
+    }
+    unicasts_.push_back(addr);
+}
+
 void IfaceMgr::closeSockets() {
     for (IfaceCollection::iterator iface = ifaces_.begin();
          iface != ifaces_.end(); ++iface) {
@@ -343,8 +354,10 @@ bool IfaceMgr::openSockets4(const uint16_t port, const bool use_bcast) {
 
             }
             if (sock < 0) {
+                const char* errstr = strerror(errno);
                 isc_throw(SocketConfigError, "failed to open IPv4 socket"
-                          << " supporting broadcast traffic");
+                          << " supporting broadcast traffic, reason:"
+                          << errstr);
             }
 
             count++;
@@ -368,6 +381,23 @@ bool IfaceMgr::openSockets6(const uint16_t port) {
             continue;
         }
 
+        // Open unicast sockets if there are any unicast addresses defined
+        Iface::AddressCollection unicasts = iface->getUnicasts();
+        for (Iface::AddressCollection::iterator addr = unicasts.begin();
+             addr != unicasts.end(); ++addr) {
+
+            sock = openSocket(iface->getName(), *addr, port);
+            if (sock < 0) {
+                const char* errstr = strerror(errno);
+                isc_throw(SocketConfigError, "failed to open unicast socket on "
+                          << addr->toText() << " on interface " << iface->getName()
+                          << ", reason: " << errstr);
+            }
+
+            count++;
+
+        }
+
         Iface::AddressCollection addrs = iface->getAddresses();
         for (Iface::AddressCollection::iterator addr = addrs.begin();
              addr != addrs.end();
@@ -389,7 +419,10 @@ bool IfaceMgr::openSockets6(const uint16_t port) {
 
             sock = openSocket(iface->getName(), *addr, port);
             if (sock < 0) {
-                isc_throw(SocketConfigError, "failed to open unicast socket");
+                const char* errstr = strerror(errno);
+                isc_throw(SocketConfigError, "failed to open link-local socket on "
+                          << addr->toText() << " on interface "
+                          << iface->getName() << ", reason: " << errstr);
             }
 
             // Binding socket to unicast address and then joining multicast group
@@ -414,8 +447,10 @@ bool IfaceMgr::openSockets6(const uint16_t port) {
                                    IOAddress(ALL_DHCP_RELAY_AGENTS_AND_SERVERS),
                                    port);
             if (sock2 < 0) {
+                const char* errstr = strerror(errno);
                 isc_throw(SocketConfigError, "Failed to open multicast socket on "
-                          << " interface " << iface->getFullName());
+                          << " interface " << iface->getFullName() << ", reason:"
+                          << errstr);
                 iface->delSocket(sock); // delete previously opened socket
             }
 #endif
@@ -608,7 +643,9 @@ IfaceMgr::getLocalAddress(const IOAddress& remote_addr, const uint16_t port) {
         // interface.
         sock.open(asio::ip::udp::v4(), err_code);
         if (err_code) {
-            isc_throw(Unexpected, "failed to open UDPv4 socket");
+            const char* errstr = strerror(errno);
+            isc_throw(Unexpected, "failed to open UDPv4 socket, reason:"
+                      << errstr);
         }
         sock.set_option(asio::socket_base::broadcast(true), err_code);
         if (err_code) {
@@ -1137,16 +1174,50 @@ uint16_t IfaceMgr::getSocket(const isc::dhcp::Pkt6& pkt) {
                   << pkt.getIface());
     }
 
+
     const Iface::SocketCollection& socket_collection = iface->getSockets();
+
+    Iface::SocketCollection::const_iterator candidate = socket_collection.end();
+
     Iface::SocketCollection::const_iterator s;
     for (s = socket_collection.begin(); s != socket_collection.end(); ++s) {
-        if ((s->family_ == AF_INET6) &&
-            (!s->addr_.getAddress().to_v6().is_multicast())) {
+
+        // We should not merge those conditions for debugging reasons.
+
+        // V4 sockets are useless for sending v6 packets.
+        if (s->family_ != AF_INET6) {
+            continue;
+        }
+
+        // Sockets bound to multicast address are useless for sending anything.
+        if (s->addr_.getAddress().to_v6().is_multicast()) {
+            continue;
+        }
+
+        if (s->addr_ == pkt.getLocalAddr()) {
+            // This socket is bound to the source address. This is perfect
+            // match, no need to look any further.
             return (s->sockfd_);
         }
-        /// @todo: Add more checks here later. If remote address is
-        /// not link-local, we can't use link local bound socket
-        /// to send data.
+
+        // If we don't have any other candidate, this one will do
+        if (candidate == socket_collection.end()) {
+            candidate = s;
+        } else {
+            // If we want to send something to link-local and the socket is
+            // bound to link-local or we want to send to global and the socket
+            // is bound to global, then use it as candidate
+            if ( (pkt.getRemoteAddr().getAddress().to_v6().is_link_local() &&
+                s->addr_.getAddress().to_v6().is_link_local()) ||
+                 (!pkt.getRemoteAddr().getAddress().to_v6().is_link_local() &&
+                  !s->addr_.getAddress().to_v6().is_link_local()) ) {
+                candidate = s;
+            }
+        }
+    }
+
+    if (candidate != socket_collection.end()) {
+        return (candidate->sockfd_);
     }
 
     isc_throw(Unexpected, "Interface " << iface->getFullName()
@@ -1175,5 +1246,6 @@ uint16_t IfaceMgr::getSocket(isc::dhcp::Pkt4 const& pkt) {
               << " does not have any suitable IPv4 sockets open.");
 }
 
+
 } // end of namespace isc::dhcp
 } // end of namespace isc

+ 24 - 0
src/lib/dhcp/iface_mgr.h

@@ -264,6 +264,27 @@ public:
     /// @return collection of sockets added to interface
     const SocketCollection& getSockets() const { return sockets_; }
 
+    /// @brief Removes any unicast addresses
+    ///
+    /// Removes any unicast addresses that the server was configured to
+    /// listen on
+    void clearUnicasts() {
+        unicasts_.clear();
+    }
+
+    /// @brief Adds unicast the server should listen on
+    ///
+    /// @throw BadValue if specified address is already defined on interface
+    /// @param addr unicast address to listen on
+    void addUnicast(const isc::asiolink::IOAddress& addr);
+
+    /// @brief Returns a container of addresses the server should listen on
+    ///
+    /// @return address collection (may be empty)
+    const AddressCollection& getUnicasts() const {
+        return unicasts_;
+    }
+
 protected:
     /// Socket used to send data.
     SocketCollection sockets_;
@@ -277,6 +298,9 @@ protected:
     /// List of assigned addresses.
     AddressCollection addrs_;
 
+    /// List of unicast addresses the server should listen on
+    AddressCollection unicasts_;
+
     /// Link-layer address.
     uint8_t mac_[MAX_MAC_LEN];
 

+ 4 - 2
src/lib/dhcp/option_definition.cc

@@ -137,12 +137,14 @@ OptionDefinition::optionFactory(Option::Universe u, uint16_t type,
             return (factoryGeneric(u, type, begin, end));
 
         case OPT_UINT8_TYPE:
-            return (array_type_ ? factoryGeneric(u, type, begin, end) :
+            return (array_type_ ?
+                    factoryIntegerArray<uint8_t>(u, type, begin, end) :
                     factoryInteger<uint8_t>(u, type, getEncapsulatedSpace(),
                                             begin, end, callback));
 
         case OPT_INT8_TYPE:
-            return (array_type_ ? factoryGeneric(u, type, begin, end) :
+            return (array_type_ ?
+                    factoryIntegerArray<int8_t>(u, type, begin, end) :
                     factoryInteger<int8_t>(u, type, getEncapsulatedSpace(),
                                            begin, end, callback));
 

+ 4 - 0
src/lib/dhcp/pkt4.cc

@@ -108,6 +108,10 @@ Pkt4::pack() {
         isc_throw(InvalidOperation, "Can't build Pkt4 packet. HWAddr not set.");
     }
 
+    // Clear the output buffer to make sure that consecutive calls to pack()
+    // will not result in concatenation of multiple packet copies.
+    buffer_out_.clear();
+
     try {
         size_t hw_len = hwaddr_->hwaddr_.size();
 

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

@@ -72,6 +72,7 @@ public:
     /// Prepares on-wire format of message and all its options.
     /// Options must be stored in options_ field.
     /// Output buffer will be stored in buffer_out_.
+    /// The buffer_out_ is cleared before writting to the buffer.
     ///
     /// @throw InvalidOperation if packing fails
     void

+ 17 - 15
src/lib/dhcp/pkt6.cc

@@ -43,7 +43,7 @@ Pkt6::Pkt6(const uint8_t* buf, uint32_t buf_len, DHCPv6Proto proto /* = UDP */)
     remote_addr_("::"),
     local_port_(0),
     remote_port_(0),
-    bufferOut_(0) {
+    buffer_out_(0) {
     data_.resize(buf_len);
     memcpy(&data_[0], buf, buf_len);
 }
@@ -58,7 +58,7 @@ Pkt6::Pkt6(uint8_t msg_type, uint32_t transid, DHCPv6Proto proto /*= UDP*/) :
     remote_addr_("::"),
     local_port_(0),
     remote_port_(0),
-    bufferOut_(0) {
+    buffer_out_(0) {
 }
 
 uint16_t Pkt6::len() {
@@ -199,6 +199,8 @@ Pkt6::pack() {
 void
 Pkt6::packUDP() {
     try {
+        // Make sure that the buffer is empty before we start writting to it.
+        buffer_out_.clear();
 
         // is this a relayed packet?
         if (!relay_info_.empty()) {
@@ -215,11 +217,11 @@ Pkt6::packUDP() {
                  relay != relay_info_.end(); ++relay) {
 
                 // build relay-forw/relay-repl header (see RFC3315, section 7)
-                bufferOut_.writeUint8(relay->msg_type_);
-                bufferOut_.writeUint8(relay->hop_count_);
-                bufferOut_.writeData(&(relay->linkaddr_.toBytes()[0]),
+                buffer_out_.writeUint8(relay->msg_type_);
+                buffer_out_.writeUint8(relay->hop_count_);
+                buffer_out_.writeData(&(relay->linkaddr_.toBytes()[0]),
                                      isc::asiolink::V6ADDRESS_LEN);
-                bufferOut_.writeData(&relay->peeraddr_.toBytes()[0],
+                buffer_out_.writeData(&relay->peeraddr_.toBytes()[0],
                                      isc::asiolink::V6ADDRESS_LEN);
 
                 // store every option in this relay scope. Usually that will be
@@ -230,28 +232,28 @@ Pkt6::packUDP() {
                 for (OptionCollection::const_iterator opt =
                          relay->options_.begin();
                      opt != relay->options_.end(); ++opt) {
-                    (opt->second)->pack(bufferOut_);
+                    (opt->second)->pack(buffer_out_);
                 }
 
                 // and include header relay-msg option. Its payload will be
                 // generated in the next iteration (if there are more relays)
                 // or outside the loop (if there are no more relays and the
                 // payload is a direct message)
-                bufferOut_.writeUint16(D6O_RELAY_MSG);
-                bufferOut_.writeUint16(relay->relay_msg_len_);
+                buffer_out_.writeUint16(D6O_RELAY_MSG);
+                buffer_out_.writeUint16(relay->relay_msg_len_);
             }
 
         }
 
         // DHCPv6 header: message-type (1 octect) + transaction id (3 octets)
-        bufferOut_.writeUint8(msg_type_);
+        buffer_out_.writeUint8(msg_type_);
         // store 3-octet transaction-id
-        bufferOut_.writeUint8( (transid_ >> 16) & 0xff );
-        bufferOut_.writeUint8( (transid_ >> 8) & 0xff );
-        bufferOut_.writeUint8( (transid_) & 0xff );
+        buffer_out_.writeUint8( (transid_ >> 16) & 0xff );
+        buffer_out_.writeUint8( (transid_ >> 8) & 0xff );
+        buffer_out_.writeUint8( (transid_) & 0xff );
 
         // the rest are options
-        LibDHCP::packOptions(bufferOut_, options_);
+        LibDHCP::packOptions(buffer_out_, options_);
     }
     catch (const Exception& e) {
        // An exception is thrown and message will be written to Logger
@@ -502,7 +504,7 @@ Pkt6::delOption(uint16_t type) {
 }
 
 void Pkt6::repack() {
-    bufferOut_.writeData(&data_[0], data_.size());
+    buffer_out_.writeData(&data_[0], data_.size());
 }
 
 void

+ 4 - 3
src/lib/dhcp/pkt6.h

@@ -115,6 +115,7 @@ public:
     /// Options must be stored in options_ field.
     /// Output buffer will be stored in data_. Length
     /// will be set in data_len_.
+    /// The output buffer is cleared before new data is written to it.
     ///
     /// @throw BadValue if packet protocol is invalid, InvalidOperation
     /// if packing fails, or NotImplemented if protocol is TCP (IPv6 over TCP is
@@ -139,7 +140,7 @@ public:
     /// zero length
     ///
     /// @return reference to output buffer
-    const isc::util::OutputBuffer& getBuffer() const { return (bufferOut_); };
+    const isc::util::OutputBuffer& getBuffer() const { return (buffer_out_); };
 
     /// @brief Returns protocol of this packet (UDP or TCP).
     ///
@@ -530,7 +531,7 @@ protected:
     /// remote TCP or UDP port
     uint16_t remote_port_;
 
-    /// output buffer (used during message transmission)
+    /// Output buffer (used during message transmission)
     ///
     /// @warning This protected member is accessed by derived
     /// classes directly. One of such derived classes is
@@ -538,7 +539,7 @@ protected:
     /// behavior must be taken into consideration before making
     /// changes to this member such as access scope restriction or
     /// data format change etc.
-    isc::util::OutputBuffer bufferOut_;
+    isc::util::OutputBuffer buffer_out_;
 
     /// packet timestamp
     boost::posix_time::ptime timestamp_;

+ 134 - 0
src/lib/dhcp/tests/iface_mgr_unittest.cc

@@ -145,6 +145,27 @@ public:
         return (sockets_count);
     }
 
+    /// @brief returns socket bound to a specific address (or NULL)
+    ///
+    /// A helper function, used to pick a socketinfo that is bound to a given
+    /// address.
+    ///
+    /// @param sockets sockets collection
+    /// @param addr address the socket is bound to
+    ///
+    /// @return socket info structure (or NULL)
+    const isc::dhcp::SocketInfo*
+    getSocketByAddr(const isc::dhcp::Iface::SocketCollection& sockets,
+                    const IOAddress& addr) {
+        for (isc::dhcp::Iface::SocketCollection::const_iterator s =
+                 sockets.begin(); s != sockets.end(); ++s) {
+            if (s->addr_ == addr) {
+                return (&(*s));
+            }
+        }
+        return (NULL);
+    }
+
 };
 
 // We need some known interface to work reliably. Loopback interface is named
@@ -781,6 +802,7 @@ TEST_F(IfaceMgrTest, sendReceive6) {
     // try to send/receive data over the closed socket. Closed socket's descriptor is
     // still being hold by IfaceMgr which will try to use it to receive data.
     close(socket1);
+    close(socket2);
     EXPECT_THROW(ifacemgr->receive6(10), SocketReadError);
     EXPECT_THROW(ifacemgr->send(sendPkt), SocketWriteError);
 }
@@ -1520,4 +1542,116 @@ TEST_F(IfaceMgrTest, controlSession) {
     close(pipefd[0]);
 }
 
+// Test checks if the unicast sockets can be opened.
+// This test is now disabled, because there is no reliable way to test it. We
+// can't even use loopback, beacuse openSockets() skips loopback interface
+// (as it should be, because DHCP server is not supposed to listen on loopback).
+TEST_F(IfaceMgrTest, DISABLED_openUnicastSockets) {
+    /// @todo Need to implement a test that is able to check whether we can open
+    /// unicast sockets. There are 2 problems with it:
+    /// 1. We need to have a non-link-local address on an interface that is
+    ///    up, running, IPv6 and multicast capable
+    /// 2. We need that information on every OS that we run tests on. So far
+    ///    we are only supporting interface detection in Linux.
+    ///
+    /// To achieve this, we will probably need a pre-test setup, similar to what
+    /// BIND9 is doing (i.e. configuring well known addresses on loopback).
+
+    scoped_ptr<NakedIfaceMgr> ifacemgr(new NakedIfaceMgr());
+
+    // Get the interface (todo: which interface)
+    Iface* iface = ifacemgr->getIface("eth0");
+    ASSERT_TRUE(iface);
+    iface->inactive6_ = false;
+
+    // Tell the interface that it should bind to this global interface
+    EXPECT_NO_THROW(iface->addUnicast(IOAddress("2001:db8::1")));
+
+    // Tell IfaceMgr to open sockets. This should trigger at least 2 sockets
+    // to open on eth0: link-local and global. On some systems (Linux), an
+    // additional socket for multicast may be opened.
+    EXPECT_TRUE(ifacemgr->openSockets6(PORT1));
+
+    const Iface::SocketCollection& sockets = iface->getSockets();
+    ASSERT_GE(2, sockets.size());
+
+    // Global unicast should be first
+    EXPECT_TRUE(getSocketByAddr(sockets, IOAddress("2001:db8::1")));
+    EXPECT_TRUE(getSocketByAddr(sockets, IOAddress("figure-out-link-local-addr")));
+}
+
+// Checks if there is a protection against unicast duplicates.
+TEST_F(IfaceMgrTest, unicastDuplicates) {
+    NakedIfaceMgr ifacemgr;
+
+    Iface* iface = ifacemgr.getIface(LOOPBACK);
+    if (iface == NULL) {
+        cout << "Local loopback interface not found. Skipping test. " << endl;
+        return;
+    }
+
+    // Tell the interface that it should bind to this global interface
+    EXPECT_NO_THROW(iface->addUnicast(IOAddress("2001:db8::1")));
+
+    // Tell the interface that it should bind to this global interface
+    EXPECT_THROW(iface->addUnicast(IOAddress("2001:db8::1")), BadValue);
+}
+
+// This test requires addresses 2001:db8:15c::1/128 and fe80::1/64 to be
+// configured on loopback interface
+//
+// Useful commands:
+// ip a a 2001:db8:15c::1/128 dev lo
+// ip a a fe80::1/64 dev lo
+//
+// If you do not issue those commands before running this test, it will fail.
+TEST_F(IfaceMgrTest, DISABLED_getSocket) {
+    // Testing socket operation in a portable way is tricky
+    // without interface detection implemented.
+
+    scoped_ptr<NakedIfaceMgr> ifacemgr(new NakedIfaceMgr());
+
+    IOAddress lo_addr("::1");
+    IOAddress link_local("fe80::1");
+    IOAddress global("2001:db8:15c::1");
+
+    IOAddress dst_link_local("fe80::dead:beef");
+    IOAddress dst_global("2001:db8:15c::dead:beef");
+
+    // Bind loopback address
+    int socket1 = ifacemgr->openSocket(LOOPBACK, lo_addr, 10547);
+    EXPECT_GE(socket1, 0); // socket >= 0
+
+    // Bind link-local address
+    int socket2 = ifacemgr->openSocket(LOOPBACK, link_local, 10547);
+    EXPECT_GE(socket2, 0);
+
+    int socket3 = ifacemgr->openSocket(LOOPBACK, global, 10547);
+    EXPECT_GE(socket3, 0);
+
+    // Let's make sure those sockets are unique
+    EXPECT_NE(socket1, socket2);
+    EXPECT_NE(socket2, socket3);
+    EXPECT_NE(socket3, socket1);
+
+    // Create a packet
+    Pkt6 pkt6(DHCPV6_SOLICIT, 123);
+    pkt6.setIface(LOOPBACK);
+
+    // Check that packets sent to link-local will get socket bound to link local
+    pkt6.setLocalAddr(global);
+    pkt6.setRemoteAddr(dst_global);
+    EXPECT_EQ(socket3, ifacemgr->getSocket(pkt6));
+
+    // Check that packets sent to link-local will get socket bound to link local
+    pkt6.setLocalAddr(link_local);
+    pkt6.setRemoteAddr(dst_link_local);
+    EXPECT_EQ(socket2, ifacemgr->getSocket(pkt6));
+
+    // Close sockets here because the following tests will want to
+    // open sockets on the same ports.
+    ifacemgr->closeSockets();
+}
+
+
 }

+ 1 - 1
src/lib/dhcp/tests/libdhcp++_unittest.cc

@@ -707,7 +707,7 @@ TEST_F(LibDhcpTest, stdOptionDefs4) {
                                     typeid(OptionCustom));
 
     LibDhcpTest::testStdOptionDefs4(DHO_DHCP_PARAMETER_REQUEST_LIST, begin, end,
-                                    typeid(Option));
+                                    typeid(OptionUint8Array));
 
     LibDhcpTest::testStdOptionDefs4(DHO_DHCP_MESSAGE, begin, end,
                                     typeid(OptionString));

+ 33 - 4
src/lib/dhcpsrv/cfgmgr.cc

@@ -16,6 +16,7 @@
 #include <dhcp/libdhcp++.h>
 #include <dhcpsrv/cfgmgr.h>
 #include <dhcpsrv/dhcpsrv_log.h>
+#include <string>
 
 using namespace isc::asiolink;
 using namespace isc::util;
@@ -269,14 +270,31 @@ std::string CfgMgr::getDataDir() {
 
 void
 CfgMgr::addActiveIface(const std::string& iface) {
-    if (isIfaceListedActive(iface)) {
+
+    size_t pos = iface.find("/");
+    std::string iface_copy = iface;
+
+    if (pos != std::string::npos) {
+        std::string addr_string = iface.substr(pos + 1);
+        try {
+            IOAddress addr(addr_string);
+            iface_copy = iface.substr(0,pos);
+            unicast_addrs_.insert(make_pair(iface_copy, addr));
+        } catch (...) {
+            isc_throw(BadValue, "Can't convert '" << addr_string
+                      << "' into address in interface defition ('"
+                      << iface << "')");
+        }
+    }
+
+    if (isIfaceListedActive(iface_copy)) {
         isc_throw(DuplicateListeningIface,
-                  "attempt to add duplicate interface '" << iface << "'"
+                  "attempt to add duplicate interface '" << iface_copy << "'"
                   " to the set of interfaces on which server listens");
     }
     LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE, DHCPSRV_CFGMGR_ADD_IFACE)
-        .arg(iface);
-    active_ifaces_.push_back(iface);
+        .arg(iface_copy);
+    active_ifaces_.push_back(iface_copy);
 }
 
 void
@@ -292,6 +310,8 @@ CfgMgr::deleteActiveIfaces() {
               DHCPSRV_CFGMGR_CLEAR_ACTIVE_IFACES);
     active_ifaces_.clear();
     all_ifaces_active_ = false;
+
+    unicast_addrs_.clear();
 }
 
 bool
@@ -319,6 +339,15 @@ CfgMgr::isIfaceListedActive(const std::string& iface) const {
     return (false);
 }
 
+const isc::asiolink::IOAddress*
+CfgMgr::getUnicast(const std::string& iface) const {
+    UnicastIfacesCollection::const_iterator addr = unicast_addrs_.find(iface);
+    if (addr == unicast_addrs_.end()) {
+        return (NULL);
+    }
+    return (&(*addr).second);
+}
+
 CfgMgr::CfgMgr()
     : datadir_(DHCP_DATA_DIR),
       all_ifaces_active_(false) {

+ 17 - 0
src/lib/dhcpsrv/cfgmgr.h

@@ -305,6 +305,17 @@ public:
     /// interfaces on which server is configured to listen.
     bool isActiveIface(const std::string& iface) const;
 
+    /// @brief returns unicast a given interface should listen on (or NULL)
+    ///
+    /// This method will return an address for a specified interface, if the
+    /// server is supposed to listen on unicast address. This address is
+    /// intended to be used immediately. This pointer is valid only until
+    /// the next configuration change.
+    ///
+    /// @return IOAddress pointer (or NULL if none)
+    const isc::asiolink::IOAddress*
+    getUnicast(const std::string& iface) const;
+
 protected:
 
     /// @brief Protected constructor.
@@ -372,6 +383,12 @@ private:
     std::list<std::string> active_ifaces_;
     //@}
 
+    /// @name a collection of unicast addresses and the interfaces names the
+    //        server is supposed to listen on
+    //@{
+    typedef std::map<std::string, isc::asiolink::IOAddress> UnicastIfacesCollection;
+    UnicastIfacesCollection unicast_addrs_;
+
     /// A flag which indicates that server should listen on all available
     /// interfaces.
     bool all_ifaces_active_;

+ 1 - 0
src/lib/dhcpsrv/dhcp_parsers.cc

@@ -231,6 +231,7 @@ InterfaceListConfigParser::commit() {
 
 bool
 InterfaceListConfigParser::isIfaceAdded(const std::string& iface) const {
+
     for (IfaceListStorage::const_iterator it = interfaces_.begin();
          it != interfaces_.end(); ++it) {
         if (iface == *it) {

+ 14 - 1
src/lib/dhcpsrv/subnet.cc

@@ -176,13 +176,26 @@ Subnet4::Subnet4(const isc::asiolink::IOAddress& prefix, uint8_t length,
                  const Triplet<uint32_t>& t1,
                  const Triplet<uint32_t>& t2,
                  const Triplet<uint32_t>& valid_lifetime)
-    :Subnet(prefix, length, t1, t2, valid_lifetime) {
+    :Subnet(prefix, length, t1, t2, valid_lifetime),
+    siaddr_(IOAddress("0.0.0.0")) {
     if (!prefix.isV4()) {
         isc_throw(BadValue, "Non IPv4 prefix " << prefix.toText()
                   << " specified in subnet4");
     }
 }
 
+void Subnet4::setSiaddr(const isc::asiolink::IOAddress& siaddr) {
+    if (!siaddr.isV4()) {
+        isc_throw(BadValue, "Can't set siaddr to non-IPv4 address "
+                  << siaddr.toText());
+    }
+    siaddr_ = siaddr;
+}
+
+isc::asiolink::IOAddress Subnet4::getSiaddr() const {
+    return (siaddr_);
+}
+
 const PoolCollection& Subnet::getPools(Lease::Type type) const {
     // check if the type is valid (and throw if it isn't)
     checkType(type);

+ 15 - 0
src/lib/dhcpsrv/subnet.h

@@ -505,6 +505,18 @@ public:
             const Triplet<uint32_t>& t2,
             const Triplet<uint32_t>& valid_lifetime);
 
+    /// @brief Sets siaddr for the Subnet4
+    ///
+    /// Will be used for siaddr field (the next server) that typically is used
+    /// as TFTP server. If not specified, the default value of 0.0.0.0 is
+    /// used.
+    void setSiaddr(const isc::asiolink::IOAddress& siaddr);
+
+    /// @brief Returns siaddr for this subnet
+    ///
+    /// @return siaddr value
+    isc::asiolink::IOAddress getSiaddr() const;
+
 protected:
 
     /// @brief Check if option is valid and can be added to a subnet.
@@ -527,6 +539,9 @@ protected:
     /// @param type type to be checked
     /// @throw BadValue if invalid value is used
     virtual void checkType(Lease::Type type) const;
+
+    /// @brief siaddr value for this subnet
+    isc::asiolink::IOAddress siaddr_;
 };
 
 /// @brief A pointer to a Subnet4 object

+ 59 - 3
src/lib/dhcpsrv/tests/cfgmgr_unittest.cc

@@ -579,20 +579,55 @@ TEST_F(CfgMgrTest, optionSpace6) {
 TEST_F(CfgMgrTest, addActiveIface) {
     CfgMgr& cfg_mgr = CfgMgr::instance();
 
-    cfg_mgr.addActiveIface("eth0");
-    cfg_mgr.addActiveIface("eth1");
+    EXPECT_NO_THROW(cfg_mgr.addActiveIface("eth0"));
+    EXPECT_NO_THROW(cfg_mgr.addActiveIface("eth1"));
 
     EXPECT_TRUE(cfg_mgr.isActiveIface("eth0"));
     EXPECT_TRUE(cfg_mgr.isActiveIface("eth1"));
     EXPECT_FALSE(cfg_mgr.isActiveIface("eth2"));
 
-    cfg_mgr.deleteActiveIfaces();
+    EXPECT_NO_THROW(cfg_mgr.deleteActiveIfaces());
 
     EXPECT_FALSE(cfg_mgr.isActiveIface("eth0"));
     EXPECT_FALSE(cfg_mgr.isActiveIface("eth1"));
     EXPECT_FALSE(cfg_mgr.isActiveIface("eth2"));
 }
 
+
+// This test verifies that it is possible to specify interfaces that server
+// should listen on.
+TEST_F(CfgMgrTest, addUnicastAddresses) {
+    CfgMgr& cfg_mgr = CfgMgr::instance();
+
+    EXPECT_NO_THROW(cfg_mgr.addActiveIface("eth1/2001:db8::1"));
+    EXPECT_NO_THROW(cfg_mgr.addActiveIface("eth2/2001:db8::2"));
+    EXPECT_NO_THROW(cfg_mgr.addActiveIface("eth3"));
+
+    EXPECT_TRUE(cfg_mgr.isActiveIface("eth1"));
+    EXPECT_TRUE(cfg_mgr.isActiveIface("eth2"));
+    EXPECT_TRUE(cfg_mgr.isActiveIface("eth3"));
+    EXPECT_FALSE(cfg_mgr.isActiveIface("eth4"));
+
+    ASSERT_TRUE(cfg_mgr.getUnicast("eth1"));
+    EXPECT_EQ("2001:db8::1", cfg_mgr.getUnicast("eth1")->toText());
+    EXPECT_EQ("2001:db8::2", cfg_mgr.getUnicast("eth2")->toText());
+    EXPECT_FALSE(cfg_mgr.getUnicast("eth3"));
+    EXPECT_FALSE(cfg_mgr.getUnicast("eth4"));
+
+    EXPECT_NO_THROW(cfg_mgr.deleteActiveIfaces());
+
+    EXPECT_FALSE(cfg_mgr.isActiveIface("eth1"));
+    EXPECT_FALSE(cfg_mgr.isActiveIface("eth2"));
+    EXPECT_FALSE(cfg_mgr.isActiveIface("eth3"));
+    EXPECT_FALSE(cfg_mgr.isActiveIface("eth4"));
+
+    ASSERT_FALSE(cfg_mgr.getUnicast("eth1"));
+    ASSERT_FALSE(cfg_mgr.getUnicast("eth2"));
+    EXPECT_FALSE(cfg_mgr.getUnicast("eth3"));
+    EXPECT_FALSE(cfg_mgr.getUnicast("eth4"));
+}
+
+
 // This test verifies that it is possible to set the flag which configures the
 // server to listen on all interfaces.
 TEST_F(CfgMgrTest, activateAllIfaces) {
@@ -618,6 +653,27 @@ TEST_F(CfgMgrTest, activateAllIfaces) {
     EXPECT_FALSE(cfg_mgr.isActiveIface("eth2"));
 }
 
+/// @todo Add unit-tests for testing:
+/// - addActiveIface() with invalid interface name
+/// - addActiveIface() with the same interface twice
+/// - addActiveIface() with a bogus address
+///
+/// This is somewhat tricky. Care should be taken here, because it is rather
+/// difficult to decide if interface name is valid or not. Some servers, e.g.
+/// dibbler, allow to specify interface names that are not currently present in
+/// the system. The server accepts them, but upon discovering that they are
+/// yet available (for different definitions of not being available), adds
+/// the to to-be-activated list.
+///
+/// Cases covered by dibbler are:
+/// - missing interface (e.g. PPP connection that is not established yet)
+/// - downed interface (no link local address, no way to open sockets)
+/// - up, but not running interface (wifi up, but not associated)
+/// - tentative addresses (interface up and running, but DAD procedure is
+///   still in progress)
+/// - weird interfaces without link-local addresses (don't ask, 6rd tunnels
+///   look weird to me as well)
+
 // No specific tests for getSubnet6. That method (2 overloaded versions) is tested
 // in Dhcpv6SrvTest.selectSubnetAddr and Dhcpv6SrvTest.selectSubnetIface
 // (see src/bin/dhcp6/tests/dhcp6_srv_unittest.cc)

+ 18 - 0
src/lib/dhcpsrv/tests/subnet_unittest.cc

@@ -58,6 +58,24 @@ TEST(Subnet4Test, in_range) {
     EXPECT_FALSE(subnet.inRange(IOAddress("255.255.255.255")));
 }
 
+// Checks whether siaddr field can be set and retrieved correctly.
+TEST(Subnet4Test, siaddr) {
+    Subnet4 subnet(IOAddress("192.0.2.1"), 24, 1000, 2000, 3000);
+
+    // Check if the default is 0.0.0.0
+    EXPECT_EQ("0.0.0.0", subnet.getSiaddr().toText());
+
+    // Check that we can set it up
+    EXPECT_NO_THROW(subnet.setSiaddr(IOAddress("1.2.3.4")));
+
+    // Check that we can get it back
+    EXPECT_EQ("1.2.3.4", subnet.getSiaddr().toText());
+
+    // Check that only v4 addresses are supported
+    EXPECT_THROW(subnet.setSiaddr(IOAddress("2001:db8::1")),
+        BadValue);
+}
+
 TEST(Subnet4Test, Pool4InSubnet4) {
 
     Subnet4Ptr subnet(new Subnet4(IOAddress("192.1.2.0"), 24, 1, 2, 3));

+ 1 - 1
tests/tools/perfdhcp/perf_pkt6.cc

@@ -43,7 +43,7 @@ PerfPkt6::rawPack() {
                                options_,
                                getTransidOffset(),
                                getTransid(),
-                               bufferOut_));
+                               buffer_out_));
 }
 
 bool