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
 691.	[bug]		marcin
 	libdhcp++: Created definitions for standard DHCPv4 options:
 	libdhcp++: Created definitions for standard DHCPv4 options:
 	tftp-server-name (66) and boot-file-name (67). Also, fixed definition
 	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/Makefile
                  src/bin/usermgr/tests/Makefile
                  src/bin/usermgr/tests/Makefile
                  src/bin/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/Makefile
                  src/lib/asiolink/Makefile
                  src/lib/asiolink/Makefile
                  src/lib/asiolink/tests/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/spec_config.h.pre
            src/bin/d2/tests/test_data_files_config.h
            src/bin/d2/tests/test_data_files_config.h
            src/bin/tests/process_rename_test.py
            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/config/tests/data_def_unittests_config.h
            src/lib/dhcpsrv/tests/test_libraries.h
            src/lib/dhcpsrv/tests/test_libraries.h
            src/lib/python/isc/config/tests/config_test
            src/lib/python/isc/config/tests/config_test

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

@@ -4390,6 +4390,30 @@ Dhcp4/subnet4	[]	list	(default)
       </para>
       </para>
     </section>
     </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">
     <section id="dhcp4-std">
       <title>Supported Standards</title>
       <title>Supported Standards</title>
       <para>The following standards and draft standards are currently
       <para>The following standards and draft standards are currently
@@ -4669,6 +4693,43 @@ Dhcp6/subnet6/	list
       </para>
       </para>
     </section>
     </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>
     <section>
       <title>Subnet and Address Pool</title>
       <title>Subnet and Address Pool</title>
       <para>
       <para>

+ 2 - 0
src/Makefile.am

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

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

@@ -255,7 +255,7 @@ Query::addWildcardNXRRSETProof(ZoneFinder& finder, ConstRRsetPtr nsec) {
     if (nsec->getRdataCount() == 0) {
     if (nsec->getRdataCount() == 0) {
         isc_throw(BadNSEC, "NSEC for WILDCARD_NXRRSET is empty");
         isc_throw(BadNSEC, "NSEC for WILDCARD_NXRRSET is empty");
     }
     }
-    
+
     ConstZoneFinderContextPtr fcontext =
     ConstZoneFinderContextPtr fcontext =
         finder.find(*qname_, RRType::NSEC(),
         finder.find(*qname_, RRType::NSEC(),
                     dnssec_opt_ | ZoneFinder::NO_WILDCARD);
                     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'])
                 self.__propagate_component_config(new_config['components'])
             return isc.config.ccsession.create_answer(0)
             return isc.config.ccsession.create_answer(0)
         except Exception as e:
         except Exception as e:
+            logger.error(BIND10_RECONFIGURE_ERROR, e)
             return isc.config.ccsession.create_answer(1, str(e))
             return isc.config.ccsession.create_answer(1, str(e))
 
 
     def get_processes(self):
     def get_processes(self):
@@ -597,6 +598,13 @@ class Init:
             process, the log_starting/log_started methods are not used.
             process, the log_starting/log_started methods are not used.
         """
         """
         logger.info(BIND10_STARTING_CC)
         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.ccs = isc.config.ModuleCCSession(SPECFILE_LOCATION,
                                       self.config_handler,
                                       self.config_handler,
                                       self.command_handler,
                                       self.command_handler,
@@ -764,9 +772,14 @@ class Init:
         it might want to choose if it is for this one).
         it might want to choose if it is for this one).
         """
         """
         logger.info(BIND10_STOP_PROCESS, process)
         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):
     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
 will wait for the configuration manager before reporting an error is
 set with the command line --wait switch, which has a default value of
 set with the command line --wait switch, which has a default value of
 ten seconds.
 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))  {
             (config_id.compare("rebind-timer") == 0))  {
             parser = new Uint32Parser(config_id, uint32_values_);
             parser = new Uint32Parser(config_id, uint32_values_);
         } else if ((config_id.compare("subnet") == 0) ||
         } 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_);
             parser = new StringParser(config_id, string_values_);
         } else if (config_id.compare("pool") == 0) {
         } else if (config_id.compare("pool") == 0) {
             parser = new Pool4Parser(config_id, pools_);
             parser = new Pool4Parser(config_id, pools_);
@@ -264,14 +265,34 @@ protected:
         Triplet<uint32_t> t2 = getParam("rebind-timer");
         Triplet<uint32_t> t2 = getParam("rebind-timer");
         Triplet<uint32_t> valid = getParam("valid-lifetime");
         Triplet<uint32_t> valid = getParam("valid-lifetime");
 
 
-        /// @todo: Convert this to logger once the parser is working reliably
         stringstream tmp;
         stringstream tmp;
         tmp << addr.toText() << "/" << (int)len
         tmp << addr.toText() << "/" << (int)len
             << " with params t1=" << t1 << ", t2=" << t2 << ", valid=" << valid;
             << " with params t1=" << t1 << ", t2=" << t2 << ", valid=" << valid;
 
 
         LOG_INFO(dhcp4_logger, DHCP4_CONFIG_NEW_SUBNET).arg(tmp.str());
         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) {
     } else if (config_id.compare("option-def") == 0) {
         parser  = new OptionDefListParser(config_id,
         parser  = new OptionDefListParser(config_id,
                                           globalContext()->option_defs_);
                                           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,
         parser  = new StringParser(config_id,
                                     globalContext()->string_values_);
                                     globalContext()->string_values_);
     } else if (config_id.compare("lease-database") == 0) {
     } else if (config_id.compare("lease-database") == 0) {

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

@@ -16,7 +16,7 @@
           "item_default": ""
           "item_default": ""
         }
         }
       },
       },
- 
+
       { "item_name": "interfaces",
       { "item_name": "interfaces",
         "item_type": "list",
         "item_type": "list",
         "item_optional": false,
         "item_optional": false,
@@ -48,6 +48,12 @@
         "item_default": 4000
         "item_default": 4000
       },
       },
 
 
+      { "item_name": "next-server",
+        "item_type": "string",
+        "item_optional": true,
+        "item_default": ""
+      },
+
       { "item_name": "option-def",
       { "item_name": "option-def",
         "item_type": "list",
         "item_type": "list",
         "item_optional": false,
         "item_optional": false,
@@ -218,6 +224,13 @@
                   "item_optional": false,
                   "item_optional": false,
                   "item_default": 7200
                   "item_default": 7200
                 },
                 },
+
+                { "item_name": "next-server",
+                  "item_type": "string",
+                  "item_optional": true,
+                  "item_default": "0.0.0.0"
+                },
+
                 { "item_name": "pool",
                 { "item_name": "pool",
                   "item_type": "list",
                   "item_type": "list",
                   "item_optional": false,
                   "item_optional": false,
@@ -290,7 +303,7 @@
 
 
         {
         {
             "command_name": "libreload",
             "command_name": "libreload",
-            "command_description": "Reloads the current hooks libraries.", 
+            "command_description": "Reloads the current hooks libraries.",
             "command_args": []
             "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
 and continue allocation with that server. This is a normal behavior and
 indicates successful operation.
 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
 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
 the specified client after receiving a DISCOVER message from it. There are
 many possible reasons for such a failure.
 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
 in response to client's REQUEST message. This is a normal behavior and
 indicates successful operation.
 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
 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
 specified client after receiving a REQUEST message from it.  There are many
 possible reasons for such a failure. Additional messages will indicate the
 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;
         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)
     LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL_DATA, DHCP4_SUBNET_SELECTED)
         .arg(subnet->toText());
         .arg(subnet->toText());
 
 
@@ -811,13 +818,6 @@ Dhcpv4Srv::assignLease(const Pkt4Ptr& question, Pkt4Ptr& answer) {
         opt->setUint32(lease->valid_lft_);
         opt->setUint32(lease->valid_lft_);
         answer->addOption(opt);
         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)
         // Subnet mask (type 1)
         answer->addOption(getNetmaskOption(subnet));
         answer->addOption(getNetmaskOption(subnet));
 
 
@@ -914,15 +914,18 @@ Dhcpv4Srv::processDiscover(Pkt4Ptr& discover) {
 
 
     copyDefaultFields(discover, offer);
     copyDefaultFields(discover, offer);
     appendDefaultOptions(offer, DHCPOFFER);
     appendDefaultOptions(offer, DHCPOFFER);
-    appendRequestedOptions(discover, offer);
     appendRequestedVendorOptions(discover, offer);
     appendRequestedVendorOptions(discover, offer);
 
 
     assignLease(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);
     return (offer);
 }
 }
@@ -938,7 +941,6 @@ Dhcpv4Srv::processRequest(Pkt4Ptr& request) {
 
 
     copyDefaultFields(request, ack);
     copyDefaultFields(request, ack);
     appendDefaultOptions(ack, DHCPACK);
     appendDefaultOptions(ack, DHCPACK);
-    appendRequestedOptions(request, ack);
     appendRequestedVendorOptions(request, ack);
     appendRequestedVendorOptions(request, ack);
 
 
     // Note that we treat REQUEST message uniformly, regardless if this is a
     // Note that we treat REQUEST message uniformly, regardless if this is a
@@ -946,10 +948,14 @@ Dhcpv4Srv::processRequest(Pkt4Ptr& request) {
     // or even rebinding.
     // or even rebinding.
     assignLease(request, ack);
     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);
     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());
     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
 // This test checks if it is possible to override global values
 // on a per subnet basis.
 // on a per subnet basis.
 TEST_F(Dhcp4ParserTest, subnetLocal) {
 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());
     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
 // a dummy MAC address
 const uint8_t dummyMacAddr[] = {0, 1, 2, 3, 4, 5};
 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/option_custom.h>
 #include <dhcp/iface_mgr.h>
 #include <dhcp/iface_mgr.h>
 #include <dhcpsrv/cfgmgr.h>
 #include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/lease.h>
 #include <dhcpsrv/lease_mgr.h>
 #include <dhcpsrv/lease_mgr.h>
 #include <dhcpsrv/lease_mgr_factory.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->getLocalHWAddr() == a->getLocalHWAddr());
     EXPECT_TRUE(q->getRemoteHWAddr() == a->getRemoteHWAddr());
     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_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
     // Check that something is offered
     EXPECT_TRUE(a->getYiaddr().toText() != "0.0.0.0");
     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*/) {
 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());
     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) {
 void Dhcpv4SrvTest::testDiscoverRequest(const uint8_t msg_type) {
     // Create an instance of the tested class.
     // Create an instance of the tested class.
     boost::scoped_ptr<NakedDhcpv4Srv> srv(new NakedDhcpv4Srv(0));
     boost::scoped_ptr<NakedDhcpv4Srv> srv(new NakedDhcpv4Srv(0));
@@ -318,17 +425,22 @@ void Dhcpv4SrvTest::testDiscoverRequest(const uint8_t msg_type) {
     // are returned when requested.
     // are returned when requested.
     configureRequestedOptions();
     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) {
     if (msg_type == DHCPDISCOVER) {
         ASSERT_NO_THROW(
         ASSERT_NO_THROW(
-            rsp = srv->processDiscover(req);
-            );
+            rsp = srv->processDiscover(received);
+        );
 
 
         // Should return OFFER
         // Should return OFFER
         ASSERT_TRUE(rsp);
         ASSERT_TRUE(rsp);
         EXPECT_EQ(DHCPOFFER, rsp->getType());
         EXPECT_EQ(DHCPOFFER, rsp->getType());
 
 
     } else {
     } else {
-        ASSERT_NO_THROW(rsp = srv->processRequest(req););
+        ASSERT_NO_THROW(rsp = srv->processRequest(received));
 
 
         // Should return ACK
         // Should return ACK
         ASSERT_TRUE(rsp);
         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
     // We did not request any options so these should not be present
     // in the RSP.
     // 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.
     // Repeat the test but request some options.
     // Add 'Parameter Request List' option.
     // Add 'Parameter Request List' option.
     addPrlOption(req);
     addPrlOption(req);
 
 
+    ASSERT_TRUE(createPacketFromBuffer(req, received));
+    ASSERT_TRUE(received->getOption(DHO_DHCP_PARAMETER_REQUEST_LIST));
+
     if (msg_type == DHCPDISCOVER) {
     if (msg_type == DHCPDISCOVER) {
-        ASSERT_NO_THROW(rsp = srv->processDiscover(req););
+        ASSERT_NO_THROW(rsp = srv->processDiscover(received));
 
 
         // Should return non-NULL packet.
         // Should return non-NULL packet.
         ASSERT_TRUE(rsp);
         ASSERT_TRUE(rsp);
         EXPECT_EQ(DHCPOFFER, rsp->getType());
         EXPECT_EQ(DHCPOFFER, rsp->getType());
 
 
     } else {
     } else {
-        ASSERT_NO_THROW(rsp = srv->processRequest(req););
+        ASSERT_NO_THROW(rsp = srv->processRequest(received));
 
 
         // Should return non-NULL packet.
         // Should return non-NULL packet.
         ASSERT_TRUE(rsp);
         ASSERT_TRUE(rsp);
@@ -364,7 +479,41 @@ void Dhcpv4SrvTest::testDiscoverRequest(const uint8_t msg_type) {
     }
     }
 
 
     // Check that the requested options are returned.
     // 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.
 /// @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)
     /// @param a answer (server's message)
     void messageCheck(const Pkt4Ptr& q, const Pkt4Ptr& a);
     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
     /// @brief generates client-id option
     ///
     ///
@@ -183,6 +205,32 @@ public:
     /// @return relayed DISCOVER
     /// @return relayed DISCOVER
     Pkt4Ptr captureRelayedDiscover();
     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
     /// @brief generates a DHCPv4 packet based on provided hex string
     ///
     ///
     /// @return created packet
     /// @return created packet
@@ -190,6 +238,14 @@ public:
 
 
     /// @brief Tests if Discover or Request message is processed correctly
     /// @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
     /// @param msg_type DHCPDISCOVER or DHCPREQUEST
     void testDiscoverRequest(const uint8_t msg_type);
     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.
         // reopen sockets according to new configuration.
         openActiveSockets(getPort());
         openActiveSockets(getPort());
 
 
-    } catch (const DhcpConfigError& ex) {
+    } catch (const std::exception& ex) {
         LOG_ERROR(dhcp6_logger, DHCP6_CONFIG_LOAD_FAIL).arg(ex.what());
         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
 been requested via a call to the 'shutdown' method of the core Dhcpv6Srv
 object.
 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
 % DHCP6_SRV_CONSTRUCT_ERROR error creating Dhcpv6Srv object, reason: %1
 This error message indicates that during startup, the construction of a
 This error message indicates that during startup, the construction of a
 core component within the IPv6 DHCP server (the Dhcpv6 server object)
 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");
                       << " trying to reopen sockets after reconfiguration");
         }
         }
         if (CfgMgr::instance().isActiveIface(iface->getName())) {
         if (CfgMgr::instance().isActiveIface(iface->getName())) {
-            iface_ptr->inactive4_ = false;
+            iface_ptr->inactive6_ = false;
             LOG_INFO(dhcp6_logger, DHCP6_ACTIVATE_INTERFACE)
             LOG_INFO(dhcp6_logger, DHCP6_ACTIVATE_INTERFACE)
                 .arg(iface->getFullName());
                 .arg(iface->getFullName());
 
 
@@ -2297,6 +2297,15 @@ Dhcpv6Srv::openActiveSockets(const uint16_t port) {
             iface_ptr->inactive6_ = true;
             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
     // Let's reopen active sockets. openSockets6 will check internally whether
     // sockets are marked active or inactive.
     // 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)
             # Append it to buffer (but check the data go away)
             if fileno in self.sendbuffs:
             if fileno in self.sendbuffs:
                 (last_sent, buff) = self.sendbuffs[fileno]
                 (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)
                     self.kill_socket(fileno, sock)
                     return False
                     return False
                 buff += msg
                 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'
 % 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
 Debug message. Creating a new subscription. Also creating a new data structure
 to hold it.
 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
 // 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
 // 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
 // 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
 // for the result codes: SUCCESS if the requested type of RR exists; NXRRSET
 // otherwise.  Due to its simplicity we implement it separately, rather than
 // otherwise.  Due to its simplicity we implement it separately, rather than
 // sharing the code with findInternal.
 // 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() {
 void IfaceMgr::closeSockets() {
     for (IfaceCollection::iterator iface = ifaces_.begin();
     for (IfaceCollection::iterator iface = ifaces_.begin();
          iface != ifaces_.end(); ++iface) {
          iface != ifaces_.end(); ++iface) {
@@ -343,8 +354,10 @@ bool IfaceMgr::openSockets4(const uint16_t port, const bool use_bcast) {
 
 
             }
             }
             if (sock < 0) {
             if (sock < 0) {
+                const char* errstr = strerror(errno);
                 isc_throw(SocketConfigError, "failed to open IPv4 socket"
                 isc_throw(SocketConfigError, "failed to open IPv4 socket"
-                          << " supporting broadcast traffic");
+                          << " supporting broadcast traffic, reason:"
+                          << errstr);
             }
             }
 
 
             count++;
             count++;
@@ -368,6 +381,23 @@ bool IfaceMgr::openSockets6(const uint16_t port) {
             continue;
             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();
         Iface::AddressCollection addrs = iface->getAddresses();
         for (Iface::AddressCollection::iterator addr = addrs.begin();
         for (Iface::AddressCollection::iterator addr = addrs.begin();
              addr != addrs.end();
              addr != addrs.end();
@@ -389,7 +419,10 @@ bool IfaceMgr::openSockets6(const uint16_t port) {
 
 
             sock = openSocket(iface->getName(), *addr, port);
             sock = openSocket(iface->getName(), *addr, port);
             if (sock < 0) {
             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
             // 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),
                                    IOAddress(ALL_DHCP_RELAY_AGENTS_AND_SERVERS),
                                    port);
                                    port);
             if (sock2 < 0) {
             if (sock2 < 0) {
+                const char* errstr = strerror(errno);
                 isc_throw(SocketConfigError, "Failed to open multicast socket on "
                 isc_throw(SocketConfigError, "Failed to open multicast socket on "
-                          << " interface " << iface->getFullName());
+                          << " interface " << iface->getFullName() << ", reason:"
+                          << errstr);
                 iface->delSocket(sock); // delete previously opened socket
                 iface->delSocket(sock); // delete previously opened socket
             }
             }
 #endif
 #endif
@@ -608,7 +643,9 @@ IfaceMgr::getLocalAddress(const IOAddress& remote_addr, const uint16_t port) {
         // interface.
         // interface.
         sock.open(asio::ip::udp::v4(), err_code);
         sock.open(asio::ip::udp::v4(), err_code);
         if (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);
         sock.set_option(asio::socket_base::broadcast(true), err_code);
         if (err_code) {
         if (err_code) {
@@ -1137,16 +1174,50 @@ uint16_t IfaceMgr::getSocket(const isc::dhcp::Pkt6& pkt) {
                   << pkt.getIface());
                   << pkt.getIface());
     }
     }
 
 
+
     const Iface::SocketCollection& socket_collection = iface->getSockets();
     const Iface::SocketCollection& socket_collection = iface->getSockets();
+
+    Iface::SocketCollection::const_iterator candidate = socket_collection.end();
+
     Iface::SocketCollection::const_iterator s;
     Iface::SocketCollection::const_iterator s;
     for (s = socket_collection.begin(); s != socket_collection.end(); ++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_);
             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()
     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.");
               << " does not have any suitable IPv4 sockets open.");
 }
 }
 
 
+
 } // end of namespace isc::dhcp
 } // end of namespace isc::dhcp
 } // end of namespace isc
 } // end of namespace isc

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

@@ -264,6 +264,27 @@ public:
     /// @return collection of sockets added to interface
     /// @return collection of sockets added to interface
     const SocketCollection& getSockets() const { return sockets_; }
     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:
 protected:
     /// Socket used to send data.
     /// Socket used to send data.
     SocketCollection sockets_;
     SocketCollection sockets_;
@@ -277,6 +298,9 @@ protected:
     /// List of assigned addresses.
     /// List of assigned addresses.
     AddressCollection addrs_;
     AddressCollection addrs_;
 
 
+    /// List of unicast addresses the server should listen on
+    AddressCollection unicasts_;
+
     /// Link-layer address.
     /// Link-layer address.
     uint8_t mac_[MAX_MAC_LEN];
     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));
             return (factoryGeneric(u, type, begin, end));
 
 
         case OPT_UINT8_TYPE:
         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(),
                     factoryInteger<uint8_t>(u, type, getEncapsulatedSpace(),
                                             begin, end, callback));
                                             begin, end, callback));
 
 
         case OPT_INT8_TYPE:
         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(),
                     factoryInteger<int8_t>(u, type, getEncapsulatedSpace(),
                                            begin, end, callback));
                                            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.");
         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 {
     try {
         size_t hw_len = hwaddr_->hwaddr_.size();
         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.
     /// Prepares on-wire format of message and all its options.
     /// Options must be stored in options_ field.
     /// Options must be stored in options_ field.
     /// Output buffer will be stored in buffer_out_.
     /// Output buffer will be stored in buffer_out_.
+    /// The buffer_out_ is cleared before writting to the buffer.
     ///
     ///
     /// @throw InvalidOperation if packing fails
     /// @throw InvalidOperation if packing fails
     void
     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_("::"),
     remote_addr_("::"),
     local_port_(0),
     local_port_(0),
     remote_port_(0),
     remote_port_(0),
-    bufferOut_(0) {
+    buffer_out_(0) {
     data_.resize(buf_len);
     data_.resize(buf_len);
     memcpy(&data_[0], buf, 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_("::"),
     remote_addr_("::"),
     local_port_(0),
     local_port_(0),
     remote_port_(0),
     remote_port_(0),
-    bufferOut_(0) {
+    buffer_out_(0) {
 }
 }
 
 
 uint16_t Pkt6::len() {
 uint16_t Pkt6::len() {
@@ -199,6 +199,8 @@ Pkt6::pack() {
 void
 void
 Pkt6::packUDP() {
 Pkt6::packUDP() {
     try {
     try {
+        // Make sure that the buffer is empty before we start writting to it.
+        buffer_out_.clear();
 
 
         // is this a relayed packet?
         // is this a relayed packet?
         if (!relay_info_.empty()) {
         if (!relay_info_.empty()) {
@@ -215,11 +217,11 @@ Pkt6::packUDP() {
                  relay != relay_info_.end(); ++relay) {
                  relay != relay_info_.end(); ++relay) {
 
 
                 // build relay-forw/relay-repl header (see RFC3315, section 7)
                 // 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);
                                      isc::asiolink::V6ADDRESS_LEN);
-                bufferOut_.writeData(&relay->peeraddr_.toBytes()[0],
+                buffer_out_.writeData(&relay->peeraddr_.toBytes()[0],
                                      isc::asiolink::V6ADDRESS_LEN);
                                      isc::asiolink::V6ADDRESS_LEN);
 
 
                 // store every option in this relay scope. Usually that will be
                 // store every option in this relay scope. Usually that will be
@@ -230,28 +232,28 @@ Pkt6::packUDP() {
                 for (OptionCollection::const_iterator opt =
                 for (OptionCollection::const_iterator opt =
                          relay->options_.begin();
                          relay->options_.begin();
                      opt != relay->options_.end(); ++opt) {
                      opt != relay->options_.end(); ++opt) {
-                    (opt->second)->pack(bufferOut_);
+                    (opt->second)->pack(buffer_out_);
                 }
                 }
 
 
                 // and include header relay-msg option. Its payload will be
                 // and include header relay-msg option. Its payload will be
                 // generated in the next iteration (if there are more relays)
                 // generated in the next iteration (if there are more relays)
                 // or outside the loop (if there are no more relays and the
                 // or outside the loop (if there are no more relays and the
                 // payload is a direct message)
                 // 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)
         // DHCPv6 header: message-type (1 octect) + transaction id (3 octets)
-        bufferOut_.writeUint8(msg_type_);
+        buffer_out_.writeUint8(msg_type_);
         // store 3-octet transaction-id
         // 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
         // the rest are options
-        LibDHCP::packOptions(bufferOut_, options_);
+        LibDHCP::packOptions(buffer_out_, options_);
     }
     }
     catch (const Exception& e) {
     catch (const Exception& e) {
        // An exception is thrown and message will be written to Logger
        // An exception is thrown and message will be written to Logger
@@ -502,7 +504,7 @@ Pkt6::delOption(uint16_t type) {
 }
 }
 
 
 void Pkt6::repack() {
 void Pkt6::repack() {
-    bufferOut_.writeData(&data_[0], data_.size());
+    buffer_out_.writeData(&data_[0], data_.size());
 }
 }
 
 
 void
 void

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

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

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

@@ -145,6 +145,27 @@ public:
         return (sockets_count);
         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
 // 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
     // 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.
     // still being hold by IfaceMgr which will try to use it to receive data.
     close(socket1);
     close(socket1);
+    close(socket2);
     EXPECT_THROW(ifacemgr->receive6(10), SocketReadError);
     EXPECT_THROW(ifacemgr->receive6(10), SocketReadError);
     EXPECT_THROW(ifacemgr->send(sendPkt), SocketWriteError);
     EXPECT_THROW(ifacemgr->send(sendPkt), SocketWriteError);
 }
 }
@@ -1520,4 +1542,116 @@ TEST_F(IfaceMgrTest, controlSession) {
     close(pipefd[0]);
     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));
                                     typeid(OptionCustom));
 
 
     LibDhcpTest::testStdOptionDefs4(DHO_DHCP_PARAMETER_REQUEST_LIST, begin, end,
     LibDhcpTest::testStdOptionDefs4(DHO_DHCP_PARAMETER_REQUEST_LIST, begin, end,
-                                    typeid(Option));
+                                    typeid(OptionUint8Array));
 
 
     LibDhcpTest::testStdOptionDefs4(DHO_DHCP_MESSAGE, begin, end,
     LibDhcpTest::testStdOptionDefs4(DHO_DHCP_MESSAGE, begin, end,
                                     typeid(OptionString));
                                     typeid(OptionString));

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

@@ -16,6 +16,7 @@
 #include <dhcp/libdhcp++.h>
 #include <dhcp/libdhcp++.h>
 #include <dhcpsrv/cfgmgr.h>
 #include <dhcpsrv/cfgmgr.h>
 #include <dhcpsrv/dhcpsrv_log.h>
 #include <dhcpsrv/dhcpsrv_log.h>
+#include <string>
 
 
 using namespace isc::asiolink;
 using namespace isc::asiolink;
 using namespace isc::util;
 using namespace isc::util;
@@ -269,14 +270,31 @@ std::string CfgMgr::getDataDir() {
 
 
 void
 void
 CfgMgr::addActiveIface(const std::string& iface) {
 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,
         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");
                   " to the set of interfaces on which server listens");
     }
     }
     LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE, DHCPSRV_CFGMGR_ADD_IFACE)
     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
 void
@@ -292,6 +310,8 @@ CfgMgr::deleteActiveIfaces() {
               DHCPSRV_CFGMGR_CLEAR_ACTIVE_IFACES);
               DHCPSRV_CFGMGR_CLEAR_ACTIVE_IFACES);
     active_ifaces_.clear();
     active_ifaces_.clear();
     all_ifaces_active_ = false;
     all_ifaces_active_ = false;
+
+    unicast_addrs_.clear();
 }
 }
 
 
 bool
 bool
@@ -319,6 +339,15 @@ CfgMgr::isIfaceListedActive(const std::string& iface) const {
     return (false);
     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()
 CfgMgr::CfgMgr()
     : datadir_(DHCP_DATA_DIR),
     : datadir_(DHCP_DATA_DIR),
       all_ifaces_active_(false) {
       all_ifaces_active_(false) {

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

@@ -305,6 +305,17 @@ public:
     /// interfaces on which server is configured to listen.
     /// interfaces on which server is configured to listen.
     bool isActiveIface(const std::string& iface) const;
     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:
 protected:
 
 
     /// @brief Protected constructor.
     /// @brief Protected constructor.
@@ -372,6 +383,12 @@ private:
     std::list<std::string> active_ifaces_;
     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
     /// A flag which indicates that server should listen on all available
     /// interfaces.
     /// interfaces.
     bool all_ifaces_active_;
     bool all_ifaces_active_;

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

@@ -231,6 +231,7 @@ InterfaceListConfigParser::commit() {
 
 
 bool
 bool
 InterfaceListConfigParser::isIfaceAdded(const std::string& iface) const {
 InterfaceListConfigParser::isIfaceAdded(const std::string& iface) const {
+
     for (IfaceListStorage::const_iterator it = interfaces_.begin();
     for (IfaceListStorage::const_iterator it = interfaces_.begin();
          it != interfaces_.end(); ++it) {
          it != interfaces_.end(); ++it) {
         if (iface == *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>& t1,
                  const Triplet<uint32_t>& t2,
                  const Triplet<uint32_t>& t2,
                  const Triplet<uint32_t>& valid_lifetime)
                  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()) {
     if (!prefix.isV4()) {
         isc_throw(BadValue, "Non IPv4 prefix " << prefix.toText()
         isc_throw(BadValue, "Non IPv4 prefix " << prefix.toText()
                   << " specified in subnet4");
                   << " 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 {
 const PoolCollection& Subnet::getPools(Lease::Type type) const {
     // check if the type is valid (and throw if it isn't)
     // check if the type is valid (and throw if it isn't)
     checkType(type);
     checkType(type);

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

@@ -505,6 +505,18 @@ public:
             const Triplet<uint32_t>& t2,
             const Triplet<uint32_t>& t2,
             const Triplet<uint32_t>& valid_lifetime);
             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:
 protected:
 
 
     /// @brief Check if option is valid and can be added to a subnet.
     /// @brief Check if option is valid and can be added to a subnet.
@@ -527,6 +539,9 @@ protected:
     /// @param type type to be checked
     /// @param type type to be checked
     /// @throw BadValue if invalid value is used
     /// @throw BadValue if invalid value is used
     virtual void checkType(Lease::Type type) const;
     virtual void checkType(Lease::Type type) const;
+
+    /// @brief siaddr value for this subnet
+    isc::asiolink::IOAddress siaddr_;
 };
 };
 
 
 /// @brief A pointer to a Subnet4 object
 /// @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) {
 TEST_F(CfgMgrTest, addActiveIface) {
     CfgMgr& cfg_mgr = CfgMgr::instance();
     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("eth0"));
     EXPECT_TRUE(cfg_mgr.isActiveIface("eth1"));
     EXPECT_TRUE(cfg_mgr.isActiveIface("eth1"));
     EXPECT_FALSE(cfg_mgr.isActiveIface("eth2"));
     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("eth0"));
     EXPECT_FALSE(cfg_mgr.isActiveIface("eth1"));
     EXPECT_FALSE(cfg_mgr.isActiveIface("eth1"));
     EXPECT_FALSE(cfg_mgr.isActiveIface("eth2"));
     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
 // This test verifies that it is possible to set the flag which configures the
 // server to listen on all interfaces.
 // server to listen on all interfaces.
 TEST_F(CfgMgrTest, activateAllIfaces) {
 TEST_F(CfgMgrTest, activateAllIfaces) {
@@ -618,6 +653,27 @@ TEST_F(CfgMgrTest, activateAllIfaces) {
     EXPECT_FALSE(cfg_mgr.isActiveIface("eth2"));
     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
 // No specific tests for getSubnet6. That method (2 overloaded versions) is tested
 // in Dhcpv6SrvTest.selectSubnetAddr and Dhcpv6SrvTest.selectSubnetIface
 // in Dhcpv6SrvTest.selectSubnetAddr and Dhcpv6SrvTest.selectSubnetIface
 // (see src/bin/dhcp6/tests/dhcp6_srv_unittest.cc)
 // (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")));
     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) {
 TEST(Subnet4Test, Pool4InSubnet4) {
 
 
     Subnet4Ptr subnet(new Subnet4(IOAddress("192.1.2.0"), 24, 1, 2, 3));
     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_,
                                options_,
                                getTransidOffset(),
                                getTransidOffset(),
                                getTransid(),
                                getTransid(),
-                               bufferOut_));
+                               buffer_out_));
 }
 }
 
 
 bool
 bool