Browse Source

[3232] Merge branch 'master' into trac3232

Conflicts:
	src/bin/dhcp6/dhcp6_messages.mes
	src/bin/dhcp6/dhcp6_srv.cc
	src/bin/dhcp6/dhcp6_srv.h
	src/bin/dhcp6/tests/dhcp6_test_utils.cc
Marcin Siodelski 11 years ago
parent
commit
58de1eed51
100 changed files with 6604 additions and 1580 deletions
  1. 158 32
      ChangeLog
  2. 5 5
      README
  3. 64 16
      configure.ac
  4. 212 30
      doc/guide/bind10-guide.xml
  5. 4 1
      m4macros/ax_python_sqlite3.m4
  6. 7 3
      src/Makefile.am
  7. 30 8
      src/bin/Makefile.am
  8. 4 4
      src/bin/auth/auth_srv.cc
  9. 3 4
      src/bin/auth/tests/Makefile.am
  10. 12 13
      src/bin/auth/tests/auth_srv_unittest.cc
  11. 7 7
      src/bin/bind10/init_messages.mes
  12. 9 2
      src/bin/cfgmgr/plugins/Makefile.am
  13. 9 1
      src/bin/cfgmgr/plugins/tests/Makefile.am
  14. 13 1
      src/bin/d2/d2_cfg_mgr.cc
  15. 21 1
      src/bin/d2/d2_cfg_mgr.h
  16. 4 3
      src/bin/d2/d2_config.cc
  17. 176 158
      src/bin/d2/d2_messages.mes
  18. 43 16
      src/bin/d2/d2_update_mgr.cc
  19. 15 7
      src/bin/d2/d2_update_mgr.h
  20. 1 1
      src/bin/d2/dns_client.cc
  21. 7 7
      src/bin/d2/nc_add.cc
  22. 23 18
      src/bin/d2/nc_remove.cc
  23. 64 4
      src/bin/d2/nc_trans.cc
  24. 21 1
      src/bin/d2/nc_trans.h
  25. 20 3
      src/bin/d2/tests/d2_cfg_mgr_unittests.cc
  26. 103 0
      src/bin/d2/tests/d2_update_mgr_unittests.cc
  27. 4 3
      src/bin/d2/tests/nc_add_unittests.cc
  28. 11 10
      src/bin/d2/tests/nc_remove_unittests.cc
  29. 81 0
      src/bin/d2/tests/nc_trans_unittests.cc
  30. 16 2
      src/bin/dhcp4/ctrl_dhcp4_srv.cc
  31. 62 57
      src/bin/dhcp4/dhcp4_messages.mes
  32. 73 42
      src/bin/dhcp4/dhcp4_srv.cc
  33. 40 23
      src/bin/dhcp4/dhcp4_srv.h
  34. 7 4
      src/bin/dhcp4/tests/Makefile.am
  35. 379 0
      src/bin/dhcp4/tests/d2_unittest.cc
  36. 117 0
      src/bin/dhcp4/tests/d2_unittest.h
  37. 143 19
      src/bin/dhcp4/tests/dhcp4_srv_unittest.cc
  38. 1 1
      src/bin/dhcp4/tests/dhcp4_test_utils.h
  39. 76 75
      src/bin/dhcp4/tests/fqdn_unittest.cc
  40. 2 0
      src/bin/dhcp6/config_parser.cc
  41. 14 0
      src/bin/dhcp6/ctrl_dhcp6_srv.cc
  42. 88 1
      src/bin/dhcp6/dhcp6.spec
  43. 76 65
      src/bin/dhcp6/dhcp6_messages.mes
  44. 84 163
      src/bin/dhcp6/dhcp6_srv.cc
  45. 37 11
      src/bin/dhcp6/dhcp6_srv.h
  46. 8 4
      src/bin/dhcp6/tests/Makefile.am
  47. 109 0
      src/bin/dhcp6/tests/config_parser_unittest.cc
  48. 383 0
      src/bin/dhcp6/tests/d2_unittest.cc
  49. 117 0
      src/bin/dhcp6/tests/d2_unittest.h
  50. 213 52
      src/bin/dhcp6/tests/dhcp6_srv_unittest.cc
  51. 40 0
      src/bin/dhcp6/tests/dhcp6_test_utils.cc
  52. 12 20
      src/bin/dhcp6/tests/dhcp6_test_utils.h
  53. 194 132
      src/bin/dhcp6/tests/fqdn_unittest.cc
  54. 3 0
      src/bin/dhcp6/tests/hooks_unittest.cc
  55. 25 1
      src/bin/dhcp6/tests/wireshark.cc
  56. 6 6
      src/bin/msgq/msgq_messages.mes
  57. 2 2
      src/bin/resolver/resolver.cc
  58. 4 2
      src/hooks/dhcp/user_chk/Makefile.am
  59. 31 6
      src/lib/Makefile.am
  60. 0 1
      src/lib/cache/Makefile.am
  61. 20 3
      src/lib/cc/data.cc
  62. 41 14
      src/lib/cc/tests/data_unittests.cc
  63. 5 5
      src/lib/datasrc/datasrc_messages.mes
  64. 4 0
      src/lib/dhcp/Makefile.am
  65. 123 0
      src/lib/dhcp/opaque_data_tuple.cc
  66. 319 0
      src/lib/dhcp/opaque_data_tuple.h
  67. 26 3
      src/lib/dhcp/option_definition.cc
  68. 11 1
      src/lib/dhcp/option_definition.h
  69. 191 0
      src/lib/dhcp/option_vendor_class.cc
  70. 197 0
      src/lib/dhcp/option_vendor_class.h
  71. 9 5
      src/lib/dhcp/std_option_defs.h
  72. 4 0
      src/lib/dhcp/tests/Makefile.am
  73. 5 0
      src/lib/dhcp/tests/iface_mgr_test_config.cc
  74. 2 0
      src/lib/dhcp/tests/iface_mgr_test_config.h
  75. 28 16
      src/lib/dhcp/tests/libdhcp++_unittest.cc
  76. 482 0
      src/lib/dhcp/tests/opaque_data_tuple_unittest.cc
  77. 456 0
      src/lib/dhcp/tests/option_vendor_class_unittest.cc
  78. 49 0
      src/lib/dhcp/tests/pkt_filter6_test_stub.cc
  79. 99 0
      src/lib/dhcp/tests/pkt_filter6_test_stub.h
  80. 6 0
      src/lib/dhcp_ddns/dhcp_ddns_messages.mes
  81. 43 5
      src/lib/dhcp_ddns/ncr_io.cc
  82. 35 2
      src/lib/dhcp_ddns/ncr_io.h
  83. 10 0
      src/lib/dhcp_ddns/ncr_udp.cc
  84. 5 0
      src/lib/dhcp_ddns/ncr_udp.h
  85. 82 18
      src/lib/dhcp_ddns/tests/ncr_udp_unittests.cc
  86. 6 1
      src/lib/dhcp_ddns/tests/watch_socket_unittests.cc
  87. 2 1
      src/lib/dhcpsrv/Makefile.am
  88. 23 19
      src/lib/dhcpsrv/cfgmgr.cc
  89. 21 3
      src/lib/dhcpsrv/cfgmgr.h
  90. 146 0
      src/lib/dhcpsrv/d2_client_cfg.cc
  91. 226 0
      src/lib/dhcpsrv/d2_client_cfg.h
  92. 95 165
      src/lib/dhcpsrv/d2_client.cc
  93. 67 206
      src/lib/dhcpsrv/d2_client.h
  94. 1 1
      src/lib/dhcpsrv/dhcp_parsers.h
  95. 57 25
      src/lib/dhcpsrv/dhcpsrv_messages.mes
  96. 7 4
      src/lib/dhcpsrv/tests/Makefile.am
  97. 82 9
      src/lib/dhcpsrv/tests/cfgmgr_unittest.cc
  98. 7 1
      src/lib/dhcpsrv/tests/d2_client_unittest.cc
  99. 119 25
      src/lib/dhcpsrv/tests/d2_udp_unittest.cc
  100. 0 0
      src/lib/dhcpsrv/tests/generic_lease_mgr_unittest.cc

+ 158 - 32
ChangeLog

@@ -1,3 +1,127 @@
+769.	[bug]		tmark
+	b10-dhcp-ddns now treats a DNS server response code of NXRRSET as a
+	successful outcome when processing a request to remove DNS data.  This
+	corrects a defect in which b10-dhcp-ddns would incorrectly fail a request
+	to remove DNS data when the DNS server's response was NXRRSET.
+	(Trac #3362, git da3b0d4f364d069ffdb47723545798ac589fae42)
+
+768.	[bug]		tmark
+	Python configuration library now properly merges changes into
+	configuration items that are maps of items.  This corrects a
+	defect in which a change to an item in a map of items could
+	committed, only to be lost upon committing a subsequent change
+	to same map of items during the same bindctl session.
+	(Trac #3373, git da3b0d4f364d069ffdb47723545798ac589fae42)
+
+767.	[func]		tomek
+	Unit-tests for all database backends are now shared. This improves
+	test coverage for memfile and any future backends that may appear.
+	(Trac #3359, git 3d6c11630ada9d0681a813cf026f6bb16aabb9fa)
+
+bind10-1.2.0beta1 released on March 6, 2014
+
+766.	[func]		muks
+	--disable-dns and --disable-dhcp configure arguments have been
+	added to conditionally disable the DNS or DHCP components
+	respectively. This facility can be used to do a DNS or DHCP-only
+	build of BIND 10. DNS and DHCP components are both enabled by
+	default.
+	(Trac #2367, git 81a689b61b1c4abf8a1a4fcbe41cfc96fd11792a)
+
+765.	[bug]		tomek
+	b10-dhcp4: Fixed a minor bug in eRouter1.0 class processing. The
+	server no longer sets giaddr field.
+	(Trac #3353, git 23c22e9b1141c699f361d45c309e737dfecf6f3f)
+
+764.	[bug]		tomek
+	b10-dhcp4: Fixed a bug caused client classification to not work
+	properly.
+	(Trac #3343, git 1801400ac874380e7a565d373b4bae96a49e21f7)
+
+763.	[func]		tmark
+	b10-dhcp-ddns may now be configured to disable DNS updates in
+	in a given direction by simply not defining any domains for that
+	direction in its configuration.  This allows it to be configured to
+	support either forward DNS or reverse DNS only.  Prior to this if
+	a request was received that could not be matched to servers in a
+	given direction it was failed immediately.
+	(Trac #3341, git 01f26bce1d9faaddb8be59802f73891ea065b200)
+
+762.	[func]		tmark
+	If configured to do so, b10-dhcp6 will now create DHCP-DDNS update
+	requests and send them to b10-dhcp-ddns for processing.
+	(Trac# 3329, git 239956696465a13196a2b6bc0f3a61aed21a5de8)
+
+761.	[doc]		stephen, jreed
+	Added "man" page for perfdhcp.
+	(Trac #2307, git ff2f538912c205fbdb1408ee613c09b90de53514)
+
+760.	[bug]		tmark
+	When merging a map of configuration elements into another, elements
+	that are themselves maps will be merged. In particular, this
+	corrects a defect which caused a configuration commit error to
+	occur when using bindctl to modify a single a parameter in
+	dhcp-ddns portion of b10-dhcp4 configuration.
+	(Trac# 3339, git 3ae0d93d89f3277a566eeb045191a43b2dd9d9b1)
+
+759.	[func]		tomek
+	b10-dhcp4, b10-dhcp6: IP address of the relay agent can now be
+	specified for both IPv4 and IPv6 subnets. That information allows
+	the server to properly handle a case where relay agent address
+	does not match subnet.  This is mostly useful in shared subnets
+	and cable networks.
+	(Trac #3322, git 5de565baea42c9096dff78ed5fbd05982a174469)
+
+758.	[bug]		tmark
+	b10-dhcp4 now correctly handles DHO_HOST_OPTION.  This corrects
+	a bug where the server would fail to recognize the option in the
+	DHCP request and then skip generating the appropriate DHCP-DDNS
+	update request.
+	(Trac #2426, git 985d66cba7665a71e17ef70c5d22c767abaad1b6)
+
+757.	[func]		tmark
+	b10-dhcp6 now parses parameters which support DHCP-DDNS updates
+	via the DHCP-DDNS module, b10-dhcp-ddns.  These parameters are
+	part of new configuration element, dhcp-ddns, defined in
+	dhcp4.spec. These parameters influence when and how DDNS updates
+	requests are created but communicating them to b10-dhcp-ddns is
+	not yet supported.  That will be provided under separate ticket,
+	Trac #3222.
+	(Trac# 3034, git 22c667a66536ff3e3741bc67025d824644ed4e7d)
+
+756.	[bug]		marcin
+	b10-dhcp6: server parses DHCPv6 Vendor Class option. Previously
+	the server failed to parse Vendor Class option having empty opaque
+	data field because of the invalid definition in libdhcp++. The
+	DHCPv6 Vendor Class option and DHCPv4 V-I Vendor Class option is
+	now represented by the new OptionVendorClass. The b10-dhcp4 is
+	affected by this change such that it uses new class to parse the
+	DHCPv4 V-I Vendor Class option.
+	(Trac #3316, git 1e61d7db5b8dc76682aa568cd62bfae0eeff46e3)
+
+755.	[func]		muks
+	Add support for the CAA RR type (RFC 6844).
+	(Trac #2512, git 39162608985e5c904448f308951c73bb9c32da8f)
+
+754.	[func]		muks
+	Add support for the TLSA RR type (RFC 6698).
+	(Trac #2185, git a168170430f6927f28597b2a6debebe31cf39b13)
+
+753.	[func]		muks
+	libdns++: the unknown/generic (RFC 3597) RDATA class now uses the
+	generic lexer in constructors from text.
+	(Trac #2426, git 0770d2df84e5608371db3a47e0456eb2a340b5f4)
+
+752.	[func]		tmark
+	If configured to do so, b10-dhcp4 will now create DHCP-DDNS update
+	requests and send them to b10-dhcp-ddns for processing.
+	(Trac# 3329, git 4546dd186782eec5cfcb4ddb61b0a3aa5c700751)
+
+751.	[func]		muks
+	The BIND 10 zone loader now supports the $GENERATE directive (a
+	BIND 9 extension).
+	(Trac #2430, git b05064f681231fe7f8571253c5786f4ff0f2ca03)
+
 750.	[func]		tomek
 	b10-dhcp4, b10-dhcp6: Simple client classification has been
 	implemented. Incoming packets can be assigned to zero or more
@@ -6,9 +130,10 @@
 	(Trac #3274, git 1791d19899b92a6ee411199f664bdfc690ec08b2)
 
 749.	[bug]		tmark
-	b10-dhcp-ddns now sets the TTL value in RRs that add A, AAAA, or PTR DNS
-	entries to the lease length provided in instigating NameChangeRequest.
-	This corrected a bug in which the TTL was always set to 0.
+	b10-dhcp-ddns now sets the TTL value in RRs that add A, AAAA, or
+	PTR DNS entries to the lease length provided in instigating
+	NameChangeRequest.  This corrected a bug in which the TTL was
+	always set to 0.
 	(Trac# 3299, git dbacf27ece77f3d857da793341c6bd31ef1ea239)
 
 748.	[bug]		marcin
@@ -32,7 +157,7 @@
 	method has been removed and replaced with several convenience methods.
 	(Trac #1485, git ecdb62db16b3f3d447db4a9d2a4079d5260431f0)
 
-745.	[bug]		muks
+745.	[bug]*		muks
 	b10-auth now returns rcode=REFUSED for all questions with
 	qtype=RRSIG (i.e., where RRSIGs are queried directly). This is
 	because RRSIGs are meaningless without being bundled alongside the
@@ -40,12 +165,12 @@
 	(Trac #2226, git 68d24e65c9c3dfee38adfbe1c93367b0083f9a58)
 
 744.	[func]		marcin
-	b10-dhcp6: Refactored the code which is processing Client FQDN option.
-	The major user-visible change is that server generates DDNS
-	NameChangeRequest for the first IPv6 address (instead of all)
-	acquired by a client. Also, the server generates fully qualified domain
-	name from acquired IPv6 address, if the client sends an empty name in
-	Client FQDN option.
+	b10-dhcp6: Refactored the code which is processing Client FQDN
+	option.  The major user-visible change is that server generates
+	DDNS NameChangeRequest for the first IPv6 address (instead of all)
+	acquired by a client. Also, the server generates fully qualified
+	domain name from acquired IPv6 address, if the client sends an
+	empty name in Client FQDN option.
 	(Trac# 3295, git aa1c94a54114e848c64771fde308fc9ac0c00fd0)
 
 743.	[func]		tmark
@@ -92,7 +217,7 @@
 	DNS messages with unsupported opcodes are received.
 	(Trac #1516, git 71611831f6d1aaaea09143d4837eddbd1d67fbf4)
 
-736.    [bug]           wlodek
+736.	[bug]		wlodek
 	b10-dhcp6 is now capable to determine if a received
 	message is addressed to it, using server identifier option.
 	The messages with non-matching server identifier are dropped.
@@ -116,20 +241,21 @@
 
 732.	[func]		tomek
 	b10-dhcp4, b10-dhcp6: Support for simplified client classification
-        added. Incoming packets are now assigned to a client class based on
-	the content of the packet's user class option (DHCPv4) or vendor class
-	option (DHCPv6). Two classes (docsis3.0 and eRouter1.0) have class
-	specific behavior in b10-dhcp4. See DHCPv4 Client Classification and
-	DHCPv6 Client Classification in BIND10 Developer's Guide for details.
-	This is a first ticket in a series of planned at least three tickets.
+	added. Incoming packets are now assigned to a client class based
+	on the content of the packet's user class option (DHCPv4) or vendor
+	class option (DHCPv6). Two classes (docsis3.0 and eRouter1.0) have
+	class specific behavior in b10-dhcp4. See DHCPv4 Client
+	Classification and DHCPv6 Client Classification in BIND10
+	Developer's Guide for details.  This is a first ticket in a series
+	of planned at least three tickets.
 	(Trac #3203, git afea612c23143f81a4201e39ba793bc837c5c9f1)
 
 731.	[func]		tmark
-	b10-dhcp4 now parses parameters which support DHCP-DDNS updates via
-	the DHCP-DDNS module, b10-dhcp-ddns.  These parameters are part of new
-	configuration element, dhcp-ddns, defined in dhcp4.spec. The parameters
-	parse, store and retrieve but do not yet govern behavior.  That will be
-	provided under separate ticket.
+	b10-dhcp4 now parses parameters which support DHCP-DDNS updates
+	via the DHCP-DDNS module, b10-dhcp-ddns.  These parameters are
+	part of new configuration element, dhcp-ddns, defined in
+	dhcp4.spec.  The parameters parse, store and retrieve but do not
+	yet govern behavior.  That will be provided under separate ticket.
 	(Trac# 3033, git 0ba859834503f2b9b908cd7bc572e0286ca9201f)
 
 730.	[bug]		tomek
@@ -154,7 +280,7 @@
 	RRset::setName() has now been removed.
 	(Trac #2335, git c918027a387da8514acf7e125fd52c8378113662)
 
-726.	[bug]		muks
+726.	[bug]*		muks
 	Don't print trailing newlines in Question::toText() output by
 	default.  This fixes some logging that were split with a line
 	feed.  It is possible to get the old behavior by passing
@@ -310,7 +436,7 @@
 	them to b10-dhcp-ddns will be implemented with the future tickets.
 	(Trac #3035, git f617e6af8cdf068320d14626ecbe14a73a6da22)
 
-705.	[bug]		kean
+705.	[bug]*		kean
 	When commands are piped into bindctl, no longer attempt to query the
 	user name and password if no default user name and password file is
 	present, or it contains no valid entries.
@@ -502,12 +628,11 @@
 	(Trac #3145, git 3a844e85ecc3067ccd1c01841f4a61366cb278f4)
 
 672.	[func]		tmark
-	Added b10-dhcp-ddnsupdate transaction base class,
-	NameChangeTransaction.  This class provides the common
-	structure and methods to implement the state models described
-	in the DHCP_DDNS design, plus integration with DNSClient
-	and its callback mechanism for asynchronous IO with the
-	DNS servers.
+	Added b10-dhcp-ddns transaction base class, NameChangeTransaction.
+	This class provides the common structure and methods to implement
+	the state models described in the DHCP_DDNS design, plus
+	integration with DNSClient and its callback mechanism for
+	asynchronous IO with the DNS servers.
 	(Trac #3086, git 079b862c9eb21056fdf957e560b8fe7b218441b6)
 
 671.	[func]		dclink, tomek
@@ -676,7 +801,7 @@
 
 645.	[func]		tomek
 	Added initial set of hooks (pkt4_receive, subnet4_select,
-	lease4_select, pkt4_send) to the DHCPv6 server.
+	lease4_select, pkt4_send) to the DHCPv4 server.
 	(Trac #2994, git be65cfba939a6a7abd3c93931ce35c33d3e8247b)
 
 644.	[func]		marcin
@@ -2315,7 +2440,8 @@ bind10-devel-20120517 released on May 17, 2012
 	now manipulates them in the separate table for the NSEC3 namespace.
 	As a result b10-xfrin now correctly updates NSEC3-signed zones by
 	inbound zone transfers.
-	(Trac #1781, #1788, #1891, git 672f129700dae33b701bb02069cf276238d66be3)
+	(Trac #1781, #1788, #1891,
+	git 672f129700dae33b701bb02069cf276238d66be3)
 
 426.	[bug]		vorner
 	The NSEC3 records are now included when transferring a

+ 5 - 5
README

@@ -19,11 +19,11 @@ tests and example programs. (It also includes an experimental proof
 of concept recursive or forwarding DNS server, b10-resolver.)
 
 BIND 10 also provides experimental DHCPv4 and DHCPv6 servers,
-b10-dhcp4 and b10-dhcp6, a portable DHCP library, libdhcp++, and
-a DHCP benchmarking tool, perfdhcp.  In this release of BIND 10,
-the DHCPv4 and DHCPv6 servers must be considered experimental.
-Limitations and known issues with this DHCP release can be found
-at http://bind10.isc.org/wiki/KeaKnownIssues
+b10-dhcp4 and b10-dhcp6, a dynamic DNS update module, b10-dhcp-ddns,
+a portable DHCP library, libdhcp++, and a DHCP benchmarking tool,
+perfdhcp.  In this release of BIND 10, the DHCPv4 and DHCPv6 servers
+must be considered experimental.  Limitations and known issues with
+this DHCP release can be found at http://bind10.isc.org/wiki/KeaKnownIssues
 
 NOTE: The API/ABI provided by libraries in BIND 10 may change in future
 point releases. So please do not assume currently that any code that you

+ 64 - 16
configure.ac

@@ -2,7 +2,7 @@
 # Process this file with autoconf to produce a configure script.
 
 AC_PREREQ([2.59])
-AC_INIT(bind10, 20130529, bind10-dev@isc.org)
+AC_INIT(bind10, 20140306, bind10-dev@isc.org)
 AC_CONFIG_SRCDIR(README)
 
 # serial-tests is not available in automake version before 1.13, so
@@ -24,6 +24,55 @@ AC_CONFIG_MACRO_DIR([m4macros])
 # Checks for programs.
 AC_PROG_CXX
 
+want_dns=yes
+AC_ARG_ENABLE(dns,
+  [AC_HELP_STRING([--disable-dns],
+  [disable DNS components])],
+  [want_dns=$enableval])
+AM_CONDITIONAL([WANT_DNS], [test "$want_dns" = "yes"])
+if test "$want_dns" = "yes"; then
+   WANT_DNS=yes
+else
+   WANT_DNS=no
+fi
+AC_SUBST(WANT_DNS)
+
+want_dhcp=yes
+AC_ARG_ENABLE(dhcp,
+  [AC_HELP_STRING([--disable-dhcp],
+  [disable DHCP components])],
+  [want_dhcp=$enableval])
+AM_CONDITIONAL([WANT_DHCP], [test "$want_dhcp" = "yes"])
+if test "$want_dhcp" = "yes"; then
+   WANT_DHCP=yes
+else
+   WANT_DHCP=no
+fi
+AC_SUBST(WANT_DHCP)
+
+want_experimental_resolver=no
+AC_ARG_ENABLE(experimental-resolver,
+  [AC_HELP_STRING([--enable-experimental-resolver],
+  [enable the experimental resolver [default=no]])],
+  [want_experimental_resolver=$enableval])
+AM_CONDITIONAL([WANT_EXPERIMENTAL_RESOLVER], [test "$want_experimental_resolver" = "yes"])
+if test "$want_experimental_resolver" = "yes"; then
+   WANT_EXPERIMENTAL_RESOLVER=yes
+else
+   WANT_EXPERIMENTAL_RESOLVER=no
+fi
+AC_SUBST(WANT_EXPERIMENTAL_RESOLVER)
+
+# At least DNS or DHCP components must be enabled
+if test "$want_dns" != "yes" -a "$want_dhcp" != "yes"; then
+    AC_MSG_ERROR([At least one of DNS or DHCP components must be enabled to do a BIND 10 build.])
+fi
+
+# Experimental resolver requires DNS components to be enabled
+if test "$want_experimental_resolver" = "yes" -a "$want_dns" != "yes"; then
+    AC_MSG_ERROR([You must also enable DNS components if you want to enable the experimental resolver.])
+fi
+
 # Enable low-performing debugging facilities? This option optionally
 # enables some debugging aids that perform slowly and hence aren't built
 # by default.
@@ -875,6 +924,10 @@ elif test "${mysql_config}" != "no" ; then
 fi
 
 if test "$MYSQL_CONFIG" != "" ; then
+    if test "$want_dhcp" != "yes"; then
+        AC_MSG_ERROR([--with-dhcp-mysql should not be used when DHCP components are disabled])
+    fi
+
     if test -d "$MYSQL_CONFIG" -o ! -x "$MYSQL_CONFIG" ; then
         AC_MSG_ERROR([--with-dhcp-mysql should point to a mysql_config program])
     fi
@@ -910,6 +963,9 @@ if test "$MYSQL_CONFIG" != "" ; then
     AC_DEFINE([HAVE_MYSQL], [1], [MySQL is present])
 fi
 
+# Solaris puts FIONREAD in filio.h
+AC_CHECK_HEADER(sys/filio.h)
+
 # ... and at the shell level, so Makefile.am can take action depending on this.
 AM_CONDITIONAL(HAVE_MYSQL, test "$MYSQL_CONFIG" != "")
 
@@ -998,25 +1054,12 @@ if test "$BOOST_NUMERIC_CAST_WOULDFAIL" = "yes" -a X"$werror_ok" = X1 -a $CLANGP
     AC_MSG_ERROR([Failed to compile a required header file.  If you are using FreeBSD and Boost installed via ports, retry with specifying --without-werror.  See the ChangeLog entry for Trac no. 1991 for more details.])
 fi
 
-build_experimental_resolver=no
-AC_ARG_ENABLE(experimental-resolver,
-  [AC_HELP_STRING([--enable-experimental-resolver],
-  [enable building of the experimental resolver [default=no]])],
-  [build_experimental_resolver=$enableval])
-AM_CONDITIONAL([BUILD_EXPERIMENTAL_RESOLVER], [test "$build_experimental_resolver" = "yes"])
-if test "$build_experimental_resolver" = "yes"; then
-   BUILD_EXPERIMENTAL_RESOLVER=yes
-else
-   BUILD_EXPERIMENTAL_RESOLVER=no
-fi
-AC_SUBST(BUILD_EXPERIMENTAL_RESOLVER)
-
 use_shared_memory=yes
 AC_ARG_WITH(shared-memory,
     AC_HELP_STRING([--with-shared-memory],
     [Build with Boost shared memory support; for large scale authoritative DNS servers]),
     [use_shared_memory=$withval])
-if test X$use_shared_memory = Xyes -a "$BOOST_MAPPED_FILE_WOULDFAIL" = "yes"; then
+if test X$use_shared_memory = Xyes -a "$BOOST_MAPPED_FILE_WOULDFAIL" = "yes" -a "$want_dns" = "yes"; then
     AC_MSG_ERROR([Boost shared memory does not compile on this system.  If you don't need it (most normal users won't) build without it by rerunning this script with --without-shared-memory; using a different compiler or a different version of Boost may also help.])
 fi
 AM_CONDITIONAL([USE_SHARED_MEMORY], [test x$use_shared_memory = xyes])
@@ -1025,7 +1068,7 @@ if test "x$use_shared_memory" = "xyes"; then
 fi
 AC_SUBST(BOOST_MAPPED_FILE_CXXFLAG)
 
-if test "$BOOST_OFFSET_PTR_OLD" = "yes" -a "$use_shared_memory" = "yes" ; then
+if test "$BOOST_OFFSET_PTR_OLD" = "yes" -a "$use_shared_memory" = "yes" -a "$want_dns" = "yes"; then
     AC_MSG_ERROR([You're trying to compile against boost older than 1.48 with
 shared memory. Older versions of boost have a bug which causes segfaults in
 offset_ptr implementation when compiled by GCC with optimisations enabled.
@@ -1716,6 +1759,11 @@ fi
 
 cat >> config.report << END
 
+Components:
+  DHCP: $want_dhcp
+  DNS: $want_dns
+  Experimental resolver: $want_experimental_resolver
+
 Features:
   $enable_features
 

+ 212 - 30
doc/guide/bind10-guide.xml

@@ -4431,15 +4431,16 @@ Dhcp4/subnet4	[]	list	(default)
         extensions will be using hooks extensions.
       </para>
       </note>
-      <para>In certain cases it is useful to differentiate between different types
-      of clients and treat them differently. The process of doing classification
-      is conducted in two steps. The first step is to assess incoming packet and
-      assign it to zero or more classes. This classification is currently simple,
-      but is expected to grow in capability soon. Currently the server checks whether
-      incoming packet has vendor class identifier option (60). If it has, content
-      of that option is interpreted as a class. For example, modern cable modems
-      will send this option with value &quot;docsis3.0&quot; and as a result the
-      packet will belong to class &quot;docsis3.0&quot;.
+      <para>In certain cases it is useful to differentiate between different
+      types of clients and treat them differently. The process of doing
+      classification is conducted in two steps. The first step is to assess
+      incoming packet and assign it to zero or more classes. This classification
+      is currently simple, but is expected to grow in capability soon. Currently
+      the server checks whether incoming packet has vendor class identifier
+      option (60). If it has, content of that option is prepended with
+      &quot;VENDOR_CLASS_&quot; then is interpreted as a class. For example,
+      modern cable modems will send this option with value &quot;docsis3.0&quot;
+      and as a result the packet will belong to class &quot;VENDOR_CLASS_docsis3.0&quot;.
       </para>
 
       <para>It is envisaged that the client classification will be used for changing
@@ -4450,12 +4451,12 @@ Dhcp4/subnet4	[]	list	(default)
       subnet selection.</para>
 
       <para>
-        For clients that belong to the docsis3.0 class, the siaddr field is set to
-        the value of next-server (if specified in a subnet). If there is
-        boot-file-name option specified, its value is also set in the file field
-        in the DHCPv4 packet. For eRouter1.0 class, the siaddr is always set to
-        0.0.0.0. That capability is expected to be moved to external hook
-        library that will be dedicated to cable modems.
+        For clients that belong to the VENDOR_CLASS_docsis3.0 class, the siaddr
+        field is set to the value of next-server (if specified in a subnet). If
+        there is boot-file-name option specified, its value is also set in the
+        file field in the DHCPv4 packet. For eRouter1.0 class, the siaddr is
+        always set to 0.0.0.0. That capability is expected to be moved to
+        external hook library that will be dedicated to cable modems.
       </para>
 
       <para>
@@ -4483,13 +4484,13 @@ Dhcp4/subnet4	[]	list	(default)
         the 192.0.2.0/24 prefix. The Administrator of that network has decided
         that addresses from range 192.0.2.10 to 192.0.2.20 are going to be
         managed by the Dhcp4 server. Only clients belonging to client class
-        docsis3.0 are allowed to use this subnet. Such a configuration can be
-        achieved in the following way:
+        VENDOR_CLASS_docsis3.0 are allowed to use this subnet. Such a
+        configuration can be achieved in the following way:
         <screen>
 &gt; <userinput>config add Dhcp4/subnet4</userinput>
 &gt; <userinput>config set Dhcp4/subnet4[0]/subnet "192.0.2.0/24"</userinput>
 &gt; <userinput>config set Dhcp4/subnet4[0]/pool [ "192.0.2.10 - 192.0.2.20" ]</userinput>
-&gt; <userinput>config set Dhcp4/subnet4[0]/client-class "docsis3.0"</userinput>
+&gt; <userinput>config set Dhcp4/subnet4[0]/client-class "VENDOR_CLASS_docsis3.0"</userinput>
 &gt; <userinput>config commit</userinput></screen>
       </para>
 
@@ -4592,6 +4593,11 @@ Dhcp4/subnet4	[]	list	(default)
         If the message is relayed it is accepted through any interface. The giaddr
         set by the relay agent is used to select the subnet for the client.
       </para>
+      <para>
+	It is also possible to specify a relay IPv4 address for a given subnet. It
+	can be used to match incoming packets into a subnet in uncommon configurations,
+	e.g. shared subnets. See <xref linkend="dhcp4-relay-override"/> for details.
+      </para>
       <note>
         <para>The subnet selection mechanism described in this section is based
         on the assumption that client classification is not used. The classification
@@ -4600,6 +4606,75 @@ Dhcp4/subnet4	[]	list	(default)
       </note>
     </section>
 
+    <section id="dhcp4-relay-override">
+      <title>Using specific relay agent for a subnet</title>
+      <para>
+        The relay has to have an interface connected to the link on which
+        the clients are being configured. Typically the relay has an IPv4
+        address configured on that interface that belongs to the subnet that
+        the server will assign addresses from. In such typical case, the
+        server is able to use IPv4 address inserted by the relay (in GIADDR
+        field of the DHCPv4 packet) to select appropriate subnet.
+      </para>
+      <para>
+        However, that is not always the case. In certain uncommon, but
+        valid deployments, the relay address may not match the subnet. This
+        usually means that there is more than one subnet allocated for a given
+        link. Two most common examples where this is the case are long lasting
+        network renumbering (where both old and new address space is still being
+        used) and a cable network. In a cable network both cable modems and the
+        devices behind them are physically connected to the same link, yet
+        they use distinct addressing. In such case, the DHCPv4 server needs
+        additional information (IPv4 address of the relay) to properly select
+        an appropriate subnet.
+      </para>
+      <para>
+        The following example assumes that there is a subnet 192.0.2.0/24
+        that is accessible via relay that uses 10.0.0.1 as its IPv4 address.
+        The server will be able to select this subnet for any incoming packets
+        that came from a relay that has an address in 192.0.2.0/24 subnet.
+        It will also select that subnet for a relay with address 10.0.0.1.
+        <screen>
+&gt; <userinput>config add Dhcp4/subnet4</userinput>
+&gt; <userinput>config set Dhcp4/subnet4[0]/subnet "192.0.2.0/24"</userinput>
+&gt; <userinput>config set Dhcp4/subnet4[0]/pool [ "192.0.2.10 - 192.0.2.20" ]</userinput>
+&gt; <userinput>config set Dhcp4/subnet4[0]/relay/ip-address "10.0.0.1"</userinput>
+&gt; <userinput>config commit</userinput></screen>
+      </para>
+
+    </section>
+
+      <section id="dhcp4-srv-example-client-class-relay">
+        <title>Segregating IPv4 clients in a cable network</title>
+        <para>
+          In certain cases, it is useful to mix relay address information,
+          introduced in <xref linkend="dhcp4-relay-override"/> with client
+          classification, explained in <xref linkend="dhcp4-subnet-class"/>.
+          One specific example is cable network, where typically modems
+          get addresses from a different subnet than all devices connected
+          behind them.
+        </para>
+        <para>
+          Let's assume that there is one CMTS (Cable Modem Termination System)
+          with one CM MAC (a physical link that modems are connected to).
+          We want the modems to get addresses from the 10.1.1.0/24 subnet, while
+          everything connected behind modems should get addresses from another
+          subnet (192.0.2.0/24). The CMTS that acts as a relay an uses address
+          10.1.1.1. The following configuration can serve that configuration:
+        <screen>
+&gt; <userinput>config add Dhcp4/subnet4</userinput>
+&gt; <userinput>config set Dhcp4/subnet4[0]/subnet "10.1.1.0/24"</userinput>
+&gt; <userinput>config set Dhcp4/subnet4[0]/pool [ "10.1.1.2 - 10.1.1.20" ]</userinput>
+&gt; <userinput>config set Dhcp4/subnet4[0]/client-class "docsis3.0"</userinput>
+&gt; <userinput>config set Dhcp4/subnet4[0]/relay/ip-address "10.1.1.1"</userinput>
+&gt; <userinput>config add Dhcp4/subnet4</userinput>
+&gt; <userinput>config set Dhcp4/subnet4[1]/subnet "192.0.2.0/24"</userinput>
+&gt; <userinput>config set Dhcp4/subnet4[1]/pool [ "192.0.2.10 - 192.0.2.20" ]</userinput>
+&gt; <userinput>config set Dhcp4/subnet4[1]/relay/ip-address "10.1.1.1"</userinput>
+&gt; <userinput>config commit</userinput></screen>
+      </para>
+      </section>
+
     <section id="dhcp4-std">
       <title>Supported Standards</title>
       <para>The following standards and draft standards are currently
@@ -4691,6 +4766,23 @@ Dhcp4/renew-timer	1000	integer	(default)
       </itemizedlist>
     </section>
 
+    <!--
+    <section id="dhcp4-srv-examples">
+      <title>Kea DHCPv4 server examples</title>
+
+      <para>
+        This section provides easy to use example. Each example can be read
+        separately. It is not intended to be read sequentially as there will
+        be many repetitions between examples. They are expected to serve as
+        easy to use copy-paste solutions to many common deployments.
+      </para>
+
+      @todo: add simple configuration for direct clients
+      @todo: add configuration for relayed clients
+      @todo: add client classification example
+
+    </section> -->
+
   </chapter>
 
   <chapter id="dhcp6">
@@ -5459,17 +5551,17 @@ should include options from the isc option space:
           The DHCPv6 server may receive requests from local (connected to the
           same subnet as the server) and remote (connecting via relays) clients.
           As server may have many subnet configurations defined, it must select
-          appropriate subnet for a given request. To do this, the server first
-          checks if there is only one subnet defined and source of the packet is
-          link-local. If this is the case, the server assumes that the only
-          subnet defined is local and client is indeed connected to it. This
-          check simplifies small deployments.
+          appropriate subnet for a given request.
           </para>
           <para>
-          If there are two or more subnets defined, the server can not assume
-          which of those (if any) subnets are local. Therefore an optional
-          "interface" parameter is available within a subnet definition to
-          designate that a given subnet is local, i.e. reachable directly over
+          The server can not assume which of configured subnets are local. It is
+          possible in IPv4, where there is reasonable expectation that the
+          server will have a (global) IPv4 address configured on the interface,
+          and can use that information to detect whether a subnet is local or
+          not. That assumption is not true in IPv6, as the DHCPv6 must be able
+          to operate with having link-local addresses only. Therefore an optional
+          &quot;interface&quot; parameter is available within a subnet definition
+          to designate that a given subnet is local, i.e. reachable directly over
           specified interface. For example the server that is intended to serve
           a local subnet over eth0 may be configured as follows:
 <screen>
@@ -5558,9 +5650,10 @@ should include options from the isc option space:
       assign it to zero or more classes. This classification is currently simple,
       but is expected to grow in capability soon. Currently the server checks whether
       incoming packet has vendor class option (16). If it has, content
-      of that option is interpreted as a class. For example, modern cable modems
-      will send this option with value &quot;docsis3.0&quot; and as a result the
-      packet will belong to class &quot;docsis3.0&quot;.
+      of that option is prepended with &quot;VENDOR_CLASS_&quot; interpreted as a
+      class. For example, modern cable modems will send this option with value
+      &quot;docsis3.0&quot; and as a result the packet will belong to class
+      &quot;VENDOR_CLASS_docsis3.0&quot;.
       </para>
 
       <para>It is envisaged that the client classification will be used for changing
@@ -5641,6 +5734,78 @@ should include options from the isc option space:
 
     </section>
 
+    <section id="dhcp6-relay-override">
+      <title>Using specific relay agent for a subnet</title>
+      <para>
+        The relay has to have an interface connected to the link on which
+        the clients are being configured. Typically the relay has a global IPv6
+        address configured on that interface that belongs to the subnet that
+        the server will assign addresses from. In such typical case, the
+        server is able to use IPv6 address inserted by the relay (in link-addr
+        field in RELAY-FORW message) to select appropriate subnet.
+      </para>
+      <para>
+        However, that is not always the case. The relay 
+        address may not match the subnet in certain deployments. This
+        usually means that there is more than one subnet allocated for a given
+        link. Two most common examples where this is the case are long lasting
+        network renumbering (where both old and new address space is still being
+        used) and a cable network. In a cable network both cable modems and the
+        devices behind them are physically connected to the same link, yet
+        they use distinct addressing. In such case, the DHCPv6 server needs
+        additional information (like the value of interface-id option or IPv6
+        address inserted in the link-addr field in RELAY-FORW message) to
+        properly select an appropriate subnet.
+      </para>
+      <para>
+        The following example assumes that there is a subnet 2001:db8:1::/64
+        that is accessible via relay that uses 3000::1 as its IPv6 address.
+        The server will be able to select this subnet for any incoming packets
+        that came from a relay that has an address in 2001:db8:1::/64 subnet.
+        It will also select that subnet for a relay with address 3000::1.
+        <screen>
+&gt; <userinput>config add Dhcp6/subnet6</userinput>
+&gt; <userinput>config set Dhcp6/subnet6[0]/subnet "2001:db8:1::/64"</userinput>
+&gt; <userinput>config set Dhcp6/subnet6[0]/pool [ "2001:db8:1::2 - 2001:db8:1::ffff" ]</userinput>
+&gt; <userinput>config set Dhcp6/subnet6[0]/relay/ip-address "3000::1"</userinput>
+&gt; <userinput>config commit</userinput></screen>
+      </para>
+
+    </section>
+
+      <section id="dhcp6-client-class-relay">
+        <title>Segregating IPv6 clients in a cable network</title>
+        <para>
+          In certain cases, it is useful to mix relay address information,
+          introduced in <xref linkend="dhcp6-relay-override"/> with client
+          classification, explained in <xref linkend="dhcp6-subnet-class"/>.
+          One specific example is cable network, where typically modems
+          get addresses from a different subnet than all devices connected
+          behind them.
+        </para>
+        <para>
+          Let's assume that there is one CMTS (Cable Modem Termination System)
+          with one CM MAC (a physical link that modems are connected to).
+          We want the modems to get addresses from the 3000::/64 subnet,
+          while everything connected behind modems should get addresses from
+          another subnet (2001:db8:1::/64). The CMTS that acts as a relay
+          an uses address 3000::1. The following configuration can serve
+          that configuration:
+        <screen>
+&gt; <userinput>config add Dhcp6/subnet6</userinput>
+&gt; <userinput>config set Dhcp6/subnet6[0]/subnet "3000::/64"</userinput>
+&gt; <userinput>config set Dhcp6/subnet6[0]/pool [ "3000::2 - 3000::ffff" ]</userinput>
+&gt; <userinput>config set Dhcp6/subnet6[0]/client-class "docsis3.0"</userinput>
+&gt; <userinput>config set Dhcp6/subnet6[0]/relay/ip-address "3000::1"</userinput>
+&gt; <userinput>config add Dhcp6/subnet6</userinput>
+&gt; <userinput>config set Dhcp6/subnet6[1]/subnet "2001:db8:1::/64"</userinput>
+&gt; <userinput>config set Dhcp6/subnet6[1]/pool [ "2001:db8:1::1 - 2001:db8:1::ffff" ]</userinput>
+&gt; <userinput>config set Dhcp6/subnet6[1]/relay/ip-address "3000::1"</userinput>
+&gt; <userinput>config commit</userinput></screen>
+      </para>
+      </section>
+
+
     <section id="dhcp6-std">
       <title>Supported Standards</title>
       <para>The following standards and draft standards are currently
@@ -5712,6 +5877,23 @@ Dhcp6/renew-timer	1000	integer	(default)
       </itemizedlist>
     </section>
 
+    <!--
+    <section id="dhcp6-srv-examples">
+      <title>Kea DHCPv6 server examples</title>
+
+      <para>
+        This section provides easy to use example. Each example can be read
+        separately. It is not intended to be read sequentially as there will
+        be many repetitions between examples. They are expected to serve as
+        easy to use copy-paste solutions to many common deployments.
+      </para>
+
+      @todo: add simple configuration for direct clients
+      @todo: add configuration for relayed clients
+      @todo: add client classification example
+
+    </section> -->
+
   </chapter>
 
   <chapter id="libdhcp">

+ 4 - 1
m4macros/ax_python_sqlite3.m4

@@ -11,7 +11,10 @@ if "$PYTHON" -c 'import sqlite3' 2>/dev/null ; then
     AC_MSG_RESULT(ok)
 else
     AC_MSG_RESULT(missing)
-    AC_MSG_ERROR([Missing sqlite3 python module.])
+
+    if test "x$want_dns" = "xyes" ; then
+        AC_MSG_ERROR([Missing sqlite3 python module that is required by DNS components.])
+    fi
 fi
 
 ])dnl AX_PYTHON_SQLITE3

+ 7 - 3
src/Makefile.am

@@ -1,6 +1,10 @@
-SUBDIRS = lib bin
-# @todo hooks lib could be a configurable switch
-SUBDIRS += hooks
+if WANT_DHCP
+
+want_hooks = hooks
+
+endif # WANT_DHCP
+
+SUBDIRS = lib bin $(want_hooks)
 
 EXTRA_DIST = \
 	cppcheck-suppress.lst		\

+ 30 - 8
src/bin/Makefile.am

@@ -1,16 +1,38 @@
-if BUILD_EXPERIMENTAL_RESOLVER
-# Build resolver only with --enable-experimental-resolver
-experimental_resolver = resolver
-endif
+if WANT_DHCP
+
+want_d2 = d2
+want_dhcp4 = dhcp4
+want_dhcp6 = dhcp6
+
+endif # WANT_DHCP
+
+if WANT_DNS
 
-SUBDIRS = bind10 bindctl cfgmgr ddns loadzone msgq cmdctl auth xfrin \
-	xfrout usermgr zonemgr stats tests $(experimental_resolver) \
-	sockcreator dhcp4 dhcp6 d2 dbutil sysinfo
+want_auth = auth
+want_dbutil = dbutil
+want_ddns = ddns
+want_loadzone = loadzone
+want_xfrin = xfrin
+want_xfrout = xfrout
+want_zonemgr = zonemgr
+
+if WANT_EXPERIMENTAL_RESOLVER
+want_resolver = resolver
+endif
 
 if USE_SHARED_MEMORY
 # Build the memory manager only if we have shared memory.
 # It is useless without it.
-SUBDIRS += memmgr
+want_memmgr = memmgr
 endif
 
+endif # WANT_DNS
+
+# The following build order must be maintained. So we create the
+# variables above and add directories in that order to SUBDIRS.
+SUBDIRS = bind10 bindctl cfgmgr $(want_ddns) $(want_loadzone) msgq cmdctl \
+	$(want_auth) $(want_xfrin) $(want_xfrout) usermgr $(want_zonemgr) \
+	stats tests $(want_resolver) sockcreator $(want_dhcp4) $(want_dhcp6) \
+	$(want_d2) $(want_dbutil) sysinfo $(want_memmgr)
+
 check-recursive: all-recursive

+ 4 - 4
src/bin/auth/auth_srv.cc

@@ -498,7 +498,7 @@ AuthSrv::processMessage(const IOMessage& io_message, Message& message,
             impl_->resumeServer(server, message, stats_attrs, false);
             return;
         }
-    } catch (const Exception& ex) {
+    } catch (const isc::Exception& ex) {
         LOG_DEBUG(auth_logger, DBG_AUTH_DETAIL, AUTH_HEADER_PARSE_FAIL)
                   .arg(ex.what());
         impl_->resumeServer(server, message, stats_attrs, false);
@@ -522,7 +522,7 @@ AuthSrv::processMessage(const IOMessage& io_message, Message& message,
                          stats_attrs);
         impl_->resumeServer(server, message, stats_attrs, true);
         return;
-    } catch (const Exception& ex) {
+    } catch (const isc::Exception& ex) {
         LOG_DEBUG(auth_logger, DBG_AUTH_DETAIL, AUTH_PACKET_PARSE_FAILED)
                   .arg(ex.what());
         makeErrorMessage(impl_->renderer_, message, buffer, Rcode::SERVFAIL(),
@@ -660,7 +660,7 @@ AuthSrvImpl::processNormalQuery(const IOMessage& io_message,
                              stats_attrs);
             return (true);
         }
-    } catch (const Exception& ex) {
+    } catch (const isc::Exception& ex) {
         LOG_ERROR(auth_logger, AUTH_PROCESS_FAIL).arg(ex.what());
         makeErrorMessage(renderer_, message, buffer, Rcode::SERVFAIL(),
                          stats_attrs);
@@ -820,7 +820,7 @@ AuthSrvImpl::processNotify(const IOMessage& io_message, Message& message,
                       .arg(parsed_answer->str());
             return (false);
         }
-    } catch (const Exception& ex) {
+    } catch (const isc::Exception& ex) {
         LOG_ERROR(auth_logger, AUTH_ZONEMGR_COMMS).arg(ex.what());
         return (false);
     }

+ 3 - 4
src/bin/auth/tests/Makefile.am

@@ -29,6 +29,9 @@ CLEANFILES += $(abs_top_builddir)/src/lib/testutils/testdata/does-not-exist.sqli
 TESTS_ENVIRONMENT = \
         $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND)
 
+EXTRA_DIST = gen-query-testdata.py
+EXTRA_DIST += gen-statisticsitems_test.py
+
 # Do not define global tests, use check-local so
 # environment can be set (needed for dynamic loading)
 TESTS =
@@ -91,8 +94,6 @@ BUILT_SOURCES += example_base_inc.cc example_nsec3_inc.cc
 BUILT_SOURCES += testdata/example-base.sqlite3
 BUILT_SOURCES += testdata/example-nsec3.sqlite3
 
-EXTRA_DIST = gen-query-testdata.py
-
 CLEANFILES += example_base_inc.cc example_nsec3_inc.cc
 
 example_base_inc.cc: $(srcdir)/testdata/example-base-inc.zone
@@ -118,8 +119,6 @@ testdata/example-nsec3.sqlite3: testdata/example-nsec3.zone testdata/example-com
 		-c "{\"database_file\": \"$(builddir)/testdata/example-nsec3.sqlite3\"}" \
 		example.com testdata/example-nsec3.zone
 
-EXTRA_DIST += gen-statisticsitems_test.py
-
 check-local:
 	B10_FROM_BUILD=${abs_top_builddir} ./run_unittests
 	$(PYTHON) $(srcdir)/gen-statisticsitems_test.py $(top_builddir)/src/bin/auth/b10-auth.xml

+ 12 - 13
src/bin/auth/tests/auth_srv_unittest.cc

@@ -44,6 +44,7 @@
 
 #include <util/unittests/mock_socketsession.h>
 #include <dns/tests/unittest_util.h>
+#include <util/unittests/wiredata.h>
 #include <testutils/dnsmessage_test.h>
 #include <testutils/srv_test.h>
 #include <testutils/mockups.h>
@@ -82,8 +83,9 @@ using namespace isc::testutils;
 using namespace isc::server_common::portconfig;
 using namespace isc::auth::unittest;
 using isc::UnitTestUtil;
-using boost::scoped_ptr;
 using isc::auth::statistics::Counters;
+using isc::util::unittests::matchWireData;
+using boost::scoped_ptr;
 
 namespace {
 const char* const CONFIG_TESTDB =
@@ -1072,10 +1074,9 @@ TEST_F(AuthSrvTest, builtInQueryViaDNSServer) {
                                      response_message, response_obuffer);
 
     createBuiltinVersionResponse(default_qid, response_data);
-    EXPECT_PRED_FORMAT4(UnitTestUtil::matchWireData,
-                        response_obuffer->getData(),
-                        response_obuffer->getLength(),
-                        &response_data[0], response_data.size());
+    matchWireData(&response_data[0], response_data.size(),
+                  response_obuffer->getData(),
+                  response_obuffer->getLength());
 
     // After it has been run, the message should be cleared
     EXPECT_EQ(0, parse_message->getRRCount(Message::SECTION_QUESTION));
@@ -1095,10 +1096,9 @@ TEST_F(AuthSrvTest, builtInQuery) {
     server.processMessage(*io_message, *parse_message, *response_obuffer,
                           &dnsserv);
     createBuiltinVersionResponse(default_qid, response_data);
-    EXPECT_PRED_FORMAT4(UnitTestUtil::matchWireData,
-                        response_obuffer->getData(),
-                        response_obuffer->getLength(),
-                        &response_data[0], response_data.size());
+    matchWireData(&response_data[0], response_data.size(),
+                  response_obuffer->getData(),
+                  response_obuffer->getLength());
     checkAllRcodeCountersZeroExcept(Rcode::NOERROR(), 1);
 }
 
@@ -1114,10 +1114,9 @@ TEST_F(AuthSrvTest, iqueryViaDNSServer) {
 
     UnitTestUtil::readWireData("iquery_response_fromWire.wire",
                                response_data);
-    EXPECT_PRED_FORMAT4(UnitTestUtil::matchWireData,
-                        response_obuffer->getData(),
-                        response_obuffer->getLength(),
-                        &response_data[0], response_data.size());
+    matchWireData(&response_data[0], response_data.size(),
+                  response_obuffer->getData(),
+                  response_obuffer->getLength());
 }
 
 // Install a Sqlite3 data source with testing data.

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

@@ -29,6 +29,9 @@ The named component failed previously and we will try to restart it to provide
 as flawless service as possible, but it should be investigated what happened,
 as it could happen again.
 
+% BIND10_COMPONENT_SHUTDOWN_ERROR An error occured stopping component %1
+An attempt to gracefully shutdown a component failed.
+
 % BIND10_COMPONENT_START component %1 is starting
 The named component is about to be started by the b10-init process.
 
@@ -151,6 +154,10 @@ it now. The new configuration is printed.
 % BIND10_RECEIVED_SIGNAL received signal %1
 The b10-init module received the given signal.
 
+% BIND10_RECONFIGURE_ERROR Error applying new config: %1
+A new configuration was received, but there was an error doing the
+re-configuration.
+
 % BIND10_RESTART_COMPONENT_SKIPPED Skipped restarting a component %1
 The b10-init module tried to restart a component after it failed (crashed)
 unexpectedly, but the b10-init then found that the component had been removed
@@ -325,10 +332,3 @@ the configuration manager to start up.  The total length of time Init
 will wait for the configuration manager before reporting an error is
 set with the command line --wait switch, which has a default value of
 ten seconds.
-
-% BIND10_RECONFIGURE_ERROR Error applying new config: %1
-A new configuration was received, but there was an error doing the
-re-configuration.
-
-% BIND10_COMPONENT_SHUTDOWN_ERROR An error occured stopping component %1
-An attempt to gracefully shutdown a component failed.

+ 9 - 2
src/bin/cfgmgr/plugins/Makefile.am

@@ -6,13 +6,20 @@ datasrc.spec: datasrc.spec.pre
 	$(SED) -e "s|@@STATIC_ZONE_FILE@@|$(pkgdatadir)/static.zone|;s|@@SQLITE3_DATABASE_FILE@@|$(localstatedir)/$(PACKAGE)/zone.sqlite3|" datasrc.spec.pre >$@
 
 config_plugindir = @prefix@/share/@PACKAGE@/config_plugins
-config_plugin_DATA = logging.spec tsig_keys.spec datasrc.spec
+config_plugin_DATA = logging.spec tsig_keys.spec
 
-python_PYTHON = b10logging.py tsig_keys.py datasrc_config_plugin.py
+python_PYTHON = b10logging.py tsig_keys.py
 pythondir = $(config_plugindir)
 
 CLEANFILES = b10logging.pyc tsig_keys.pyc datasrc.spec
 CLEANDIRS = __pycache__
 
+if WANT_DNS
+
+config_plugin_DATA += datasrc.spec
+python_PYTHON += datasrc_config_plugin.py
+
+endif
+
 clean-local:
 	rm -rf $(CLEANDIRS)

+ 9 - 1
src/bin/cfgmgr/plugins/tests/Makefile.am

@@ -1,5 +1,13 @@
 PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
-PYTESTS = tsig_keys_test.py logging_test.py datasrc_test.py
+PYTESTS = tsig_keys_test.py logging_test.py
+
+if WANT_DNS
+
+PYTESTS += datasrc_test.py
+
+endif
+
+
 
 EXTRA_DIST = $(PYTESTS)
 

+ 13 - 1
src/bin/d2/d2_cfg_mgr.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014 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
@@ -73,6 +73,18 @@ D2CfgMgr::~D2CfgMgr() {
 }
 
 bool
+D2CfgMgr::forwardUpdatesEnabled() {
+    // Forward updates are not enabled if no forward servers are defined.
+    return (getD2CfgContext()->getForwardMgr()->size() > 0);
+}
+
+bool
+D2CfgMgr::reverseUpdatesEnabled() {
+    // Reverse updates are not enabled if no revese servers are defined.
+    return (getD2CfgContext()->getReverseMgr()->size() > 0);
+}
+
+bool
 D2CfgMgr::matchForward(const std::string& fqdn, DdnsDomainPtr& domain) {
     if (fqdn.empty()) {
         // This is a programmatic error and should not happen.

+ 21 - 1
src/bin/d2/d2_cfg_mgr.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014 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
@@ -126,6 +126,26 @@ public:
         return (boost::dynamic_pointer_cast<D2CfgContext>(getContext()));
     }
 
+    /// @brief Returns whether not forward updates are enabled.
+    ///
+    /// This method currently uses the presence or absence of Foward DDNS
+    /// Domains to determine if forward updates are enabled or disabled.
+    /// @todo This could be expanded to include the check of a configurable
+    /// boolean value.
+    ///
+    /// @return true if forward updates are enabled, false otherwise.
+    bool forwardUpdatesEnabled();
+
+    /// @brief Returns whether not reverse updates are enabled.
+    ///
+    /// This method currently uses the presence or absence of Reverse DDNS
+    /// Domains to determine if reverse updates are enabled or disabled.
+    /// @todo This could be expanded to include the check of a configurable
+    /// boolean value.
+    ///
+    /// @return true if reverse updates are enabled, false otherwise.
+    bool reverseUpdatesEnabled();
+
     /// @brief Matches a given FQDN to a forward domain.
     ///
     /// This calls the matchDomain method of the forward domain manager to

+ 4 - 3
src/bin/d2/d2_config.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2014 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
@@ -20,6 +20,7 @@
 
 #include <boost/foreach.hpp>
 #include <boost/lexical_cast.hpp>
+#include <boost/algorithm/string/predicate.hpp>
 
 #include <string>
 
@@ -131,7 +132,7 @@ DdnsDomainListMgr::matchDomain(const std::string& fqdn, DdnsDomainPtr& domain) {
 
         // If the lengths are identical and the names match we're done.
         if (req_len == dom_len) {
-            if (fqdn == domain_name) {
+            if (boost::iequals(fqdn, domain_name)) {
                 // exact match, done
                 domain = map_pair.second;
                 return (true);
@@ -143,7 +144,7 @@ DdnsDomainListMgr::matchDomain(const std::string& fqdn, DdnsDomainPtr& domain) {
             // prevents "onetwo.net" from matching "two.net".
             size_t offset = req_len - dom_len;
             if ((fqdn[offset - 1] == '.')  &&
-               (fqdn.compare(offset, std::string::npos, domain_name) == 0)) {
+               (boost::iequals(fqdn.substr(offset), domain_name))) {
                 // Fqdn contains domain name, keep it if its better than
                 // any we have matched so far.
                 if (dom_len > match_len) {

+ 176 - 158
src/bin/d2/d2_messages.mes

@@ -112,6 +112,15 @@ service first starts.
 This is an informational message issued when the controller is exiting
 following a shut down (normal or otherwise) of the service.
 
+% DHCP_DDNS_ADD_FAILED DHCP_DDNS Transaction outcome: %1
+This is an error message issued after DHCP_DDNS attempts to submit DNS mapping
+entry additions have failed.  The precise reason for the failure should be
+documented in preceding log entries.
+
+% DHCP_DDNS_ADD_SUCCEEDED DHCP_DDNS successfully added the DNS mapping addition for this request: %1
+This is an informational message issued after DHCP_DDNS has submitted DNS
+mapping additions which were received and accepted by an appropriate DNS server.
+
 % DHCP_DDNS_AT_MAX_TRANSACTIONS application has %1 queued requests but has reached maximum number of %2 concurrent transactions
 This is a debug message that indicates that the application has DHCP_DDNS
 requests in the queue but is working as many concurrent requests as allowed.
@@ -132,6 +141,114 @@ has been invoked.
 This is a debug message issued when the Dhcp-Ddns application encounters an
 unrecoverable error from within the event loop.
 
+% DHCP_DDNS_FORWARD_ADD_BAD_DNSCLIENT_STATUS DHCP_DDNS received an unknown DNSClient status: %1, while adding a forward address mapping for FQDN %2 to DNS server %3
+This is an error message issued when DNSClient returns an unrecognized status
+while DHCP_DDNS was adding a forward address mapping.  The request will be
+aborted.  This is most likely a programmatic issue and should be reported.
+
+% DHCP_DDNS_FORWARD_ADD_BUILD_FAILURE DNS udpate message to add a forward DNS entry could not be constructed for this request: %1, reason: %2
+This is an error message issued when an error occurs attempting to construct
+the server bound packet requesting a forward address addition.  This is due
+to invalid data contained in the NameChangeRequest. The request will be aborted.
+This is most likely a configuration issue.
+
+% DHCP_DDNS_FORWARD_ADD_IO_ERROR DHCP_DDNS encountered an IO error sending a forward mapping add for FQDN %1 to DNS server %2
+This is an error message issued when a communication error occurs while
+DHCP_DDNS is carrying out a forward address update.  The application will
+retry against the same server or others as appropriate.
+
+% DHCP_DDNS_FORWARD_ADD_REJECTED DNS Server, %1, rejected a DNS update request to add the address mapping for FQDN, %2, with an RCODE: %3
+This is an error message issued when an update was rejected by the DNS server
+it was sent to for the reason given by the RCODE. The rcode values are defined
+in RFC 2136.
+
+% DHCP_DDNS_FORWARD_ADD_RESP_CORRUPT DHCP_DDNS received a corrupt response from the DNS server, %1, while adding forward address mapping for FQDN, %2
+This is an error message issued when the response received by DHCP_DDNS, to a
+update request to add a forward address mapping,  is mangled or malformed.
+The application will retry against the same server or others as appropriate.
+
+% DHCP_DDNS_FORWARD_REMOVE_ADDRS_BAD_DNSCLIENT_STATUS DHCP_DDNS received an unknown DNSClient status: %1, while removing a forward address mapping for FQDN %2 to DNS server %3
+This is an error message issued when DNSClient returns an unrecognized status
+while DHCP_DDNS was removing a forward address mapping.  The request will be
+aborted.  This is most likely a programmatic issue and should be reported.
+
+% DHCP_DDNS_FORWARD_REMOVE_ADDRS_BUILD_FAILURE DNS udpate message to remove a forward DNS Address entry could not be constructed for this request: %1,  reason: %2
+This is an error message issued when an error occurs attempting to construct
+the server bound packet requesting a forward address (A or AAAA) removal.  This
+is due to invalid data contained in the NameChangeRequest. The request will be
+aborted.  This is most likely a configuration issue.
+
+% DHCP_DDNS_FORWARD_REMOVE_ADDRS_IO_ERROR DHCP_DDNS encountered an IO error sending a forward mapping address removal for FQDN %1 to DNS server %2
+This is an error message issued when a communication error occurs while
+DHCP_DDNS is carrying out a forward address remove.  The application will retry
+against the same server or others as appropriate.
+
+% DHCP_DDNS_FORWARD_REMOVE_ADDRS_REJECTED DNS Server, %1, rejected a DNS update request to remove the forward address mapping for FQDN, %2, with an RCODE: %3
+This is an error message issued when an update was rejected by the DNS server
+it was sent to for the reason given by the RCODE. The rcode values are defined
+in RFC 2136.
+
+% DHCP_DDNS_FORWARD_REMOVE_ADDRS_RESP_CORRUPT DHCP_DDNS received a corrupt response from the DNS server, %1, while removing forward address mapping for FQDN, %2
+This is an error message issued when the response received by DHCP_DDNS, to a
+update request to remove a forward address mapping, is mangled or malformed.
+The application will retry against the same server or others as appropriate.
+
+% DHCP_DDNS_FORWARD_REMOVE_RRS_BAD_DNSCLIENT_STATUS DHCP_DDNS received an unknown DNSClient status: %1, while removing forward RRs for FQDN %2 to DNS server %3
+This is an error message issued when DNSClient returns an unrecognized status
+while DHCP_DDNS was removing forward RRs.  The request will be aborted. This is
+most likely a programmatic issue and should be reported.
+
+% DHCP_DDNS_FORWARD_REMOVE_RRS_BUILD_FAILURE DNS udpate message to remove forward DNS RR entries could not be constructed for this request: %1,  reason: %2
+This is an error message issued when an error occurs attempting to construct
+the server bound packet requesting forward RR (DHCID RR) removal.  This is due
+to invalid data contained in the NameChangeRequest. The request will be aborted.This is most likely a configuration issue.
+
+% DHCP_DDNS_FORWARD_REMOVE_RRS_IO_ERROR DHCP_DDNS encountered an IO error sending a forward RR removal for FQDN %1 to DNS server %2
+This is an error message issued when a communication error occurs while
+DHCP_DDNS is carrying out a forward RR remove.  The application will retry
+against the same server.
+
+% DHCP_DDNS_FORWARD_REMOVE_RRS_REJECTED DNS Server, %1, rejected a DNS update request to remove forward RR entries for FQDN, %2, with an RCODE: %3
+This is an error message issued when an update was rejected by the DNS server
+it was sent to for the reason given by the RCODE. The rcode values are defined
+in RFC 2136.
+
+% DHCP_DDNS_FORWARD_REMOVE_RRS_RESP_CORRUPT DHCP_DDNS received a corrupt response from the DNS server, %1, while removing forward RRs for FQDN, %2
+This is an error message issued when the response received by DHCP_DDNS, to a
+update request to remove forward RRs mapping, is mangled or malformed.
+The application will retry against the same server or others as appropriate.
+
+% DHCP_DDNS_FORWARD_REPLACE_BAD_DNSCLIENT_STATUS DHCP_DDNS received an unknown DNSClient status: %1, while replacing forward address mapping for FQDN %2 to DNS server %3
+This is an error message issued when DNSClient returns an unrecognized status
+while DHCP_DDNS was replacing a forward address mapping.  The request will be
+aborted.  This is most likely a programmatic issue and should be reported.
+
+% DHCP_DDNS_FORWARD_REPLACE_BUILD_FAILURE DNS update message to replace a forward DNS entry could not be constructed from this request: %1, reason: %2
+This is an error message issued when an error occurs attempting to construct
+the server bound packet requesting a forward address replacement.  This is
+due to invalid data contained in the NameChangeRequest. The request will be
+aborted.  This is most likely a configuration issue.
+
+% DHCP_DDNS_FORWARD_REPLACE_IO_ERROR DHCP_DDNS encountered an IO error sending a forward mapping replace for FQDN %1 to DNS server %2
+This is an error message issued when a communication error occurs while
+DHCP_DDNS is carrying out a forward address update.  The application will
+retry against the same server or others as appropriate.
+
+% DHCP_DDNS_FORWARD_REPLACE_REJECTED DNS Server, %1, rejected a DNS update request to replace the address mapping for FQDN, %2, with an RCODE: %3
+This is an error message issued when an update was rejected by the DNS server
+it was sent to for the reason given by the RCODE. The rcode values are defined
+in RFC 2136.
+
+% DHCP_DDNS_FORWARD_REPLACE_RESP_CORRUPT DHCP_DDNS received a corrupt response from the DNS server, %1, while replacing forward address mapping for FQDN, %2
+This is an error message issued when the response received by DHCP_DDNS, to a
+update request to replace a forward address mapping,  is mangled or malformed.
+The application will retry against the same server or others as appropriate.
+
+% DHCP_DDNS_FWD_REQUEST_IGNORED Forward updates are disabled, the forward portion of request will be ignored: %1
+This is a debug message issued when forward DNS updates are disabled and
+DHCP_DDNS receives an update request containing a forward DNS update. The
+forward update will not performed.
+
 % DHCP_DDNS_INVALID_RESPONSE received response to DNS Update message is malformed: %1
 This is a debug message issued when the DHCP-DDNS application encountered an
 error while decoding a response to DNS Update message. Typically, this error
@@ -142,7 +259,7 @@ This is a debug message issued when all of the queued requests represent clients
 for which there is a an update already in progress.  This may occur under
 normal operations but should be temporary situation.
 
-% DHCP_DDNS_NO_FWD_MATCH_ERROR the configured list of forward DDNS domains does not contain a match for FQDN %1  The request has been discarded.
+% DHCP_DDNS_NO_FWD_MATCH_ERROR the configured list of forward DDNS domains does not contain a match for: %1  The request has been discarded.
 This is an error message that indicates that DHCP_DDNS received a request to
 update a the forward DNS information for the given FQDN but for which there are
 no configured DDNS domains in the DHCP_DDNS configuration.  Either the DHCP_DDNS
@@ -154,7 +271,7 @@ This is warning message issued when there are no domains in the configuration
 which match the cited fully qualified domain name (FQDN).  The DNS Update
 request for the FQDN cannot be processed.
 
-% DHCP_DDNS_NO_REV_MATCH_ERROR the configured list of reverse DDNS domains does not contain a match for FQDN %1  The request has been discarded.
+% DHCP_DDNS_NO_REV_MATCH_ERROR the configured list of reverse DDNS domains does not contain a match for: %1  The request has been discarded.
 This is an error message that indicates that DHCP_DDNS received a request to
 update a the reverse DNS information for the given FQDN but for which there are
 no configured DDNS domains in the DHCP_DDNS configuration.  Either the DHCP_DDNS
@@ -241,77 +358,45 @@ receive was unexpected interrupted.  Normally, the read is receive is only
 interrupted as a normal part of stopping the queue manager.  This is most
 likely a programmatic issue that should be reported.
 
-% DHCP_DDNS_RUN_ENTER application has entered the event loop
-This is a debug message issued when the Dhcp-Ddns application enters
-its run method.
-
-% DHCP_DDNS_RUN_EXIT application is exiting the event loop
-This is a debug message issued when the Dhcp-Ddns exits the
-in event loop.
-
-% DHCP_DDNS_SHUTDOWN application received shutdown command with args: %1
-This is informational message issued when the application has been instructed
-to shut down by the controller.
-
-% DHCP_DDNS_STATE_MODEL_UNEXPECTED_ERROR application encountered an unexpected error while carrying out a NameChangeRequest: %1
-This is error message issued when the application fails to process a
-NameChangeRequest correctly. Some or all of the DNS updates requested as part
-of this update did not succeed. This is a programmatic error and should be
-reported.
-
-% DHCP_DDNS_FORWARD_ADD_REJECTED DNS Server, %1, rejected a DNS update request to add the address mapping for FQDN, %2, with an RCODE: %3
-This is an error message issued when an update was rejected by the DNS server
-it was sent to for the reason given by the RCODE. The rcode values are defined
-in RFC 2136.
+% DHCP_DDNS_REMOVE_FAILED DHCP_DDNS Transaction outcome: %1
+This is an error message issued after DHCP_DDNS attempts to submit DNS mapping
+entry removals have failed.  The precise reason for the failure should be
+documented in preceding log entries.
 
-% DHCP_DDNS_FORWARD_ADD_IO_ERROR DHCP_DDNS encountered an IO error sending a forward mapping add for FQDN %1 to DNS server %2
-This is an error message issued when a communication error occurs while
-DHCP_DDNS is carrying out a forward address update.  The application will
-retry against the same server or others as appropriate.
+% DHCP_DDNS_REMOVE_SUCCEEDED DHCP_DDNS successfully removed the DNS mapping addition for this request: %1
+This is an informational message issued after DHCP_DDNS has submitted DNS
+mapping removals which were received and accepted by an appropriate DNS server.
 
-% DHCP_DDNS_FORWARD_ADD_RESP_CORRUPT DHCP_DDNS received a corrupt response from the DNS server, %1, while adding forward address mapping for FQDN, %2
-This is an error message issued when the response received by DHCP_DDNS, to a
-update request to add a forward address mapping,  is mangled or malformed.
-The application will retry against the same server or others as appropriate.
+% DHCP_DDNS_REQUEST_DROPPED Request contains no enabled update requests and will be dropped: %1
+This is a debug message issued when DHCP_DDNS receives a request which does not
+contain updates in a direction that is enabled.  In other words, if only forward
+updates are enabled and request is recevied that asks only for reverse updates
+then the request is dropped.
 
-% DHCP_DDNS_FORWARD_ADD_BAD_DNSCLIENT_STATUS DHCP_DDNS received an unknown DNSClient status: %1, while adding a forward address mapping for FQDN %2 to DNS server %3
+% DHCP_DDNS_REVERSE_REMOVE_BAD_DNSCLIENT_STATUS DHCP_DDNS received an unknown DNSClient status: %1, while removing reverse address mapping for FQDN %2 to DNS server %3
 This is an error message issued when DNSClient returns an unrecognized status
-while DHCP_DDNS was adding a forward address mapping.  The request will be
+while DHCP_DDNS was removing a reverse address mapping.  The request will be
 aborted.  This is most likely a programmatic issue and should be reported.
 
-% DHCP_DDNS_FORWARD_REPLACE_REJECTED DNS Server, %1, rejected a DNS update request to replace the address mapping for FQDN, %2, with an RCODE: %3
-This is an error message issued when an update was rejected by the DNS server
-it was sent to for the reason given by the RCODE. The rcode values are defined
-in RFC 2136.
+% DHCP_DDNS_REVERSE_REMOVE_BUILD_FAILURE DNS update message to remove a reverse DNS entry could not be constructed from this request: %1,  reason: %2
+This is an error message issued when an error occurs attempting to construct
+the server bound packet requesting a reverse PTR removal.  This is
+due to invalid data contained in the NameChangeRequest. The request will be
+aborted.  This is most likely a configuration issue.
 
-% DHCP_DDNS_FORWARD_REPLACE_IO_ERROR DHCP_DDNS encountered an IO error sending a forward mapping replace for FQDN %1 to DNS server %2
+% DHCP_DDNS_REVERSE_REMOVE_IO_ERROR DHCP_DDNS encountered an IO error sending a reverse mapping remove for FQDN %1 to DNS server %2
 This is an error message issued when a communication error occurs while
-DHCP_DDNS is carrying out a forward address update.  The application will
+DHCP_DDNS is carrying out a reverse address update.  The application will
 retry against the same server or others as appropriate.
 
-% DHCP_DDNS_FORWARD_REPLACE_RESP_CORRUPT DHCP_DDNS received a corrupt response from the DNS server, %1, while replacing forward address mapping for FQDN, %2
-This is an error message issued when the response received by DHCP_DDNS, to a
-update request to replace a forward address mapping,  is mangled or malformed.
-The application will retry against the same server or others as appropriate.
-
-% DHCP_DDNS_FORWARD_REPLACE_BAD_DNSCLIENT_STATUS DHCP_DDNS received an unknown DNSClient status: %1, while replacing forward address mapping for FQDN %2 to DNS server %3
-This is an error message issued when DNSClient returns an unrecognized status
-while DHCP_DDNS was replacing a forward address mapping.  The request will be
-aborted.  This is most likely a programmatic issue and should be reported.
-
-% DHCP_DDNS_REVERSE_REPLACE_REJECTED DNS Server, %1, rejected a DNS update request to replace the reverse mapping for FQDN, %2, with an RCODE: %3
+% DHCP_DDNS_REVERSE_REMOVE_REJECTED DNS Server, %1, rejected a DNS update request to remove the reverse mapping for FQDN, %2, with an RCODE: %3
 This is an error message issued when an update was rejected by the DNS server
 it was sent to for the reason given by the RCODE. The rcode values are defined
 in RFC 2136.
 
-% DHCP_DDNS_REVERSE_REPLACE_IO_ERROR DHCP_DDNS encountered an IO error sending a reverse mapping replacement for FQDN %1 to DNS server %2
-This is an error message issued when a communication error occurs while
-DHCP_DDNS is carrying out a reverse address update.  The application will
-retry against the same server or others as appropriate.
-
-% DHCP_DDNS_REVERSE_REPLACE_RESP_CORRUPT DHCP_DDNS received a corrupt response from the DNS server, %1, while replacing reverse address mapping for FQDN, %2
+% DHCP_DDNS_REVERSE_REMOVE_RESP_CORRUPT DHCP_DDNS received a corrupt response from the DNS server, %1, while removing reverse address mapping for FQDN, %2
 This is an error message issued when the response received by DHCP_DDNS, to a
-update request to replace a reverse address,  is mangled or malformed.
+update request to remove a reverse address,  is mangled or malformed.
 The application will retry against the same server or others as appropriate.
 
 % DHCP_DDNS_REVERSE_REPLACE_BAD_DNSCLIENT_STATUS DHCP_DDNS received an unknown DNSClient status: %1, while replacing reverse address mapping for FQDN %2 to DNS server %3
@@ -319,128 +404,61 @@ This is an error message issued when DNSClient returns an unrecognized status
 while DHCP_DDNS was replacing a reverse address mapping.  The request will be
 aborted.  This is most likely a programmatic issue and should be reported.
 
-% DHCP_DDNS_TRANS_SEND_ERROR application encountered an unexpected error while attempting to send a DNS update: %1
-This is error message issued when the application is able to construct an update
-message but the attempt to send it suffered a unexpected error. This is most
-likely a programmatic error, rather than a communications issue. Some or all
-of the DNS updates requested as part of this request did not succeed.
-
-% DHCP_DDNS_FORWARD_ADD_BUILD_FAILURE DNS udpate message to add a forward DNS entry could not be constructed for this request: %1, reason: %2
-This is an error message issued when an error occurs attempting to construct
-the server bound packet requesting a forward address addition.  This is due
-to invalid data contained in the NameChangeRequest. The request will be aborted.
-This is most likely a configuration issue.
-
-% DHCP_DDNS_FORWARD_REPLACE_BUILD_FAILURE DNS update message to replace a forward DNS entry could not be constructed from this request: %1, reason: %2
-This is an error message issued when an error occurs attempting to construct
-the server bound packet requesting a forward address replacement.  This is
-due to invalid data contained in the NameChangeRequest. The request will be
-aborted.  This is most likely a configuration issue.
-
 % DHCP_DDNS_REVERSE_REPLACE_BUILD_FAILURE DNS update message to replace a reverse DNS entry could not be constructed from this request: %1, reason: %2
 This is an error message issued when an error occurs attempting to construct
 the server bound packet requesting a reverse PTR replacement.  This is
 due to invalid data contained in the NameChangeRequest. The request will be
 aborted.  This is most likely a configuration issue.
 
-% DHCP_DDNS_ADD_SUCCEEDED DHCP_DDNS successfully added the DNS mapping addition for this request: %1
-This is a debug message issued after DHCP_DDNS has submitted DNS mapping
-additions which were received and accepted by an appropriate DNS server.
-
-% DHCP_DDNS_ADD_FAILED DHCP_DDNS failed attempting to make DNS mapping additions for this request: %1, event: %2
-This is an error message issued after DHCP_DDNS attempts to submit DNS mapping
-entry additions have failed.  The precise reason for the failure should be
-documented in preceding log entries.
-
-% DHCP_DDNS_FORWARD_REMOVE_ADDRS_BUILD_FAILURE DNS udpate message to remove a forward DNS Address entry could not be constructed for this request: %1,  reason: %2
-This is an error message issued when an error occurs attempting to construct
-the server bound packet requesting a forward address (A or AAAA) removal.  This
-is due to invalid data contained in the NameChangeRequest. The request will be
-aborted.  This is most likely a configuration issue.
-
-% DHCP_DDNS_FORWARD_REMOVE_ADDRS_REJECTED DNS Server, %1, rejected a DNS update request to remove the forward address mapping for FQDN, %2, with an RCODE: %3
-This is an error message issued when an update was rejected by the DNS server
-it was sent to for the reason given by the RCODE. The rcode values are defined
-in RFC 2136.
-
-% DHCP_DDNS_FORWARD_REMOVE_ADDRS_IO_ERROR DHCP_DDNS encountered an IO error sending a forward mapping address removal for FQDN %1 to DNS server %2
+% DHCP_DDNS_REVERSE_REPLACE_IO_ERROR DHCP_DDNS encountered an IO error sending a reverse mapping replacement for FQDN %1 to DNS server %2
 This is an error message issued when a communication error occurs while
-DHCP_DDNS is carrying out a forward address remove.  The application will retry
-against the same server or others as appropriate.
-
-% DHCP_DDNS_FORWARD_REMOVE_ADDRS_RESP_CORRUPT DHCP_DDNS received a corrupt response from the DNS server, %1, while removing forward address mapping for FQDN, %2
-This is an error message issued when the response received by DHCP_DDNS, to a
-update request to remove a forward address mapping, is mangled or malformed.
-The application will retry against the same server or others as appropriate.
-
-% DHCP_DDNS_FORWARD_REMOVE_ADDRS_BAD_DNSCLIENT_STATUS DHCP_DDNS received an unknown DNSClient status: %1, while removing a forward address mapping for FQDN %2 to DNS server %3
-This is an error message issued when DNSClient returns an unrecognized status
-while DHCP_DDNS was removing a forward address mapping.  The request will be
-aborted.  This is most likely a programmatic issue and should be reported.
-
-% DHCP_DDNS_FORWARD_REMOVE_RRS_BUILD_FAILURE DNS udpate message to remove forward DNS RR entries could not be constructed for this request: %1,  reason: %2
-This is an error message issued when an error occurs attempting to construct
-the server bound packet requesting forward RR (DHCID RR) removal.  This is due
-to invalid data contained in the NameChangeRequest. The request will be aborted.This is most likely a configuration issue.
+DHCP_DDNS is carrying out a reverse address update.  The application will
+retry against the same server or others as appropriate.
 
-% DHCP_DDNS_FORWARD_REMOVE_RRS_REJECTED DNS Server, %1, rejected a DNS update request to remove forward RR entries for FQDN, %2, with an RCODE: %3
+% DHCP_DDNS_REVERSE_REPLACE_REJECTED DNS Server, %1, rejected a DNS update request to replace the reverse mapping for FQDN, %2, with an RCODE: %3
 This is an error message issued when an update was rejected by the DNS server
 it was sent to for the reason given by the RCODE. The rcode values are defined
 in RFC 2136.
 
-% DHCP_DDNS_FORWARD_REMOVE_RRS_IO_ERROR DHCP_DDNS encountered an IO error sending a forward RR removal for FQDN %1 to DNS server %2
-This is an error message issued when a communication error occurs while
-DHCP_DDNS is carrying out a forward RR remove.  The application will retry
-against the same server.
-
-% DHCP_DDNS_FORWARD_REMOVE_RRS_RESP_CORRUPT DHCP_DDNS received a corrupt response from the DNS server, %1, while removing forward RRs for FQDN, %2
+% DHCP_DDNS_REVERSE_REPLACE_RESP_CORRUPT DHCP_DDNS received a corrupt response from the DNS server, %1, while replacing reverse address mapping for FQDN, %2
 This is an error message issued when the response received by DHCP_DDNS, to a
-update request to remove forward RRs mapping, is mangled or malformed.
+update request to replace a reverse address,  is mangled or malformed.
 The application will retry against the same server or others as appropriate.
 
-% DHCP_DDNS_FORWARD_REMOVE_RRS_BAD_DNSCLIENT_STATUS DHCP_DDNS received an unknown DNSClient status: %1, while removing forward RRs for FQDN %2 to DNS server %3
-This is an error message issued when DNSClient returns an unrecognized status
-while DHCP_DDNS was removing forward RRs.  The request will be aborted. This is
-most likely a programmatic issue and should be reported.
-
-% DHCP_DDNS_REVERSE_REMOVE_BUILD_FAILURE DNS update message to remove a reverse DNS entry could not be constructed from this request: %1,  reason: %2
-This is an error message issued when an error occurs attempting to construct
-the server bound packet requesting a reverse PTR removal.  This is
-due to invalid data contained in the NameChangeRequest. The request will be
-aborted.  This is most likely a configuration issue.
-
-% DHCP_DDNS_REVERSE_REMOVE_REJECTED DNS Server, %1, rejected a DNS update request to remove the reverse mapping for FQDN, %2, with an RCODE: %3
-This is an error message issued when an update was rejected by the DNS server
-it was sent to for the reason given by the RCODE. The rcode values are defined
-in RFC 2136.
+% DHCP_DDNS_REV_REQUEST_IGNORED Reverse updates are disabled, the reverse portion of request will be ignored: %1
+This is a debug message issued when reverse DNS updates are disabled and
+DHCP_DDNS receives an update request containing a reverse DNS update.  The
+reverse update will not performed.
 
-% DHCP_DDNS_REVERSE_REMOVE_IO_ERROR DHCP_DDNS encountered an IO error sending a reverse mapping remove for FQDN %1 to DNS server %2
-This is an error message issued when a communication error occurs while
-DHCP_DDNS is carrying out a reverse address update.  The application will
-retry against the same server or others as appropriate.
+% DHCP_DDNS_RUN_ENTER application has entered the event loop
+This is a debug message issued when the Dhcp-Ddns application enters
+its run method.
 
-% DHCP_DDNS_REVERSE_REMOVE_RESP_CORRUPT DHCP_DDNS received a corrupt response from the DNS server, %1, while removing reverse address mapping for FQDN, %2
-This is an error message issued when the response received by DHCP_DDNS, to a
-update request to remove a reverse address,  is mangled or malformed.
-The application will retry against the same server or others as appropriate.
+% DHCP_DDNS_RUN_EXIT application is exiting the event loop
+This is a debug message issued when the Dhcp-Ddns exits the
+in event loop.
 
-% DHCP_DDNS_REVERSE_REMOVE_BAD_DNSCLIENT_STATUS DHCP_DDNS received an unknown DNSClient status: %1, while removing reverse address mapping for FQDN %2 to DNS server %3
-This is an error message issued when DNSClient returns an unrecognized status
-while DHCP_DDNS was removing a reverse address mapping.  The request will be
-aborted.  This is most likely a programmatic issue and should be reported.
+% DHCP_DDNS_SHUTDOWN application received shutdown command with args: %1
+This is informational message issued when the application has been instructed
+to shut down by the controller.
 
-% DHCP_DDNS_REMOVE_SUCCEEDED DHCP_DDNS successfully removed the DNS mapping addition for this request: %1
-This is a debug message issued after DHCP_DDNS has submitted DNS mapping
-removals which were received and accepted by an appropriate DNS server.
+% DHCP_DDNS_STARTING_TRANSACTION Transaction Key: %1
+This is a debug message issued when DHCP-DDNS has begun a transaction for
+a given request.
 
-% DHCP_DDNS_REMOVE_FAILED DHCP_DDNS failed attempting to make DNS mapping removals for this request: %1, event: %2
-This is an error message issued after DHCP_DDNS attempts to submit DNS mapping
-entry removals have failed.  The precise reason for the failure should be
-documented in preceding log entries.
+% DHCP_DDNS_STATE_MODEL_UNEXPECTED_ERROR application encountered an unexpected error while carrying out a NameChangeRequest: %1
+This is error message issued when the application fails to process a
+NameChangeRequest correctly. Some or all of the DNS updates requested as part
+of this update did not succeed. This is a programmatic error and should be
+reported.
 
-% DHCP_DDNS_STARTING_TRANSACTION Transaction Key: %1
+% DHCP_DDNS_TRANS_SEND_ERROR application encountered an unexpected error while attempting to send a DNS update: %1
+This is error message issued when the application is able to construct an update
+message but the attempt to send it suffered a unexpected error. This is most
+likely a programmatic error, rather than a communications issue. Some or all
+of the DNS updates requested as part of this request did not succeed.
 
-% DHCP_DDNS_UPDATE_REQUEST_SENT for transaction key: %1 to server: %2
+% DHCP_DDNS_UPDATE_REQUEST_SENT %1 for transaction key: %2 to server: %3
 This is a debug message issued when DHCP_DDNS sends a DNS request to a DNS
 server.
 

+ 43 - 16
src/bin/d2/d2_update_mgr.cc

@@ -132,34 +132,61 @@ D2UpdateMgr::makeTransaction(dhcp_ddns::NameChangeRequestPtr& next_ncr) {
             << key.toStr());
     }
 
+    int direction_count = 0;
     // If forward change is enabled, match to forward servers.
     DdnsDomainPtr forward_domain;
     if (next_ncr->isForwardChange()) {
-        bool matched = cfg_mgr_->matchForward(next_ncr->getFqdn(),
-                                             forward_domain);
-        // Could not find a match for forward DNS server. Log it and get out.
-        // This has the net affect of dropping the request on the floor.
-        if (!matched) {
-            LOG_ERROR(dctl_logger, DHCP_DDNS_NO_FWD_MATCH_ERROR)
-                      .arg(next_ncr->getFqdn());
-            return;
+        if (!cfg_mgr_->forwardUpdatesEnabled()) {
+            next_ncr->setForwardChange(false);
+            LOG_DEBUG(dctl_logger, DBGLVL_TRACE_DETAIL_DATA,
+                      DHCP_DDNS_FWD_REQUEST_IGNORED).arg(next_ncr->toText());
+        } else {
+            bool matched = cfg_mgr_->matchForward(next_ncr->getFqdn(),
+                                                  forward_domain);
+            // Could not find a match for forward DNS server. Log it and get
+            // out. This has the net affect of dropping the request on the
+            // floor.
+            if (!matched) {
+                LOG_ERROR(dctl_logger, DHCP_DDNS_NO_FWD_MATCH_ERROR)
+                          .arg(next_ncr->toText());
+                return;
+            }
+
+            ++direction_count;
         }
     }
 
     // If reverse change is enabled, match to reverse servers.
     DdnsDomainPtr reverse_domain;
     if (next_ncr->isReverseChange()) {
-        bool matched = cfg_mgr_->matchReverse(next_ncr->getIpAddress(),
-                                              reverse_domain);
-        // Could not find a match for reverse DNS server. Log it and get out.
-        // This has the net affect of dropping the request on the floor.
-        if (!matched) {
-            LOG_ERROR(dctl_logger, DHCP_DDNS_NO_REV_MATCH_ERROR)
-                      .arg(next_ncr->getIpAddress());
-            return;
+        if (!cfg_mgr_->reverseUpdatesEnabled()) {
+            next_ncr->setReverseChange(false);
+            LOG_DEBUG(dctl_logger, DBGLVL_TRACE_DETAIL_DATA,
+                      DHCP_DDNS_REV_REQUEST_IGNORED).arg(next_ncr->toText());
+        } else {
+            bool matched = cfg_mgr_->matchReverse(next_ncr->getIpAddress(),
+                                                  reverse_domain);
+            // Could not find a match for reverse DNS server. Log it and get
+            // out. This has the net affect of dropping the request on the
+            // floor.
+            if (!matched) {
+                LOG_ERROR(dctl_logger, DHCP_DDNS_NO_REV_MATCH_ERROR)
+                          .arg(next_ncr->toText());
+                return;
+            }
+
+            ++direction_count;
         }
     }
 
+    // If there is nothing to actually do, then the request falls on the floor.
+    // Should we log this?
+    if (!direction_count) {
+        LOG_DEBUG(dctl_logger, DBGLVL_TRACE_DETAIL_DATA,
+                  DHCP_DDNS_REQUEST_DROPPED).arg(next_ncr->toText());
+        return;
+    }
+
     // We matched to the required servers, so construct the transaction.
     // @todo If multi-threading is implemented, one would pass in an
     // empty IOServicePtr, rather than our instance value.  This would cause

+ 15 - 7
src/bin/d2/d2_update_mgr.h

@@ -147,13 +147,21 @@ protected:
 
     /// @brief Create a new transaction for the given request.
     ///
-    /// This method will attempt to match the request to a list of configured
-    /// DNS servers.  If a list of servers is found, it will instantiate a
-    /// transaction for it and add the transaction to the transaction list.
+    /// This method will attempt to match the request to suitable DNS servers.
+    /// If matching servers are found, it will instantiate a transaction for
+    /// the requests, add the transaction to the transaction list, and start
+    /// the transaction.
     ///
-    /// If no servers are found that match the request, this constitutes a
-    /// configuration error.  The error will be logged and the request will
-    /// be discarded.
+    /// If updates in a given direction are disabled requests for updates in
+    /// that direction will be ignored.  For example: If a request is received
+    /// which asks for updates both directions but only forward updates are
+    /// enabled; only the forward update will be attempted.  Effectively, the
+    /// request will be treated as if it only asked for forward updates.
+    ///
+    /// If updates in a given direction are enabled, and a request asks for
+    /// updates in that direction, failing to match the request to a list
+    /// of servers is an error which will be logged and the request will be
+    /// discarded.
     ///
     /// @param ncr the NameChangeRequest for which to create a transaction.
     ///
@@ -162,7 +170,7 @@ protected:
     void makeTransaction(isc::dhcp_ddns::NameChangeRequestPtr& ncr);
 
 public:
-    /// @brief Gets the UpdateMgr's IOService.
+    /// @brief Gets the D2UpdateMgr's IOService.
     ///
     /// @return returns a reference to the IOService
     const IOServicePtr& getIOService() {

+ 1 - 1
src/bin/d2/dns_client.cc

@@ -142,7 +142,7 @@ DNSClientImpl::operator()(asiodns::IOFetch::Result result) {
         try {
             response_->fromWire(response_buf);
 
-        } catch (const Exception& ex) {
+        } catch (const isc::Exception& ex) {
             status = DNSClient::INVALID_RESPONSE;
             LOG_DEBUG(dctl_logger, DBGLVL_TRACE_DETAIL,
                       DHCP_DDNS_INVALID_RESPONSE).arg(ex.what());

+ 7 - 7
src/bin/d2/nc_add.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2014 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
@@ -202,7 +202,7 @@ NameAddTransaction::addingFwdAddrsHandler() {
 
         // Call sendUpdate() to initiate the async send. Note it also sets
         // next event to NOP_EVT.
-        sendUpdate();
+        sendUpdate("Foward Add");
         break;
 
     case IO_COMPLETED_EVT: {
@@ -313,7 +313,7 @@ NameAddTransaction::replacingFwdAddrsHandler() {
 
         // Call sendUpdate() to initiate the async send. Note it also sets
         // next event to NOP_EVT.
-        sendUpdate();
+        sendUpdate("Forward Replace");
         break;
 
     case IO_COMPLETED_EVT: {
@@ -459,7 +459,7 @@ NameAddTransaction::replacingRevPtrsHandler() {
 
         // Call sendUpdate() to initiate the async send. Note it also sets
         // next event to NOP_EVT.
-        sendUpdate();
+        sendUpdate("Reverse Replace");
         break;
 
     case IO_COMPLETED_EVT: {
@@ -539,7 +539,7 @@ void
 NameAddTransaction::processAddOkHandler() {
     switch(getNextEvent()) {
     case UPDATE_OK_EVT:
-        LOG_DEBUG(dctl_logger, DBGLVL_TRACE_DETAIL, DHCP_DDNS_ADD_SUCCEEDED)
+        LOG_INFO(dctl_logger, DHCP_DDNS_ADD_SUCCEEDED)
                   .arg(getNcr()->toText());
         setNcrStatus(dhcp_ddns::ST_COMPLETED);
         endModel();
@@ -556,9 +556,9 @@ NameAddTransaction::processAddFailedHandler() {
     switch(getNextEvent()) {
     case UPDATE_FAILED_EVT:
     case NO_MORE_SERVERS_EVT:
-        LOG_ERROR(dctl_logger, DHCP_DDNS_ADD_FAILED).arg(getNcr()->toText())
-        .arg(getEventLabel(getNextEvent()));
         setNcrStatus(dhcp_ddns::ST_FAILED);
+        LOG_ERROR(dctl_logger, DHCP_DDNS_ADD_FAILED)
+                  .arg(transactionOutcomeString());
         endModel();
         break;
     default:

+ 23 - 18
src/bin/d2/nc_remove.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2014 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
@@ -207,16 +207,18 @@ NameRemoveTransaction::removingFwdAddrsHandler() {
 
         // Call sendUpdate() to initiate the async send. Note it also sets
         // next event to NOP_EVT.
-        sendUpdate();
+        sendUpdate("Forward A/AAAA Remove");
         break;
 
     case IO_COMPLETED_EVT: {
         switch (getDnsUpdateStatus()) {
         case DNSClient::SUCCESS: {
             // We successfully received a response packet from the server.
+            // The RCODE will be based on a value-dependent RRset search,
+            // see RFC 2136 section 3.2.3/3.2.4.
             const dns::Rcode& rcode = getDnsUpdateResponse()->getRcode();
             if ((rcode == dns::Rcode::NOERROR()) ||
-                (rcode == dns::Rcode::NXDOMAIN())) {
+                (rcode == dns::Rcode::NXRRSET())) {
                 // We were able to remove it or it wasn't there, now we
                 // need to remove any other RRs for this FQDN.
                 transition(REMOVING_FWD_RRS_ST, UPDATE_OK_EVT);
@@ -311,21 +313,21 @@ NameRemoveTransaction::removingFwdRRsHandler() {
 
         // Call sendUpdate() to initiate the async send. Note it also sets
         // next event to NOP_EVT.
-        sendUpdate();
+        sendUpdate("Forward RR Remove");
         break;
 
     case IO_COMPLETED_EVT: {
         switch (getDnsUpdateStatus()) {
         case DNSClient::SUCCESS: {
             // We successfully received a response packet from the server.
+            // The RCODE will be based on a value-dependent RRset search,
+            // see RFC 2136 section 3.2.3/3.2.4.
             const dns::Rcode& rcode = getDnsUpdateResponse()->getRcode();
-            // @todo Not sure if NXDOMAIN is ok here, but I think so.
-            // A Rcode of NXDOMAIN would mean there are no RRs for the FQDN,
-            // which is fine.  We were asked to delete them, they are not there
-            // so all is well.
             if ((rcode == dns::Rcode::NOERROR()) ||
-                (rcode == dns::Rcode::NXDOMAIN())) {
-                // We were able to remove the forward mapping. Mark it as done.
+                (rcode == dns::Rcode::NXRRSET())) {
+                // We were able to remove them or they were not there (
+                // Rcode of NXRRSET means there are no matching RRsets).
+                // In either case, we consider it success and mark it as done.
                 setForwardChangeCompleted(true);
 
                 // If request calls for reverse update then do that next,
@@ -464,18 +466,21 @@ NameRemoveTransaction::removingRevPtrsHandler() {
 
         // Call sendUpdate() to initiate the async send. Note it also sets
         // next event to NOP_EVT.
-        sendUpdate();
+        sendUpdate("Reverse Remove");
         break;
 
     case IO_COMPLETED_EVT: {
         switch (getDnsUpdateStatus()) {
         case DNSClient::SUCCESS: {
             // We successfully received a response packet from the server.
+            // The RCODE will be based on a value-dependent RRset search,
+            // see RFC 2136 section 3.2.3/3.2.4.
             const dns::Rcode& rcode = getDnsUpdateResponse()->getRcode();
             if ((rcode == dns::Rcode::NOERROR()) ||
-                (rcode == dns::Rcode::NXDOMAIN())) {
-                // We were able to update the reverse mapping. Mark it as done.
-                // @todo For now we are also treating NXDOMAIN as success.
+                (rcode == dns::Rcode::NXRRSET())) {
+                // We were able to remove the reverse mapping or they were
+                // not there (Rcode of NXRRSET means there are no matching
+                // RRsets). In either case, mark it as done.
                 setReverseChangeCompleted(true);
                 transition(PROCESS_TRANS_OK_ST, UPDATE_OK_EVT);
             } else {
@@ -547,8 +552,8 @@ void
 NameRemoveTransaction::processRemoveOkHandler() {
     switch(getNextEvent()) {
     case UPDATE_OK_EVT:
-        LOG_DEBUG(dctl_logger, DBGLVL_TRACE_DETAIL, DHCP_DDNS_REMOVE_SUCCEEDED)
-                  .arg(getNcr()->toText());
+        LOG_INFO(dctl_logger, DHCP_DDNS_REMOVE_SUCCEEDED)
+                .arg(getNcr()->toText());
         setNcrStatus(dhcp_ddns::ST_COMPLETED);
         endModel();
         break;
@@ -565,9 +570,9 @@ NameRemoveTransaction::processRemoveFailedHandler() {
     case UPDATE_FAILED_EVT:
     case NO_MORE_SERVERS_EVT:
     case SERVER_IO_ERROR_EVT:
-        LOG_ERROR(dctl_logger, DHCP_DDNS_REMOVE_FAILED).arg(getNcr()->toText())
-        .arg(getEventLabel(getNextEvent()));
         setNcrStatus(dhcp_ddns::ST_FAILED);
+        LOG_ERROR(dctl_logger, DHCP_DDNS_REMOVE_FAILED)
+                  .arg(transactionOutcomeString());
         endModel();
         break;
     default:

+ 64 - 4
src/bin/d2/nc_trans.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2014 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
@@ -16,6 +16,8 @@
 #include <d2/nc_trans.h>
 #include <dns/rdata.h>
 
+#include <sstream>
+
 namespace isc {
 namespace d2 {
 
@@ -94,18 +96,75 @@ NameChangeTransaction::operator()(DNSClient::Status status) {
     // set to indicate IO completed.
     // runModel is exception safe so we are good to call it here.
     // It won't exit until we hit the next IO wait or the state model ends.
+    setDnsUpdateStatus(status);
     LOG_DEBUG(dctl_logger, DBGLVL_TRACE_DETAIL,
               DHCP_DDNS_UPDATE_RESPONSE_RECEIVED)
               .arg(getTransactionKey().toStr())
               .arg(current_server_->toText())
-              .arg(status);
+              .arg(responseString());
 
-    setDnsUpdateStatus(status);
     runModel(IO_COMPLETED_EVT);
 }
 
+std::string
+NameChangeTransaction::responseString() const {
+    std::ostringstream stream;
+    switch (getDnsUpdateStatus()) {
+        case DNSClient::SUCCESS:
+            stream << "SUCCESS, rcode: ";
+            if (getDnsUpdateResponse()) {
+                 stream << getDnsUpdateResponse()->getRcode().toText();
+            } else {
+                stream << " update response is NULL";
+            }
+            break;
+        case DNSClient::TIMEOUT:
+            stream << "TIMEOUT";
+            break;
+        case DNSClient::IO_STOPPED:
+            stream << "IO_STOPPED";
+            break;
+        case DNSClient::INVALID_RESPONSE:
+            stream << "INVALID_RESPONSE";
+            break;
+        case DNSClient::OTHER:
+            stream << "OTHER";
+            break;
+        default:
+            stream << "UKNOWNN("
+                   << static_cast<int>(getDnsUpdateStatus()) << ")";
+            break;
+
+    }
+
+    return (stream.str());
+}
+
+std::string
+NameChangeTransaction::transactionOutcomeString() const {
+    std::ostringstream stream;
+    stream << "Status: " << (getNcrStatus() == dhcp_ddns::ST_COMPLETED
+                             ? "Completed, " : "Failed, ")
+           << "Event: " << getEventLabel(getNextEvent()) << ", ";
+
+    if (ncr_->isForwardChange()) {
+        stream << " Forward change:" << (getForwardChangeCompleted()
+                                         ? " completed, " : " failed, ");
+    }
+
+    if (ncr_->isReverseChange()) {
+        stream << " Reverse change:" << (getReverseChangeCompleted()
+                                          ? " completed, " : " failed, ");
+    }
+
+    stream << " request: " << ncr_->toText();
+    return (stream.str());
+}
+
+
 void
-NameChangeTransaction::sendUpdate(bool /* use_tsig_ */) {
+NameChangeTransaction::sendUpdate(const std::string& comment,
+                                  bool /* use_tsig_ */) {
     try {
         ++update_attempts_;
         // @todo add logic to add/replace TSIG key info in request if
@@ -122,6 +181,7 @@ NameChangeTransaction::sendUpdate(bool /* use_tsig_ */) {
         postNextEvent(NOP_EVT);
         LOG_DEBUG(dctl_logger, DBGLVL_TRACE_DETAIL,
                   DHCP_DDNS_UPDATE_REQUEST_SENT)
+                  .arg(comment)
                   .arg(getTransactionKey().toStr())
                   .arg(current_server_->toText());
     } catch (const std::exception& ex) {

+ 21 - 1
src/bin/d2/nc_trans.h

@@ -207,12 +207,14 @@ protected:
     /// currently selected server.  Since the send is asynchronous, the method
     /// posts NOP_EVT as the next event and then returns.
     ///
+    /// @param comment text to include in log detail
     /// @param use_tsig True if the update should be include a TSIG key. This
     /// is not yet implemented.
     ///
     /// If an exception occurs it will be logged and and the transaction will
     /// be failed.
-    virtual void sendUpdate(bool use_tsig = false);
+    virtual void sendUpdate(const std::string& comment = "",
+                            bool use_tsig = false);
 
     /// @brief Adds events defined by NameChangeTransaction to the event set.
     ///
@@ -401,6 +403,24 @@ protected:
     /// the RData cannot be added to the given RRset.
     void addPtrRdata(dns::RRsetPtr& rrset);
 
+    /// @brief Returns a string version of the current response status and rcode
+    ///
+    /// Renders a string containing the a text label current DNS update status
+    /// and RCODE (if status is DNSClient::SUCCESS)
+    ///
+    /// @return std::string containing constructed text
+    std::string responseString() const;
+
+    /// @brief Returns a string version of transaction outcome.
+    ///
+    /// Renders a string containing summarizes the outcome of the
+    /// transaction. The information includes the overall status,
+    /// the last event, whether not forward and reverse changes were
+    /// done, as well as the NCR serviced.
+    ///
+    /// @return std::string containing constructed text
+    std::string transactionOutcomeString() const;
+
 public:
     /// @brief Fetches the NameChangeRequest for this transaction.
     ///

+ 20 - 3
src/bin/d2/tests/d2_cfg_mgr_unittests.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2013  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2014 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
@@ -1008,6 +1008,10 @@ TEST_F(D2CfgMgrTest, fullConfig) {
         EXPECT_EQ(3, count);
     }
 
+    // Test directional update flags.
+    EXPECT_TRUE(cfg_mgr_->forwardUpdatesEnabled());
+    EXPECT_TRUE(cfg_mgr_->reverseUpdatesEnabled());
+
     // Verify that parsing the exact same configuration a second time
     // does not cause a duplicate value errors. 
     answer_ = cfg_mgr_->parseConfig(config_set_);
@@ -1061,11 +1065,19 @@ TEST_F(D2CfgMgrTest, forwardMatch) {
     D2CfgContextPtr context;
     ASSERT_NO_THROW(context = cfg_mgr_->getD2CfgContext());
 
+    // Test directional update flags.
+    EXPECT_TRUE(cfg_mgr_->forwardUpdatesEnabled());
+    EXPECT_FALSE(cfg_mgr_->reverseUpdatesEnabled());
+
     DdnsDomainPtr match;
     // Verify that an exact match works.
     EXPECT_TRUE(cfg_mgr_->matchForward("tmark.org", match));
     EXPECT_EQ("tmark.org", match->getName());
 
+    // Verify that search is case insensisitive.
+    EXPECT_TRUE(cfg_mgr_->matchForward("TMARK.ORG", match));
+    EXPECT_EQ("tmark.org", match->getName());
+
     // Verify that an exact match works.
     EXPECT_TRUE(cfg_mgr_->matchForward("one.tmark.org", match));
     EXPECT_EQ("one.tmark.org", match->getName());
@@ -1207,7 +1219,8 @@ TEST_F(D2CfgMgrTest, matchReverse) {
                         "  \"dns_servers\" : [ "
                         "  { \"ip_address\": \"127.0.0.1\" } "
                         "  ] }, "
-                        "{ \"name\": \"2.0.3.0.8.B.D.0.1.0.0.2.ip6.arpa.\" , "
+                        // Note mixed case to test case insensitivity.
+                        "{ \"name\": \"2.0.3.0.8.b.d.0.1.0.0.2.IP6.ARPA.\" , "
                         "  \"dns_servers\" : [ "
                         "  { \"ip_address\": \"127.0.0.1\" } "
                         "  ] },"
@@ -1227,6 +1240,10 @@ TEST_F(D2CfgMgrTest, matchReverse) {
     D2CfgContextPtr context;
     ASSERT_NO_THROW(context = cfg_mgr_->getD2CfgContext());
 
+    // Test directional update flags.
+    EXPECT_FALSE(cfg_mgr_->forwardUpdatesEnabled());
+    EXPECT_TRUE(cfg_mgr_->reverseUpdatesEnabled());
+
     DdnsDomainPtr match;
 
     // Verify an exact match.
@@ -1247,7 +1264,7 @@ TEST_F(D2CfgMgrTest, matchReverse) {
 
     // Verify a IPv6 match.
     EXPECT_TRUE(cfg_mgr_->matchReverse("2001:db8:302:99::",match));
-    EXPECT_EQ("2.0.3.0.8.B.D.0.1.0.0.2.ip6.arpa.", match->getName());
+    EXPECT_EQ("2.0.3.0.8.b.d.0.1.0.0.2.IP6.ARPA.", match->getName());
 
     // Verify a IPv6 wild card match.
     EXPECT_TRUE(cfg_mgr_->matchReverse("2001:db8:99:302::",match));

+ 103 - 0
src/bin/d2/tests/d2_update_mgr_unittests.cc

@@ -347,6 +347,109 @@ TEST_F(D2UpdateMgrTest, transactionList) {
     EXPECT_NO_THROW(update_mgr_->removeTransaction(ncr->getDhcid()));
 }
 
+/// @brief Checks transaction creation when both update directions are enabled.
+/// Verifies that when both directions are enabled and servers are matched to
+/// the request, that the transaction is created with both directions turned on.
+TEST_F(D2UpdateMgrTest, bothEnabled) {
+    // Grab a canned request for test purposes.
+    NameChangeRequestPtr& ncr = canned_ncrs_[0];
+    ncr->setReverseChange(true);
+
+    // Verify we are requesting both directions.
+    ASSERT_TRUE(ncr->isForwardChange());
+    ASSERT_TRUE(ncr->isReverseChange());
+
+    // Verify both both directions are enabled.
+    ASSERT_TRUE(cfg_mgr_->forwardUpdatesEnabled());
+    ASSERT_TRUE(cfg_mgr_->reverseUpdatesEnabled());
+
+    // Attempt to make a transaction.
+    ASSERT_NO_THROW(update_mgr_->makeTransaction(ncr));
+
+    // Verify we create a transaction with both directions turned on.
+    EXPECT_EQ(1, update_mgr_->getTransactionCount());
+    EXPECT_TRUE(ncr->isForwardChange());
+    EXPECT_TRUE(ncr->isReverseChange());
+}
+
+/// @brief Checks transaction creation when reverse updates are disabled.
+/// Verifies that when reverse updates are disabled, and there matching forward
+/// servers, that the transaction is still created but with only the forward
+/// direction turned on.
+TEST_F(D2UpdateMgrTest, reverseDisable) {
+    // Make a NCR which requests both directions.
+    NameChangeRequestPtr& ncr = canned_ncrs_[0];
+    ncr->setReverseChange(true);
+
+    // Wipe out forward domain list.
+    DdnsDomainMapPtr emptyDomains(new DdnsDomainMap());
+    cfg_mgr_->getD2CfgContext()->getReverseMgr()->setDomains(emptyDomains);
+
+    // Verify enable methods are correct.
+    ASSERT_TRUE(cfg_mgr_->forwardUpdatesEnabled());
+    ASSERT_FALSE(cfg_mgr_->reverseUpdatesEnabled());
+
+    // Attempt to make a transaction.
+    ASSERT_NO_THROW(update_mgr_->makeTransaction(ncr));
+
+    // Verify we create a transaction with only forward turned on.
+    EXPECT_EQ(1, update_mgr_->getTransactionCount());
+    EXPECT_TRUE(ncr->isForwardChange());
+    EXPECT_FALSE(ncr->isReverseChange());
+}
+
+/// @brief Checks transaction creation when forward updates are disabled.
+/// Verifies that when forward updates are disabled, and there matching reverse
+/// servers, that the transaction is still created but with only the reverse
+/// direction turned on.
+TEST_F(D2UpdateMgrTest, forwardDisabled) {
+    // Make a NCR which requests both directions.
+    NameChangeRequestPtr& ncr = canned_ncrs_[0];
+    ncr->setReverseChange(true);
+
+    // Wipe out forward domain list.
+    DdnsDomainMapPtr emptyDomains(new DdnsDomainMap());
+    cfg_mgr_->getD2CfgContext()->getForwardMgr()->setDomains(emptyDomains);
+
+    // Verify enable methods are correct.
+    ASSERT_FALSE(cfg_mgr_->forwardUpdatesEnabled());
+    ASSERT_TRUE(cfg_mgr_->reverseUpdatesEnabled());
+
+    // Attempt to make a transaction.
+    ASSERT_NO_THROW(update_mgr_->makeTransaction(ncr));
+
+    // Verify we create a transaction with only reverse turned on.
+    EXPECT_EQ(1, update_mgr_->getTransactionCount());
+    EXPECT_FALSE(ncr->isForwardChange());
+    EXPECT_TRUE(ncr->isReverseChange());
+}
+
+
+/// @brief Checks transaction creation when neither update direction is enabled.
+/// Verifies that transactions are not created when both forward and reverse
+/// directions are disabled.
+TEST_F(D2UpdateMgrTest, bothDisabled) {
+    // Grab a canned request for test purposes.
+    NameChangeRequestPtr& ncr = canned_ncrs_[0];
+    ncr->setReverseChange(true);
+    TransactionList::iterator pos;
+
+    // Wipe out both forward and reverse domain lists.
+    DdnsDomainMapPtr emptyDomains(new DdnsDomainMap());
+    cfg_mgr_->getD2CfgContext()->getForwardMgr()->setDomains(emptyDomains);
+    cfg_mgr_->getD2CfgContext()->getReverseMgr()->setDomains(emptyDomains);
+
+    // Verify enable methods are correct.
+    EXPECT_FALSE(cfg_mgr_->forwardUpdatesEnabled());
+    EXPECT_FALSE(cfg_mgr_->reverseUpdatesEnabled());
+
+    // Attempt to make a transaction.
+    ASSERT_NO_THROW(update_mgr_->makeTransaction(ncr));
+
+    // Verify that do not create a transaction.
+    EXPECT_EQ(0, update_mgr_->getTransactionCount());
+}
+
 /// @brief Tests D2UpdateManager's checkFinishedTransactions method.
 /// This test verifies that:
 /// 1. Completed transactions are removed from the transaction list.

+ 4 - 3
src/bin/d2/tests/nc_add_unittests.cc

@@ -54,10 +54,11 @@ public:
     /// It will also simulate an exception-based failure of sendUpdate, if
     /// the simulate_send_exception_ flag is true.
     ///
-    /// @param use_tsig_ Parameter is unused, but present in the base class
-    /// method.
+    /// @param comment Parameter is unused, but present in base class method.
+    /// @param use_tsig_ Parameter is unused, but present in base class method.
     ///
-    virtual void sendUpdate(bool /* use_tsig_ = false */) {
+    virtual void sendUpdate(const std::string& /*comment*/,
+                            bool /* use_tsig_ = false */) {
         if (simulate_send_exception_) {
             // Make the flag a one-shot by resetting it.
             simulate_send_exception_ = false;

+ 11 - 10
src/bin/d2/tests/nc_remove_unittests.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2013  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2014  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
@@ -55,10 +55,11 @@ public:
     /// It will also simulate an exception-based failure of sendUpdate, if
     /// the simulate_send_exception_ flag is true.
     ///
-    /// @param use_tsig_ Parameter is unused, but present in the base class
-    /// method.
+    /// @param comment Parameter is unused, but present in base class method
+    /// @param use_tsig Parameter is unused, but present in base class method.
     ///
-    virtual void sendUpdate(bool /* use_tsig_ = false */) {
+    virtual void sendUpdate(const std::string& /* comment */,
+                            bool /* use_tsig = false */) {
         if (simulate_send_exception_) {
             // Make the flag a one-shot by resetting it.
             simulate_send_exception_ = false;
@@ -602,8 +603,8 @@ TEST_F(NameRemoveTransactionTest, removingFwdAddrsHandler_FqdnNotInUse) {
     // Run removingFwdAddrsHandler to construct and send the request.
     EXPECT_NO_THROW(name_remove->removingFwdAddrsHandler());
 
-    // Simulate receiving a FQDN not in use response.
-    name_remove->fakeResponse(DNSClient::SUCCESS, dns::Rcode::NXDOMAIN());
+    // Simulate receiving a RRSET does not exist.
+    name_remove->fakeResponse(DNSClient::SUCCESS, dns::Rcode::NXRRSET());
 
     // Run removingFwdAddrsHandler again to process the response.
     EXPECT_NO_THROW(name_remove->removingFwdAddrsHandler());
@@ -962,8 +963,8 @@ TEST_F(NameRemoveTransactionTest, removingFwdRRsHandler_FqdnNotInUse) {
     // Run removingFwdRRsHandler to construct and send the request.
     EXPECT_NO_THROW(name_remove->removingFwdRRsHandler());
 
-    // Simulate receiving a FQDN not in use response.
-    name_remove->fakeResponse(DNSClient::SUCCESS, dns::Rcode::NXDOMAIN());
+    // Simulate receiving a RRSET does not exist response.
+    name_remove->fakeResponse(DNSClient::SUCCESS, dns::Rcode::NXRRSET());
 
     // Run removingFwdRRsHandler again to process the response.
     EXPECT_NO_THROW(name_remove->removingFwdRRsHandler());
@@ -1339,8 +1340,8 @@ TEST_F(NameRemoveTransactionTest, removingRevPtrsHandler_FqdnNotInUse) {
     EXPECT_EQ(StateModel::NOP_EVT,
               name_remove->getNextEvent());
 
-    // Simulate receiving a FQDN not in use response.
-    name_remove->fakeResponse(DNSClient::SUCCESS, dns::Rcode::NXDOMAIN());
+    // Simulate receiving a RRSET does not exist.
+    name_remove->fakeResponse(DNSClient::SUCCESS, dns::Rcode::NXRRSET());
 
     // Run removingRevPtrsHandler again to process the response.
     EXPECT_NO_THROW(name_remove->removingRevPtrsHandler());

+ 81 - 0
src/bin/d2/tests/nc_trans_unittests.cc

@@ -262,6 +262,8 @@ public:
     using NameChangeTransaction::addLeaseAddressRdata;
     using NameChangeTransaction::addDhcidRdata;
     using NameChangeTransaction::addPtrRdata;
+    using NameChangeTransaction::responseString;
+    using NameChangeTransaction::transactionOutcomeString;
 };
 
 // Declare them so Gtest can see them.
@@ -507,8 +509,87 @@ TEST_F(NameChangeTransactionTest, dnsUpdateResponseAccessors) {
 
     // Should be empty again.
     EXPECT_FALSE(name_change->getDnsUpdateResponse());
+
+}
+
+/// @brief Tests responseString method.
+TEST_F(NameChangeTransactionTest, responseString) {
+    // Create a transaction.
+    NameChangeStubPtr name_change;
+    ASSERT_NO_THROW(name_change = makeCannedTransaction());
+
+    // Make sure it is safe to call when status says success but there
+    // is no update response.
+    ASSERT_NO_THROW(name_change->setDnsUpdateStatus(DNSClient::SUCCESS));
+    EXPECT_EQ("SUCCESS, rcode:  update response is NULL",
+              name_change->responseString());
+
+    // Create a response. (We use an OUTBOUND message so we can set RCODE)
+    D2UpdateMessagePtr resp;
+    ASSERT_NO_THROW(resp.reset(new D2UpdateMessage(D2UpdateMessage::OUTBOUND)));
+    ASSERT_NO_THROW(name_change->setDnsUpdateResponse(resp));
+
+    // Make sure we decode Rcode when status is successful.
+    ASSERT_NO_THROW(resp->setRcode(dns::Rcode::NXDOMAIN()));
+    EXPECT_EQ("SUCCESS, rcode: NXDOMAIN", name_change->responseString());
+
+    // Test all of the non-success values for status.
+    ASSERT_NO_THROW(name_change->setDnsUpdateStatus(DNSClient::TIMEOUT));
+    EXPECT_EQ("TIMEOUT", name_change->responseString());
+
+    ASSERT_NO_THROW(name_change->setDnsUpdateStatus(DNSClient::IO_STOPPED));
+    EXPECT_EQ("IO_STOPPED", name_change->responseString());
+
+    ASSERT_NO_THROW(name_change->setDnsUpdateStatus(DNSClient::
+                                                    INVALID_RESPONSE));
+    EXPECT_EQ("INVALID_RESPONSE", name_change->responseString());
+
+    ASSERT_NO_THROW(name_change->setDnsUpdateStatus(DNSClient::OTHER));
+    EXPECT_EQ("OTHER", name_change->responseString());
 }
 
+/// @brief Tests transactionOutcomeString method.
+TEST_F(NameChangeTransactionTest, transactionOutcomeString) {
+    // Create a transaction.
+    NameChangeStubPtr name_change;
+    dhcp_ddns::NameChangeRequestPtr ncr;
+    ASSERT_NO_THROW(name_change = makeCannedTransaction());
+    ncr = name_change->getNcr();
+
+    // Check case of failed transaction in both directions
+    std::string exp_str("Status: Failed, Event: UNDEFINED,  Forward change:"
+                        " failed,  Reverse change: failed,  request: ");
+    exp_str += ncr->toText();
+
+    std::string tstring = name_change->transactionOutcomeString();
+    std::cout << "tstring is: [" << tstring << "]" << std::endl;
+
+    EXPECT_EQ(exp_str, name_change->transactionOutcomeString());
+
+    // Check case of success all around
+    name_change->setNcrStatus(dhcp_ddns::ST_COMPLETED);
+    name_change->setForwardChangeCompleted(true);
+    name_change->setReverseChangeCompleted(true);
+
+    exp_str = "Status: Completed, Event: UNDEFINED,  Forward change: completed,"
+              "  Reverse change: completed,  request: " + ncr->toText();
+    EXPECT_EQ(exp_str, name_change->transactionOutcomeString());
+
+    // Check case of success, with no forward change
+    name_change->setNcrStatus(dhcp_ddns::ST_COMPLETED);
+    ncr->setForwardChange(false);
+    exp_str = "Status: Completed, Event: UNDEFINED, "
+              " Reverse change: completed,  request: " + ncr->toText();
+    EXPECT_EQ(exp_str, name_change->transactionOutcomeString());
+
+    // Check case of success, with no reverse change
+    name_change->setNcrStatus(dhcp_ddns::ST_COMPLETED);
+    ncr->setForwardChange(true);
+    ncr->setReverseChange(false);
+    exp_str = "Status: Completed, Event: UNDEFINED, "
+              " Forward change: completed,  request: " + ncr->toText();
+    EXPECT_EQ(exp_str, name_change->transactionOutcomeString());
+}
 
 /// @brief Tests event and state dictionary construction and verification.
 TEST_F(NameChangeTransactionTest, dictionaryCheck) {

+ 16 - 2
src/bin/dhcp4/ctrl_dhcp4_srv.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2014 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
@@ -114,6 +114,16 @@ ControlledDhcpv4Srv::dhcp4ConfigHandler(ConstElementPtr new_config) {
         return (answer);
     }
 
+    // Server will start DDNS communications if its enabled.
+    try {
+        server_->startD2();
+    } catch (const std::exception& ex) {
+        std::ostringstream err;
+        err << "error starting DHCP_DDNS client "
+                " after server reconfiguration: " << ex.what();
+        return (isc::config::createAnswer(1, err.str()));
+    }
+
     // Configuration may change active interfaces. Therefore, we have to reopen
     // sockets according to new configuration. This operation is not exception
     // safe and we really don't want to emit exceptions to the callback caller.
@@ -211,11 +221,15 @@ void ControlledDhcpv4Srv::establishSession() {
 
     try {
         configureDhcp4Server(*this, config_session_->getFullConfig());
+
+        // Server will start DDNS communications if its enabled.
+        server_->startD2();
+
         // Configuration may disable or enable interfaces so we have to
         // reopen sockets according to new configuration.
         openActiveSockets(getPort(), useBroadcast());
 
-    } catch (const DhcpConfigError& ex) {
+    } catch (const std::exception& ex) {
         LOG_ERROR(dhcp4_logger, DHCP4_CONFIG_LOAD_FAIL).arg(ex.what());
 
     }

+ 62 - 57
src/bin/dhcp4/dhcp4_messages.mes

@@ -20,29 +20,29 @@ to receive DHCPv4 traffic. IPv4 socket on this interface will be opened once
 Interface Manager starts up procedure of opening sockets.
 
 % DHCP4_CCSESSION_STARTED control channel session started on socket %1
-A debug message issued during startup after the IPv4 DHCP server has
+A debug message issued during startup after the DHCPv4 server has
 successfully established a session with the BIND 10 control channel.
 
 % DHCP4_CCSESSION_STARTING starting control channel session, specfile: %1
-This debug message is issued just before the IPv4 DHCP server attempts
+This debug message is issued just before the DHCPv4 server attempts
 to establish a session with the BIND 10 control channel.
 
-% DHCP4_CLIENT_NAME_PROC_FAIL failed to process the fqdn or hostname sent by a client: %1
-This debug message is issued when the DHCP server was unable to process the
-FQDN or Hostname option sent by a client. This is likely because the client's
-name was malformed or due to internal server error.
+% DHCP4_CLASS_ASSIGNED client packet has been assigned to the following class(es): %1
+This debug message informs that incoming packet has been assigned to specified
+class or classes. This is a norma
 
 % DHCP4_CLASS_PROCESSING_FAILED client class specific processing failed
 This debug message means that the server processing that is unique for each
 client class has reported a failure. The response packet will not be sent.
 
-% DHCP4_CLASS_ASSIGNED client packet has been assigned to the following class(es): %1
-This debug message informs that incoming packet has been assigned to specified
-class or classes. This is a norma
+% DHCP4_CLIENT_NAME_PROC_FAIL failed to process the fqdn or hostname sent by a client: %1
+This debug message is issued when the DHCP server was unable to process the
+FQDN or Hostname option sent by a client. This is likely because the client's
+name was malformed or due to internal server error.
 
 % DHCP4_COMMAND_RECEIVED received command %1, arguments: %2
 A debug message listing the command (and possible arguments) received
-from the BIND 10 control system by the IPv4 DHCP server.
+from the BIND 10 control system by the DHCPv4 server.
 
 % DHCP4_CONFIG_COMPLETE DHCPv4 server has completed configuration: %1
 This is an informational message announcing the successful processing of a
@@ -70,7 +70,7 @@ configuration. That happens at start up and also when a server configuration
 change is committed by the administrator.
 
 % DHCP4_CONFIG_UPDATE updated configuration received: %1
-A debug message indicating that the IPv4 DHCP server has received an
+A debug message indicating that the DHCPv4 server has received an
 updated configuration from the BIND 10 configuration system.
 
 % DHCP4_DB_BACKEND_STARTED lease database started (type: %1, name: %2)
@@ -78,6 +78,11 @@ This informational message is printed every time DHCPv4 server is started
 and gives both the type and name of the database being used to store
 lease and other information.
 
+% DHCP4_DDNS_REQUEST_SEND_FAILED failed sending a request to b10-dhcp-ddns, error: %1,  ncr: %2
+This error message indicates that DHCP4 server attempted to send a DDNS
+update reqeust to the DHCP-DDNS server.  This is most likely a configuration or
+networking error.
+
 % DHCP4_DEACTIVATE_INTERFACE deactivate interface %1
 This message is printed when DHCPv4 server disables an interface from being
 used to receive DHCPv4 traffic. Sockets on this interface will not be opened
@@ -93,6 +98,11 @@ This debug message is issued when the server received an empty Hostname option
 from a client. Server does not process empty Hostname options and therefore
 option is skipped.
 
+% DHCP4_HOOKS_LIBS_RELOAD_FAIL reload of hooks libraries failed
+A "libreload" command was issued to reload the hooks libraries but for
+some reason the reload failed.  Other error messages issued from the
+hooks framework will indicate the nature of the problem.
+
 % DHCP4_HOOK_BUFFER_RCVD_SKIP received DHCPv4 buffer was dropped because a callout set the skip flag.
 This debug message is printed when a callout installed on buffer4_receive
 hook point set the skip flag. For this particular hook point, the
@@ -130,23 +140,6 @@ point, the setting of the flag instructs the server not to choose a
 subnet, an action that severely limits further processing; the server
 will be only able to offer global options - no addresses will be assigned.
 
-% DHCP4_HOOKS_LIBS_RELOAD_FAIL reload of hooks libraries failed
-A "libreload" command was issued to reload the hooks libraries but for
-some reason the reload failed.  Other error messages issued from the
-hooks framework will indicate the nature of the problem.
-
-% DHCP4_UNRECOGNIZED_RCVD_PACKET_TYPE received message (transaction id %1) has unrecognized type %2 in option 53
-This debug message indicates that the message type carried in DHCPv4 option
-53 is unrecognized by the server. The valid message types are listed
-on the IANA website: http://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml#message-type-53.
-The message will not be processed by the server.
-
-% DHCP4_UNSUPPORTED_RCVD_PACKET_TYPE received message (transaction id %1), having type %2 is not supported
-This debug message indicates that the message type carried in DHCPv4 option
-53 is valid but the message will not be processed by the server. This includes
-messages being normally sent by the server to the client, such as Offer, ACK,
-NAK etc.
-
 % DHCP4_LEASE_ADVERT lease %1 advertised (client client-id %2, hwaddr %3)
 This debug message indicates that the server successfully advertised
 a lease. It is up to the client to choose one server out of othe advertised
@@ -182,6 +175,10 @@ new DNS records for the lease being acquired or to update existing records
 for the renewed lease. The reason for the failure is printed in the logged
 message.
 
+% DHCP4_NOT_RUNNING DHCPv4 server is not running
+A warning message is issued when an attempt is made to shut down the
+DHCPv4 server but it is not running.
+
 % DHCP4_NO_SOCKETS_OPEN no interface configured to listen to DHCP traffic
 This warning message is issued when current server configuration specifies
 no interfaces that server should listen on, or specified interfaces are not
@@ -194,14 +191,18 @@ which this message has been received. The IPv4 address assigned on this
 interface must belong to one of the configured subnets. Otherwise
 received message is dropped.
 
-% DHCP4_NOT_RUNNING IPv4 DHCP server is not running
-A warning message is issued when an attempt is made to shut down the
-IPv4 DHCP server but it is not running.
-
 % DHCP4_OPEN_SOCKET opening sockets on port %1
-A debug message issued during startup, this indicates that the IPv4 DHCP
+A debug message issued during startup, this indicates that the DHCPv4
 server is about to open sockets on the specified port.
 
+% DHCP4_OPEN_SOCKET_FAIL failed to create socket: %1
+A warning message issued when IfaceMgr fails to open and bind a socket. The reason
+for the failure is appended as an argument of the log message.
+
+% DHCP4_PACKET_DROP_NO_TYPE packet received on interface %1 dropped, because of missing msg-type option
+This is a debug message informing that incoming DHCPv4 packet did not
+have mandatory DHCP message type option and thus was dropped.
+
 % DHCP4_PACKET_NOT_FOR_US received DHCPv4 message (transid=%1, iface=%2) dropped because it contains foreign server identifier
 This debug message is issued when received DHCPv4 message is dropped because
 it is addressed to a different server, i.e. a server identifier held by
@@ -209,12 +210,8 @@ this message doesn't match the identifier used by our server. The arguments
 of this message hold the name of the transaction id and interface on which
 the message has been received.
 
-% DHCP4_OPEN_SOCKET_FAIL failed to create socket: %1
-A warning message issued when IfaceMgr fails to open and bind a socket. The reason
-for the failure is appended as an argument of the log message.
-
 % DHCP4_PACKET_PARSE_FAIL failed to parse incoming packet: %1
-The IPv4 DHCP server has received a packet that it is unable to
+The DHCPv4 server has received a packet that it is unable to
 interpret. The reason why the packet is invalid is included in the message.
 
 % DHCP4_PACKET_PROCESS_FAIL failed to process packet received from %1: %2
@@ -222,10 +219,6 @@ This is a general catch-all message indicating that the processing of a
 received packet failed.  The reason is given in the message.  The server
 will not send a response but will instead ignore the packet.
 
-% DHCP4_PACKET_DROP_NO_TYPE packet received on interface %1 dropped, because of missing msg-type option
-This is a debug message informing that incoming DHCPv4 packet did not
-have mandatory DHCP message type option and thus was dropped.
-
 % DHCP4_PACKET_RECEIVED %1 (type %2) packet received on interface %3
 A debug message noting that the server has received the specified type of
 packet on the specified interface.  Note that a packet marked as UNKNOWN
@@ -233,35 +226,35 @@ may well be a valid DHCP packet, just a type not expected by the server
 (e.g. it will report a received OFFER packet as UNKNOWN).
 
 % DHCP4_PACKET_RECEIVE_FAIL error on attempt to receive packet: %1
-The IPv4 DHCP server tried to receive a packet but an error
+The DHCPv4 server tried to receive a packet but an error
 occurred during this attempt. The reason for the error is included in
 the message.
 
 % DHCP4_PACKET_SEND_FAIL failed to send DHCPv4 packet: %1
-This error is output if the IPv4 DHCP server fails to send an assembled
+This error is output if the DHCPv4 server fails to send an assembled
 DHCP message to a client. The reason for the error is included in the
 message.
 
 % DHCP4_PARSER_COMMIT_EXCEPTION parser failed to commit changes
-On receipt of message containing details to a change of the IPv4 DHCP
+On receipt of message containing details to a change of the DHCPv4
 server configuration, a set of parsers were successfully created, but one
 of them failed to commit its changes due to a low-level system exception
 being raised.  Additional messages may be output indicating the reason.
 
 % DHCP4_PARSER_COMMIT_FAIL parser failed to commit changes: %1
-On receipt of message containing details to a change of the IPv4 DHCP
+On receipt of message containing details to a change of the DHCPv4
 server configuration, a set of parsers were successfully created, but
 one of them failed to commit its changes.  The reason for the failure
 is given in the message.
 
 % DHCP4_PARSER_CREATED created parser for configuration element %1
-A debug message output during a configuration update of the IPv4 DHCP
+A debug message output during a configuration update of the DHCPv4
 server, notifying that the parser for the specified configuration element
 has been successfully created.
 
 % DHCP4_PARSER_EXCEPTION failed to create or run parser for configuration element %1
 On receipt of message containing details to a change of its configuration,
-the IPv4 DHCP server failed to create a parser to decode the contents of
+the DHCPv4 server failed to create a parser to decode the contents of
 the named configuration element, or the creation succeeded but the parsing
 actions and committal of changes failed.  The message has been output in
 response to a non-BIND 10 exception being raised.  Additional messages
@@ -269,7 +262,7 @@ may give further information.
 
 % DHCP4_PARSER_FAIL failed to create or run parser for configuration element %1: %2
 On receipt of message containing details to a change of its configuration,
-the IPv4 DHCP server failed to create a parser to decode the contents
+the DHCPv4 server failed to create a parser to decode the contents
 of the named configuration element, or the creation succeeded but the
 parsing actions and committal of changes failed.  The reason for the
 failure is given in the message.
@@ -324,40 +317,40 @@ both clones use the same client-id.
 A debug message listing the data returned to the client.
 
 % DHCP4_SERVER_FAILED server failed: %1
-The IPv4 DHCP server has encountered a fatal error and is terminating.
+The DHCPv4 server has encountered a fatal error and is terminating.
 The reason for the failure is included in the message.
 
 % DHCP4_SESSION_FAIL failed to establish BIND 10 session (%1), running stand-alone
 The server has failed to establish communication with the rest of BIND
 10 and is running in stand-alone mode.  (This behavior will change once
-the IPv4 DHCP server is properly integrated with the rest of BIND 10.)
+the DHCPv4 server is properly integrated with the rest of BIND 10.)
 
 % DHCP4_SHUTDOWN server shutdown
-The IPv4 DHCP server has terminated normally.
+The DHCPv4 server has terminated normally.
 
 % DHCP4_SHUTDOWN_REQUEST shutdown of server requested
-This debug message indicates that a shutdown of the IPv4 server has
+This debug message indicates that a shutdown of the DHCPv4 server has
 been requested via a call to the 'shutdown' method of the core Dhcpv4Srv
 object.
 
 % DHCP4_SRV_CONSTRUCT_ERROR error creating Dhcpv4Srv object, reason: %1
 This error message indicates that during startup, the construction of a
-core component within the IPv4 DHCP server (the Dhcpv4 server object)
+core component within the DHCPv4 server (the Dhcpv4 server object)
 has failed.  As a result, the server will exit.  The reason for the
 failure is given within the message.
 
 % DHCP4_STANDALONE skipping message queue, running standalone
-This is a debug message indicating that the IPv4 server is running in
+This is a debug message indicating that the DHCPv4 server is running in
 standalone mode, not connected to the message queue.  Standalone mode
 is only useful during program development, and should not be used in a
 production environment.
 
 % DHCP4_STARTING server starting
-This informational message indicates that the IPv4 DHCP server has
+This informational message indicates that the DHCPv4 server has
 processed any command-line switches and is starting.
 
 % DHCP4_START_INFO pid: %1, port: %2, verbose: %3, standalone: %4
-This is a debug message issued during the IPv4 DHCP server startup.
+This is a debug message issued during the DHCPv4 server startup.
 It lists some information about the parameters with which the server
 is running.
 
@@ -370,3 +363,15 @@ steps in the processing of incoming client message.
 This warning message is output when a packet was received from a subnet
 for which the DHCPv4 server has not been configured. The most probable
 cause is a misconfiguration of the server.
+
+% DHCP4_UNRECOGNIZED_RCVD_PACKET_TYPE received message (transaction id %1) has unrecognized type %2 in option 53
+This debug message indicates that the message type carried in DHCPv4 option
+53 is unrecognized by the server. The valid message types are listed
+on the IANA website: http://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml#message-type-53.
+The message will not be processed by the server.
+
+% DHCP4_UNSUPPORTED_RCVD_PACKET_TYPE received message (transaction id %1), having type %2 is not supported
+This debug message indicates that the message type carried in DHCPv4 option
+53 is valid but the message will not be processed by the server. This includes
+messages being normally sent by the server to the client, such as Offer, ACK,
+NAK etc.

+ 73 - 42
src/bin/dhcp4/dhcp4_srv.cc

@@ -80,6 +80,8 @@ Dhcp4Hooks Hooks;
 namespace isc {
 namespace dhcp {
 
+const std::string Dhcpv4Srv::VENDOR_CLASS_PREFIX("VENDOR_CLASS_");
+
 Dhcpv4Srv::Dhcpv4Srv(uint16_t port, const char* dbconfig, const bool use_bcast,
                      const bool direct_response_desired)
 : shutdown_(true), alloc_engine_(), port_(port),
@@ -230,6 +232,11 @@ Dhcpv4Srv::run() {
             }
         }
 
+        // Assign this packet to one or more classes if needed. We need to do
+        // this before calling accept(), because getSubnet4() may need client
+        // class information.
+        classifyPacket(query);
+
         // Check whether the message should be further processed or discarded.
         // There is no need to log anything here. This function logs by itself.
         if (!accept(query)) {
@@ -272,9 +279,6 @@ Dhcpv4Srv::run() {
             callout_handle->getArgument("query4", query);
         }
 
-        // Assign this packet to one or more classes if needed
-        classifyPacket(query);
-
         try {
             switch (query->getType()) {
             case DHCPDISCOVER:
@@ -420,9 +424,6 @@ Dhcpv4Srv::run() {
             LOG_ERROR(dhcp4_logger, DHCP4_PACKET_SEND_FAIL)
                 .arg(e.what());
         }
-
-        // Send NameChangeRequests to the b10-dhcp_ddns module.
-        sendNameChangeRequests();
     }
 
     return (true);
@@ -704,12 +705,11 @@ Dhcpv4Srv::processClientName(const Pkt4Ptr& query, Pkt4Ptr& answer) {
             processClientFqdnOption(fqdn, answer);
 
         } else {
-            OptionCustomPtr hostname = boost::dynamic_pointer_cast<OptionCustom>
+            OptionStringPtr hostname = boost::dynamic_pointer_cast<OptionString>
                 (query->getOption(DHO_HOST_NAME));
             if (hostname) {
                 processHostnameOption(hostname, answer);
             }
-
         }
     } catch (const Exception& ex) {
         // In some rare cases it is possible that the client's name processing
@@ -762,7 +762,7 @@ Dhcpv4Srv::processClientFqdnOption(const Option4ClientFqdnPtr& fqdn,
 }
 
 void
-Dhcpv4Srv::processHostnameOption(const OptionCustomPtr& opt_hostname,
+Dhcpv4Srv::processHostnameOption(const OptionStringPtr& opt_hostname,
                                  Pkt4Ptr& answer) {
     // Fetch D2 configuration.
     D2ClientMgr& d2_mgr = CfgMgr::instance().getD2ClientMgr();
@@ -772,7 +772,7 @@ Dhcpv4Srv::processHostnameOption(const OptionCustomPtr& opt_hostname,
         return;
     }
 
-    std::string hostname = isc::util::str::trim(opt_hostname->readString());
+    std::string hostname = isc::util::str::trim(opt_hostname->getValue());
     unsigned int label_count = OptionDataTypeUtil::getLabelCount(hostname);
     // The hostname option sent by the client should be at least 1 octet long.
     // If it isn't we ignore this option. (Per RFC 2131, section 3.14)
@@ -786,7 +786,7 @@ Dhcpv4Srv::processHostnameOption(const OptionCustomPtr& opt_hostname,
     // possible that we will use the hostname option provided by the client
     // to perform the DNS update and we will send the same option to him to
     // indicate that we accepted this hostname.
-    OptionCustomPtr opt_hostname_resp(new OptionCustom(*opt_hostname));
+    OptionStringPtr opt_hostname_resp(new OptionString(*opt_hostname));
 
     // The hostname option may be unqualified or fully qualified. The lab_count
     // holds the number of labels for the name. The number of 1 means that
@@ -803,12 +803,14 @@ Dhcpv4Srv::processHostnameOption(const OptionCustomPtr& opt_hostname,
     /// conversion if needed and possible.
     if ((d2_mgr.getD2ClientConfig()->getReplaceClientName()) ||
         (label_count < 2)) {
-        opt_hostname_resp->writeString("");
+        // Set to root domain to signal later on that we should replace it.
+        // DHO_HOST_NAME is a string option which cannot be empty.
+        opt_hostname_resp->setValue(".");
     } else if (label_count == 2) {
         // If there are two labels, it means that the client has specified
         // the unqualified name. We have to concatenate the unqalified name
         // with the domain name.
-        opt_hostname_resp->writeString(d2_mgr.qualifyName(hostname));
+        opt_hostname_resp->setValue(d2_mgr.qualifyName(hostname));
     }
 
     answer->addOption(opt_hostname_resp);
@@ -881,26 +883,23 @@ queueNameChangeRequest(const isc::dhcp_ddns::NameChangeType chg_type,
             .arg(ex.what());
         return;
     }
+
     // Create NameChangeRequest
-    NameChangeRequest ncr(chg_type, lease->fqdn_fwd_, lease->fqdn_rev_,
-                          lease->hostname_, lease->addr_.toText(),
-                          dhcid, lease->cltt_ + lease->valid_lft_,
-                          lease->valid_lft_);
-    // And queue it.
+    NameChangeRequestPtr ncr(new NameChangeRequest(chg_type, lease->fqdn_fwd_,
+                                                   lease->fqdn_rev_,
+                                                   lease->hostname_,
+                                                   lease->addr_.toText(),
+                                                   dhcid,
+                                                   (lease->cltt_ +
+                                                    lease->valid_lft_),
+                                                   lease->valid_lft_));
+
     LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL_DATA, DHCP4_QUEUE_NCR)
         .arg(chg_type == CHG_ADD ? "add" : "remove")
-        .arg(ncr.toText());
-    name_change_reqs_.push(ncr);
-}
+        .arg(ncr->toText());
 
-void
-Dhcpv4Srv::sendNameChangeRequests() {
-    while (!name_change_reqs_.empty()) {
-        /// @todo Once next NameChangeRequest is picked from the queue
-        /// we should send it to the b10-dhcp_ddns module. Currently we
-        /// just drop it.
-        name_change_reqs_.pop();
-    }
+    // And pass it to the the manager.
+    CfgMgr::instance().getD2ClientMgr().sendRequest(ncr);
 }
 
 void
@@ -962,7 +961,7 @@ Dhcpv4Srv::assignLease(const Pkt4Ptr& question, Pkt4Ptr& answer) {
     std::string hostname;
     bool fqdn_fwd = false;
     bool fqdn_rev = false;
-    OptionCustomPtr opt_hostname;
+    OptionStringPtr opt_hostname;
     Option4ClientFqdnPtr fqdn = boost::dynamic_pointer_cast<
         Option4ClientFqdn>(answer->getOption(DHO_FQDN));
     if (fqdn) {
@@ -970,10 +969,17 @@ Dhcpv4Srv::assignLease(const Pkt4Ptr& question, Pkt4Ptr& answer) {
         fqdn_fwd = fqdn->getFlag(Option4ClientFqdn::FLAG_S);
         fqdn_rev = !fqdn->getFlag(Option4ClientFqdn::FLAG_N);
     } else {
-        opt_hostname = boost::dynamic_pointer_cast<OptionCustom>
+        opt_hostname = boost::dynamic_pointer_cast<OptionString>
             (answer->getOption(DHO_HOST_NAME));
         if (opt_hostname) {
-            hostname = opt_hostname->readString();
+            hostname = opt_hostname->getValue();
+            // DHO_HOST_NAME is string option which cannot be blank,
+            // we use "." to know we should replace it with a fully
+            // generated name. The local string variable needs to be
+            // blank in logic below.
+            if (hostname == ".") {
+                hostname = "";
+            }
             /// @todo It could be configurable what sort of updates the
             /// server is doing when Hostname option was sent.
             fqdn_fwd = true;
@@ -1027,7 +1033,7 @@ Dhcpv4Srv::assignLease(const Pkt4Ptr& question, Pkt4Ptr& answer) {
                     fqdn->setDomainName(lease->hostname_,
                                         Option4ClientFqdn::FULL);
                 } else if (opt_hostname) {
-                    opt_hostname->writeString(lease->hostname_);
+                    opt_hostname->setValue(lease->hostname_);
                 }
 
             } catch (const Exception& ex) {
@@ -1415,7 +1421,8 @@ Dhcpv4Srv::selectSubnet(const Pkt4Ptr& question) const {
     // level functions.
     if (question->isRelayed()) {
         subnet = CfgMgr::instance().getSubnet4(question->getGiaddr(),
-                                               question->classes_);
+                                               question->classes_,
+                                               true);
 
     // The message is not relayed so it is sent directly by a client. But
     // the client may be renewing its lease and in such case it unicasts
@@ -1815,15 +1822,15 @@ void Dhcpv4Srv::classifyPacket(const Pkt4Ptr& pkt) {
     // quals subscriber-id option that was inserted by the relay (CMTS).
     // This kind of logic will appear here soon.
     if (vendor_class->getValue().find(DOCSIS3_CLASS_MODEM) != std::string::npos) {
-        pkt->addClass(DOCSIS3_CLASS_MODEM);
-        classes += string(DOCSIS3_CLASS_MODEM) + " ";
+        pkt->addClass(VENDOR_CLASS_PREFIX + DOCSIS3_CLASS_MODEM);
+        classes += string(VENDOR_CLASS_PREFIX + DOCSIS3_CLASS_MODEM) + " ";
     } else
     if (vendor_class->getValue().find(DOCSIS3_CLASS_EROUTER) != std::string::npos) {
-        pkt->addClass(DOCSIS3_CLASS_EROUTER);
-        classes += string(DOCSIS3_CLASS_EROUTER) + " ";
+        pkt->addClass(VENDOR_CLASS_PREFIX + DOCSIS3_CLASS_EROUTER);
+        classes += string(VENDOR_CLASS_PREFIX + DOCSIS3_CLASS_EROUTER) + " ";
     } else {
-        classes += vendor_class->getValue();
-        pkt->addClass(vendor_class->getValue());
+        classes += VENDOR_CLASS_PREFIX + vendor_class->getValue();
+        pkt->addClass(VENDOR_CLASS_PREFIX + vendor_class->getValue());
     }
 
     if (!classes.empty()) {
@@ -1839,7 +1846,7 @@ bool Dhcpv4Srv::classSpecificProcessing(const Pkt4Ptr& query, const Pkt4Ptr& rsp
         return (true);
     }
 
-    if (query->inClass(DOCSIS3_CLASS_MODEM)) {
+    if (query->inClass(VENDOR_CLASS_PREFIX + DOCSIS3_CLASS_MODEM)) {
 
         // Set next-server. This is TFTP server address. Cable modems will
         // download their configuration from that server.
@@ -1860,7 +1867,7 @@ bool Dhcpv4Srv::classSpecificProcessing(const Pkt4Ptr& query, const Pkt4Ptr& rsp
         }
     }
 
-    if (query->inClass(DOCSIS3_CLASS_EROUTER)) {
+    if (query->inClass(VENDOR_CLASS_PREFIX + DOCSIS3_CLASS_EROUTER)) {
 
         // Do not set TFTP server address for eRouter devices.
         rsp->setSiaddr(IOAddress("0.0.0.0"));
@@ -1869,5 +1876,29 @@ bool Dhcpv4Srv::classSpecificProcessing(const Pkt4Ptr& query, const Pkt4Ptr& rsp
     return (true);
 }
 
+void
+Dhcpv4Srv::startD2() {
+    D2ClientMgr& d2_mgr = CfgMgr::instance().getD2ClientMgr();
+    if (d2_mgr.ddnsEnabled()) {
+        // Updates are enabled, so lets start the sender, passing in
+        // our error handler.
+        // This may throw so wherever this is called needs to ready.
+        d2_mgr.startSender(boost::bind(&Dhcpv4Srv::d2ClientErrorHandler,
+                                       this, _1, _2));
+    }
+}
+
+void
+Dhcpv4Srv::d2ClientErrorHandler(const
+                                dhcp_ddns::NameChangeSender::Result result,
+                                dhcp_ddns::NameChangeRequestPtr& ncr) {
+    LOG_ERROR(dhcp4_logger, DHCP4_DDNS_REQUEST_SEND_FAILED).
+              arg(result).arg((ncr ? ncr->toText() : " NULL "));
+    // We cannot communicate with b10-dhcp-ddns, suspend futher updates.
+    /// @todo We may wish to revisit this, but for now we will simpy turn
+    /// them off.
+    CfgMgr::instance().getD2ClientMgr().suspendUpdates();
+}
+
 }   // namespace dhcp
 }   // namespace isc

+ 40 - 23
src/bin/dhcp4/dhcp4_srv.h

@@ -18,9 +18,11 @@
 #include <dhcp/dhcp4.h>
 #include <dhcp/pkt4.h>
 #include <dhcp/option.h>
+#include <dhcp/option_string.h>
 #include <dhcp/option4_client_fqdn.h>
 #include <dhcp/option_custom.h>
 #include <dhcp_ddns/ncr_msg.h>
+#include <dhcpsrv/d2_client_mgr.h>
 #include <dhcpsrv/subnet.h>
 #include <dhcpsrv/alloc_engine.h>
 #include <hooks/callout_handle.h>
@@ -165,6 +167,30 @@ public:
     /// @param use_bcast should broadcast flags be set on the sockets.
     static void openActiveSockets(const uint16_t port, const bool use_bcast);
 
+    /// @brief Starts DHCP_DDNS client IO if DDNS updates are enabled.
+    ///
+    /// If updates are enabled, it Instructs the D2ClientMgr singleton to
+    /// enter send mode.  If D2ClientMgr encounters errors it may throw
+    /// D2ClientErrors. This method does not catch exceptions.
+    void startD2();
+
+    /// @brief Implements the error handler for DHCP_DDNS IO errors
+    ///
+    /// Invoked when a NameChangeRequest send to b10-dhcp-ddns completes with
+    /// a failed status.  These are communications errors, not data related
+    /// failures.
+    ///
+    /// This method logs the failure and then suspends all further updates.
+    /// Updating can only be restored by reconfiguration or restarting the
+    /// server.  There is currently no retry logic so the first IO error that
+    /// occurs will suspend updates.
+    /// @todo We may wish to make this more robust or sophisticated.
+    ///
+    /// @param result Result code of the send operation.
+    /// @param ncr NameChangeRequest which failed to send.
+    virtual void d2ClientErrorHandler(const dhcp_ddns::
+                                      NameChangeSender::Result result,
+                                      dhcp_ddns::NameChangeRequestPtr& ncr);
 protected:
 
     /// @name Functions filtering and sanity-checking received messages.
@@ -405,6 +431,14 @@ protected:
     /// @param [out] answer A response message to be sent to a client.
     void processClientName(const Pkt4Ptr& query, Pkt4Ptr& answer);
 
+    /// @brief this is a prefix added to the contend of vendor-class option
+    ///
+    /// If incoming packet has a vendor class option, its content is
+    /// prepended with this prefix and then interpreted as a class.
+    /// For example, a packet that sends vendor class with value of "FOO"
+    /// will cause the packet to be assigned to class VENDOR_CLASS_FOO.
+    static const std::string VENDOR_CLASS_PREFIX;
+
 private:
     /// @brief Process Client FQDN %Option sent by a client.
     ///
@@ -427,10 +461,10 @@ private:
     /// prepare the Hostname option to be sent back to the client in the
     /// server's response.
     ///
-    /// @param opt_hostname An @c OptionCustom object encapsulating the Hostname
+    /// @param opt_hostname An @c OptionString object encapsulating the Hostname
     /// %Option.
     /// @param [out] answer A response message to be sent to a client.
-    void processHostnameOption(const OptionCustomPtr& opt_hostname,
+    void processHostnameOption(const OptionStringPtr& opt_hostname,
                                Pkt4Ptr& answer);
 
 protected:
@@ -455,10 +489,10 @@ protected:
     /// @brief Creates the NameChangeRequest and adds to the queue for
     /// processing.
     ///
-    /// This function adds the @c isc::dhcp_ddns::NameChangeRequest to the
-    /// queue and emits the debug message which indicates whether the request
-    /// being added is to remove DNS entry or add a new entry. This function
-    /// is exception free.
+    /// This creates the @c isc::dhcp_ddns::NameChangeRequest; emits a
+    /// the debug message which indicates whether the request being added is
+    /// to remove DNS entry or add a new entry; and then sends the request
+    /// to the D2ClientMgr for transmission to b10-dhcp-ddns.
     ///
     /// @param chg_type A type of the NameChangeRequest (ADD or REMOVE).
     /// @param lease A lease for which the NameChangeRequest is created and
@@ -466,17 +500,6 @@ protected:
     void queueNameChangeRequest(const isc::dhcp_ddns::NameChangeType chg_type,
                                 const Lease4Ptr& lease);
 
-    /// @brief Sends all outstanding NameChangeRequests to b10-dhcp-ddns module.
-    ///
-    /// The purpose of this function is to pick all outstanding
-    /// NameChangeRequests from the FIFO queue and send them to b10-dhcp-ddns
-    /// module.
-    ///
-    /// @todo Currently this function simply removes all requests from the
-    /// queue but doesn't send them anywhere. In the future, the
-    /// NameChangeSender will be used to deliver requests to the other module.
-    void sendNameChangeRequests();
-
     /// @brief Attempts to renew received addresses
     ///
     /// Attempts to renew existing lease. This typically includes finding a lease that
@@ -686,12 +709,6 @@ private:
     int hook_index_pkt4_receive_;
     int hook_index_subnet4_select_;
     int hook_index_pkt4_send_;
-
-protected:
-
-    /// Holds a list of @c isc::dhcp_ddns::NameChangeRequest objects which
-    /// are waiting for sending  to b10-dhcp-ddns module.
-    std::queue<isc::dhcp_ddns::NameChangeRequest> name_change_reqs_;
 };
 
 }; // namespace isc::dhcp

+ 7 - 4
src/bin/dhcp4/tests/Makefile.am

@@ -58,24 +58,27 @@ if HAVE_GTEST
 # to unexpected errors. For this reason, the --enable-static-link option is
 # ignored for unit tests built here.
 
-nodistdir=$(abs_top_builddir)/src/bin/dhcp4/tests
-nodist_LTLIBRARIES = libco1.la libco2.la
+noinst_LTLIBRARIES = libco1.la libco2.la
+
+# -rpath /nowhere is a hack to trigger libtool to not create a
+# convenience archive, resulting in shared modules
 
 libco1_la_SOURCES  = callout_library_1.cc callout_library_common.h
 libco1_la_CXXFLAGS = $(AM_CXXFLAGS)
 libco1_la_CPPFLAGS = $(AM_CPPFLAGS)
-libco1_la_LDFLAGS = -avoid-version -export-dynamic -module
+libco1_la_LDFLAGS = -avoid-version -export-dynamic -module -rpath /nowhere
 
 libco2_la_SOURCES  = callout_library_2.cc callout_library_common.h
 libco2_la_CXXFLAGS = $(AM_CXXFLAGS)
 libco2_la_CPPFLAGS = $(AM_CPPFLAGS)
-libco2_la_LDFLAGS = -avoid-version -export-dynamic -module
+libco2_la_LDFLAGS = -avoid-version -export-dynamic -module -rpath /nowhere
 
 TESTS += dhcp4_unittests
 
 dhcp4_unittests_SOURCES = ../dhcp4_srv.h ../dhcp4_srv.cc ../ctrl_dhcp4_srv.cc
 dhcp4_unittests_SOURCES += ../dhcp4_log.h ../dhcp4_log.cc
 dhcp4_unittests_SOURCES += ../config_parser.cc ../config_parser.h
+dhcp4_unittests_SOURCES += d2_unittest.h d2_unittest.cc
 dhcp4_unittests_SOURCES += dhcp4_test_utils.h
 dhcp4_unittests_SOURCES += dhcp4_unittests.cc
 dhcp4_unittests_SOURCES += dhcp4_srv_unittest.cc

+ 379 - 0
src/bin/dhcp4/tests/d2_unittest.cc

@@ -0,0 +1,379 @@
+// Copyright (C) 2014 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/iface_mgr.h>
+#include <dhcp4/config_parser.h>
+#include <dhcp4/tests/d2_unittest.h>
+#include <dhcpsrv/cfgmgr.h>
+
+#include <gtest/gtest.h>
+
+#include <string>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::data;
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+void
+D2Dhcpv4Srv::d2ClientErrorHandler(const
+                                dhcp_ddns::NameChangeSender::Result result,
+                                dhcp_ddns::NameChangeRequestPtr& ncr) {
+    ++error_count_;
+    // call base class error handler
+    Dhcpv4Srv::d2ClientErrorHandler(result, ncr);
+}
+
+const bool Dhcp4SrvD2Test::SHOULD_PASS;
+const bool Dhcp4SrvD2Test::SHOULD_FAIL;
+
+Dhcp4SrvD2Test::Dhcp4SrvD2Test() : rcode_(-1) {
+}
+
+Dhcp4SrvD2Test::~Dhcp4SrvD2Test() {
+    reset();
+}
+
+dhcp_ddns::NameChangeRequestPtr
+Dhcp4SrvD2Test::buildTestNcr(uint32_t dhcid_id_num) {
+    // Build an NCR from json string.
+    std::ostringstream stream;
+
+    stream <<
+        "{"
+        " \"change_type\" : 0 , "
+        " \"forward_change\" : true , "
+        " \"reverse_change\" : false , "
+        " \"fqdn\" : \"myhost.example.com.\" , "
+        " \"ip_address\" : \"192.168.2.1\" , "
+        " \"dhcid\" : \""
+
+        << std::hex << std::setfill('0') << std::setw(16)
+        << dhcid_id_num << "\" , "
+
+        " \"lease_expires_on\" : \"20140121132405\" , "
+        " \"lease_length\" : 1300 "
+        "}";
+
+    return (dhcp_ddns::NameChangeRequest::fromJSON(stream.str()));
+}
+
+void
+Dhcp4SrvD2Test::reset() {
+    std::string config = "{ \"interfaces\": [ \"*\" ],"
+            "\"hooks-libraries\": [ ], "
+            "\"rebind-timer\": 2000, "
+            "\"renew-timer\": 1000, "
+            "\"valid-lifetime\": 4000, "
+            "\"subnet4\": [ ], "
+            "\"dhcp-ddns\": { \"enable-updates\" : false }, "
+            "\"option-def\": [ ], "
+            "\"option-data\": [ ] }";
+    configure(config, SHOULD_PASS);
+}
+
+void
+Dhcp4SrvD2Test::configureD2(bool enable_d2, const bool exp_result,
+                            const std::string& ip_address,
+                            const uint32_t port) {
+    std::ostringstream config;
+    config <<
+        "{ \"interfaces\": [ \"*\" ],"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet4\": [ { "
+        "    \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+        "    \"subnet\": \"192.0.2.0/24\" } ],"
+        " \"dhcp-ddns\" : {"
+        "     \"enable-updates\" : " << (enable_d2 ? "true" : "false") <<  ", "
+        "     \"server-ip\" : \"" << ip_address << "\", "
+        "     \"server-port\" : " << port << ", "
+        "     \"ncr-protocol\" : \"UDP\", "
+        "     \"ncr-format\" : \"JSON\", "
+        "     \"always-include-fqdn\" : true, "
+        "     \"allow-client-update\" : true, "
+        "     \"override-no-update\" : true, "
+        "     \"override-client-update\" : true, "
+        "     \"replace-client-name\" : true, "
+        "     \"generated-prefix\" : \"test.prefix\", "
+        "     \"qualifying-suffix\" : \"test.suffix.\" },"
+        "\"valid-lifetime\": 4000 }";
+
+    configure(config.str(), exp_result);
+}
+
+void
+Dhcp4SrvD2Test::configure(const std::string& config, bool exp_result) {
+    ElementPtr json = Element::fromJSON(config);
+    ConstElementPtr status;
+
+    // Configure the server and make sure the config is accepted
+    EXPECT_NO_THROW(status = configureDhcp4Server(srv_, json));
+    ASSERT_TRUE(status);
+
+    int rcode;
+    ConstElementPtr comment = config::parseAnswer(rcode, status);
+    if (exp_result == SHOULD_PASS) {
+        ASSERT_EQ(0, rcode);
+    } else {
+        ASSERT_EQ(1, rcode);
+    }
+}
+
+// Tests ability to turn on and off ddns updates by submitting
+// by submitting the appropriate configuration to Dhcp4 server
+// and then invoking its startD2() method.
+TEST_F(Dhcp4SrvD2Test, enableDisable) {
+    // Grab the manager and verify that be default ddns is off
+    // and a sender was not started.
+    dhcp::D2ClientMgr& mgr = CfgMgr::instance().getD2ClientMgr();
+    ASSERT_FALSE(mgr.ddnsEnabled());
+    ASSERT_FALSE(mgr.amSending());
+
+    // Verify a valid config with ddns enabled configures ddns properly,
+    // but does not start the sender.
+    ASSERT_NO_FATAL_FAILURE(configureD2(true));
+    ASSERT_TRUE(mgr.ddnsEnabled());
+    ASSERT_FALSE(mgr.amSending());
+
+    // Verify that calling start does not throw and starts the sender.
+    ASSERT_NO_THROW(srv_.startD2());
+    ASSERT_TRUE(mgr.amSending());
+
+    // Verify a valid config with ddns disabled configures ddns properly.
+    // Sender should not have been started.
+    ASSERT_NO_FATAL_FAILURE(configureD2(false));
+    ASSERT_FALSE(mgr.ddnsEnabled());
+    ASSERT_FALSE(mgr.amSending());
+
+    // Verify that the sender does NOT get started when ddns is disabled.
+    srv_.startD2();
+    ASSERT_FALSE(mgr.amSending());
+}
+
+// Tests Dhcp4 server's ability to correctly handle a flawed dhcp-ddns configuration.
+// It does so by first enabling updates by submitting a valid configuration and then
+// ensuring they remain on after submitting a flawed configuration.
+// and then invoking its startD2() method.
+TEST_F(Dhcp4SrvD2Test, badConfig) {
+    // Grab the manager and verify that be default ddns is off
+    // and a sender was not started.
+    dhcp::D2ClientMgr& mgr = CfgMgr::instance().getD2ClientMgr();
+    ASSERT_FALSE(mgr.ddnsEnabled());
+
+    // Configure it enabled and start it.
+    ASSERT_NO_FATAL_FAILURE(configureD2(true));
+    ASSERT_TRUE(mgr.ddnsEnabled());
+    ASSERT_NO_THROW(srv_.startD2());
+    ASSERT_TRUE(mgr.amSending());
+
+    // Now attempt to give it an invalid configuration.
+    // Result should indicate failure.
+    ASSERT_NO_FATAL_FAILURE(configureD2(false, SHOULD_FAIL, "bogus_ip"));
+
+    // Configure was not altered, so ddns should be enabled and still sending.
+    ASSERT_TRUE(mgr.ddnsEnabled());
+    ASSERT_TRUE(mgr.amSending());
+
+    // Verify that calling start does not throw or stop the sender.
+    ASSERT_NO_THROW(srv_.startD2());
+    ASSERT_TRUE(mgr.amSending());
+
+}
+
+// Checks that submitting an identical dhcp-ddns configuration
+// is handled properly.  Not effect should be no change in
+// status for ddns updating.  Updates should still enabled and
+// in send mode.  This indicates that the sender was not stopped.
+TEST_F(Dhcp4SrvD2Test, sameConfig) {
+    // Grab the manager and verify that be default ddns is off
+    // and a sender was not started.
+    dhcp::D2ClientMgr& mgr = CfgMgr::instance().getD2ClientMgr();
+    ASSERT_FALSE(mgr.ddnsEnabled());
+
+    // Configure it enabled and start it.
+    ASSERT_NO_FATAL_FAILURE(configureD2(true));
+    ASSERT_TRUE(mgr.ddnsEnabled());
+    ASSERT_NO_THROW(srv_.startD2());
+    ASSERT_TRUE(mgr.amSending());
+
+    // Now submit an identical configuration.
+    ASSERT_NO_FATAL_FAILURE(configureD2(true));
+
+    // Configuration was not altered, so ddns should still enabled and sending.
+    ASSERT_TRUE(mgr.ddnsEnabled());
+    ASSERT_TRUE(mgr.amSending());
+
+    // Verify that calling start does not throw or stop the sender.
+    ASSERT_NO_THROW(srv_.startD2());
+    ASSERT_TRUE(mgr.amSending());
+}
+
+// Checks that submitting an different, but valid dhcp-ddns configuration
+// is handled properly.  Updates should be enabled, however they should
+// not yet be running.  This indicates that the sender was stopped and
+// replaced, but not yet started.
+TEST_F(Dhcp4SrvD2Test, differentConfig) {
+    // Grab the manager and verify that be default ddns is off
+    // and a sender was not started.
+    dhcp::D2ClientMgr& mgr = CfgMgr::instance().getD2ClientMgr();
+    ASSERT_FALSE(mgr.ddnsEnabled());
+
+    // Configure it enabled and start it.
+    ASSERT_NO_FATAL_FAILURE(configureD2(true));
+    ASSERT_TRUE(mgr.ddnsEnabled());
+    ASSERT_NO_THROW(srv_.startD2());
+    ASSERT_TRUE(mgr.amSending());
+
+    // Now enable it on a different port.
+    ASSERT_NO_FATAL_FAILURE(configureD2(true, SHOULD_PASS, "127.0.0.1", 54001));
+
+    // Configuration was altered, so ddns should still enabled but not sending.
+    ASSERT_TRUE(mgr.ddnsEnabled());
+    ASSERT_FALSE(mgr.amSending());
+
+    // Verify that calling start starts the sender.
+    ASSERT_NO_THROW(srv_.startD2());
+    ASSERT_TRUE(mgr.amSending());
+}
+
+// Checks that given a valid, enabled configuration and placing
+// sender in send mode, permits NCR requests to be sent via UPD
+// socket.  Note this test does not employ any sort of receiving
+// client to verify actual transmission.  These types of tests
+// are including under dhcp_ddns and d2 unit testing.
+TEST_F(Dhcp4SrvD2Test, simpleUDPSend) {
+    // Grab the manager and verify that be default ddns is off
+    // and a sender was not started.
+    dhcp::D2ClientMgr& mgr = CfgMgr::instance().getD2ClientMgr();
+    ASSERT_FALSE(mgr.ddnsEnabled());
+
+    // Configure it enabled and start it.
+    ASSERT_NO_FATAL_FAILURE(configureD2(true));
+    ASSERT_TRUE(mgr.ddnsEnabled());
+    ASSERT_NO_THROW(srv_.startD2());
+    ASSERT_TRUE(mgr.amSending());
+
+    // Verify that we can queue up a message.
+    dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr();
+    ASSERT_NO_THROW(mgr.sendRequest(ncr));
+    EXPECT_EQ(1, mgr.getQueueSize());
+
+    // Calling receive should detect the ready IO on the sender's select-fd,
+    // and invoke callback, which should complete the send.
+    ASSERT_NO_THROW(IfaceMgr::instance().receive4(0,0));
+
+    // Verify the queue is now empty.
+    EXPECT_EQ(0, mgr.getQueueSize());
+}
+
+// Checks that an IO error in sending a request to D2, results in ddns updates being
+// suspended.  This indicates that Dhcp4Srv's error handler has been invoked as expected.
+// Note that this unit test relies on an attempt to send to a server address of 0.0.0.0
+// port 0 fails under all OSs.
+TEST_F(Dhcp4SrvD2Test, forceUDPSendFailure) {
+    // Grab the manager and verify that be default ddns is off
+    // and a sender was not started.
+    dhcp::D2ClientMgr& mgr = CfgMgr::instance().getD2ClientMgr();
+    ASSERT_FALSE(mgr.ddnsEnabled());
+
+    // Configure it enabled and start it.
+    // Using server address of 0.0.0.0/0 should induce failure on send.
+    ASSERT_NO_FATAL_FAILURE(configureD2(true, SHOULD_PASS, "0.0.0.0", 0));
+    ASSERT_TRUE(mgr.ddnsEnabled());
+    ASSERT_NO_THROW(srv_.startD2());
+    ASSERT_TRUE(mgr.amSending());
+
+    // Queue up 3 messages.
+    for (int i = 0; i < 3; i++) {
+        dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr(i + 1);
+        ASSERT_NO_THROW(mgr.sendRequest(ncr));
+    }
+    EXPECT_EQ(3, mgr.getQueueSize());
+
+    // Calling receive should detect the ready IO on the sender's select-fd,
+    // and invoke callback, which should complete the send, which should
+    // fail.
+    ASSERT_NO_THROW(IfaceMgr::instance().receive4(0,0));
+
+    // Verify the error handler was invoked.
+    EXPECT_EQ(1, srv_.error_count_);
+
+    // Verify that updates are disabled and we are no longer sending.
+    ASSERT_FALSE(mgr.ddnsEnabled());
+    ASSERT_FALSE(mgr.amSending());
+
+    // Verify message is still in the queue.
+    EXPECT_EQ(3, mgr.getQueueSize());
+
+    // Verify that we can't just restart it.
+    /// @todo This may change if we add ability to resume.
+    ASSERT_NO_THROW(srv_.startD2());
+    ASSERT_FALSE(mgr.amSending());
+
+    // Configure it enabled and start it.
+    ASSERT_NO_FATAL_FAILURE(configureD2(true));
+    ASSERT_TRUE(mgr.ddnsEnabled());
+    ASSERT_NO_THROW(srv_.startD2());
+    ASSERT_TRUE(mgr.amSending());
+
+    // Verify message is still in the queue.
+    EXPECT_EQ(3, mgr.getQueueSize());
+
+    // This will finish sending the 1st message in queue
+    // and initiate send of 2nd message.
+    ASSERT_NO_THROW(IfaceMgr::instance().receive4(0,0));
+    EXPECT_EQ(1, srv_.error_count_);
+
+    // First message is off the queue.
+    EXPECT_EQ(2, mgr.getQueueSize());
+}
+
+// Tests error handling of D2ClientMgr::sendRequest() failure
+// by attempting to queue maximum number of messages.
+TEST_F(Dhcp4SrvD2Test, queueMaxError) {
+    // Configure it enabled and start it.
+    dhcp::D2ClientMgr& mgr = CfgMgr::instance().getD2ClientMgr();
+    ASSERT_NO_FATAL_FAILURE(configureD2(true));
+    ASSERT_TRUE(mgr.ddnsEnabled());
+    ASSERT_NO_THROW(srv_.startD2());
+    ASSERT_TRUE(mgr.amSending());
+
+    // Attempt to queue more then the maximum allowed.
+    int max_msgs = mgr.getQueueMaxSize();
+    for (int i = 0; i < max_msgs + 1; i++) {
+        dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr(i + 1);
+        ASSERT_NO_THROW(mgr.sendRequest(ncr));
+    }
+
+    // Stopping sender will complete the first message so there
+    // should be max less one.
+    EXPECT_EQ(max_msgs - 1, mgr.getQueueSize());
+
+    // Verify the error handler was invoked.
+    EXPECT_EQ(1, srv_.error_count_);
+
+    // Verify that updates are disabled and we are no longer sending.
+    ASSERT_FALSE(mgr.ddnsEnabled());
+    ASSERT_FALSE(mgr.amSending());
+}
+
+
+} // namespace test
+} // namespace dhcp
+} // namespace isc
+

+ 117 - 0
src/bin/dhcp4/tests/d2_unittest.h

@@ -0,0 +1,117 @@
+// Copyright (C) 2014 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 d2_unittest.h Defines classes for testing Dhcpv4srv with D2ClientMgr
+
+#ifndef D2_UNITTEST_H
+#define D2_UNITTEST_H
+
+#include <dhcp4/dhcp4_srv.h>
+#include <config/ccsession.h>
+
+#include <gtest/gtest.h>
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @brief Test derivation of Dhcpv4Srv class used in D2 testing.
+/// Use of this class allows the intervention at strategic points in testing
+/// by permitting overridden methods and access to scope protected members.
+class D2Dhcpv4Srv : public  Dhcpv4Srv {
+public:
+    /// @brief Counts the number of times the client error handler is called.
+    int error_count_;
+
+    /// @brief Constructor
+    D2Dhcpv4Srv()
+        : Dhcpv4Srv(0, "type=memfile", false, false), error_count_(0) {
+    }
+
+    /// @brief virtual Destructor.
+    virtual ~D2Dhcpv4Srv() {
+    }
+
+    /// @brief Override the error handler.
+    virtual void d2ClientErrorHandler(const dhcp_ddns::NameChangeSender::
+                                      Result result,
+                                      dhcp_ddns::NameChangeRequestPtr& ncr);
+};
+
+/// @brief Test fixture which permits testing the interaction between the
+/// D2ClientMgr and Dhcpv4Srv.
+class Dhcp4SrvD2Test : public ::testing::Test {
+public:
+    /// @brief Mnemonic constants for calls to configuration methods.
+    static const bool SHOULD_PASS = true;
+    static const bool SHOULD_FAIL = false;
+
+    /// @brief Constructor
+    Dhcp4SrvD2Test();
+
+    /// @brief virtual Destructor
+    virtual ~Dhcp4SrvD2Test();
+
+    /// @brief Resets the CfgMgr singleton to defaults.
+    /// Primarily used in the test destructor as gtest doesn't exit between
+    /// tests.
+    /// @todo CfgMgr should provide a method to reset everything or maybe
+    /// reconstruct the singleton.
+    void reset();
+
+    /// @brief Configures the server with D2 enabled or disabled
+    ///
+    /// Constructs a configuration string including dhcp-ddns with the
+    /// parameters given and passes it into the server's configuration handler.
+    ///
+    /// @param enable_updates value to assign to the enable-updates parameter
+    /// @param exp_result indicates if configuration should pass or fail
+    /// @param ip_address IP address for the D2 server
+    /// @param port  port for the D2 server
+    void configureD2(bool enable_updates, bool exp_result = SHOULD_PASS,
+                     const std::string& ip_address = "127.0.0.1",
+                     const uint32_t port = 53001);
+
+    /// @brief Configures the server with the given configuration
+    ///
+    /// Passes the given configuration string into the server's configuration
+    /// handler.  It accepts a flag indicating whether or not the configuration
+    /// is expected to succeed or fail.  This permits testing the server's
+    /// response to both valid and invalid configurations.
+    ///
+    /// @param config JSON string containing the configuration
+    /// @param exp_result indicates if configuration should pass or fail
+    void configure(const std::string& config, bool exp_result = SHOULD_PASS);
+
+    /// @brief Contructs a NameChangeRequest message from a fixed JSON string.
+    ///
+    /// @param dhcid_id_num Integer value to use as the DHCID.
+    dhcp_ddns::NameChangeRequestPtr buildTestNcr(uint32_t
+                                                 dhcid_id_num = 0xdeadbeef);
+
+    /// @brief Stores the return code of the last configuration attempt.
+    int rcode_;
+
+    /// @brief Stores the message component of the last configuration tattempt.
+    isc::data::ConstElementPtr comment_;
+
+    /// @brief Server object under test.
+    D2Dhcpv4Srv srv_;
+};
+
+}; // end of isc::dhcp::test namespace
+}; // end of isc::dhcp namespace
+}; // end of isc namespace
+
+#endif // D2_UNITTEST_H

+ 143 - 19
src/bin/dhcp4/tests/dhcp4_srv_unittest.cc

@@ -3294,8 +3294,8 @@ TEST_F(Dhcpv4SrvTest, clientClassification) {
 
     srv.classifyPacket(dis1);
 
-    EXPECT_TRUE(dis1->inClass("docsis3.0"));
-    EXPECT_FALSE(dis1->inClass("eRouter1.0"));
+    EXPECT_TRUE(dis1->inClass(srv.VENDOR_CLASS_PREFIX + "docsis3.0"));
+    EXPECT_FALSE(dis1->inClass(srv.VENDOR_CLASS_PREFIX + "eRouter1.0"));
 
     // Let's create a relayed DISCOVER. This particular relayed DISCOVER has
     // vendor-class set to eRouter1.0
@@ -3305,8 +3305,8 @@ TEST_F(Dhcpv4SrvTest, clientClassification) {
 
     srv.classifyPacket(dis2);
 
-    EXPECT_TRUE(dis2->inClass("eRouter1.0"));
-    EXPECT_FALSE(dis2->inClass("docsis3.0"));
+    EXPECT_TRUE(dis2->inClass(srv.VENDOR_CLASS_PREFIX + "eRouter1.0"));
+    EXPECT_FALSE(dis2->inClass(srv.VENDOR_CLASS_PREFIX + "docsis3.0"));
 }
 
 // Checks if the client-class field is indeed used for subnet selection.
@@ -3314,10 +3314,6 @@ TEST_F(Dhcpv4SrvTest, clientClassification) {
 // .clientClassification above.
 TEST_F(Dhcpv4SrvTest, clientClassify2) {
 
-    NakedDhcpv4Srv srv(0);
-
-    ConstElementPtr status;
-
     // This test configures 2 subnets. We actually only need the
     // first one, but since there's still this ugly hack that picks
     // the pool if there is only one, we must use more than one
@@ -3340,15 +3336,9 @@ TEST_F(Dhcpv4SrvTest, clientClassify2) {
         "],"
         "\"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_);
+    ASSERT_NO_THROW(configure(config));
 
+    // Create a simple packet that we'll use for classification
     Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
     dis->setRemoteAddr(IOAddress("192.0.2.1"));
     dis->setCiaddr(IOAddress("192.0.2.1"));
@@ -3358,19 +3348,153 @@ TEST_F(Dhcpv4SrvTest, clientClassify2) {
 
     // This discover does not belong to foo class, so it will not
     // be serviced
-    EXPECT_FALSE(srv.selectSubnet(dis));
+    EXPECT_FALSE(srv_.selectSubnet(dis));
 
     // Let's add the packet to bar class and try again.
     dis->addClass("bar");
 
     // Still not supported, because it belongs to wrong class.
-    EXPECT_FALSE(srv.selectSubnet(dis));
+    EXPECT_FALSE(srv_.selectSubnet(dis));
 
     // Let's add it to maching class.
     dis->addClass("foo");
 
     // This time it should work
-    EXPECT_TRUE(srv.selectSubnet(dis));
+    EXPECT_TRUE(srv_.selectSubnet(dis));
+}
+
+// Checks if relay IP address specified in the relay-info structure in
+// subnet4 is being used properly.
+TEST_F(Dhcpv4SrvTest, relayOverride) {
+
+    // We have 2 subnets defined. Note that both have a relay address
+    // defined. Both are not belonging to the subnets. That is
+    // important, because if the relay belongs to the subnet, there's
+    // no need to specify relay override.
+    string config = "{ \"interfaces\": [ \"*\" ],"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet4\": [ "
+        "{   \"pool\": [ \"192.0.2.2 - 192.0.2.100\" ],"
+        "    \"relay\": { "
+        "        \"ip-address\": \"192.0.5.1\""
+        "    },"
+        "    \"subnet\": \"192.0.2.0/24\" }, "
+        "{   \"pool\": [ \"192.0.3.1 - 192.0.3.100\" ],"
+        "    \"relay\": { "
+        "        \"ip-address\": \"192.0.5.2\""
+        "    },"
+        "    \"subnet\": \"192.0.3.0/24\" } "
+        "],"
+        "\"valid-lifetime\": 4000 }";
+
+    // Use this config to set up the server
+    ASSERT_NO_THROW(configure(config));
+
+    // Let's get the subnet configuration objects
+    const Subnet4Collection* subnets = CfgMgr::instance().getSubnets4();
+    ASSERT_EQ(2, subnets->size());
+
+    // Let's get them for easy reference
+    Subnet4Ptr subnet1 = (*subnets)[0];
+    Subnet4Ptr subnet2 = (*subnets)[1];
+    ASSERT_TRUE(subnet1);
+    ASSERT_TRUE(subnet2);
+
+    // Let's create a packet.
+    Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+    dis->setRemoteAddr(IOAddress("192.0.2.1"));
+    dis->setIface("eth0");
+    dis->setHops(1);
+    OptionPtr clientid = generateClientId();
+    dis->addOption(clientid);
+
+    // This is just a sanity check, we're using regular method: ciaddr 192.0.2.1
+    // belongs to the first subnet, so it is selected
+    dis->setGiaddr(IOAddress("192.0.2.1"));
+    EXPECT_TRUE(subnet1 == srv_.selectSubnet(dis));
+
+    // Relay belongs to the second subnet, so it  should be selected.
+    dis->setGiaddr(IOAddress("192.0.3.1"));
+    EXPECT_TRUE(subnet2 == srv_.selectSubnet(dis));
+
+    // Now let's check if the relay override for the first subnets works
+    dis->setGiaddr(IOAddress("192.0.5.1"));
+    EXPECT_TRUE(subnet1 == srv_.selectSubnet(dis));
+
+    // The same check for the second subnet...
+    dis->setGiaddr(IOAddress("192.0.5.2"));
+    EXPECT_TRUE(subnet2 == srv_.selectSubnet(dis));
+
+    // And finally, let's check if mis-matched relay address will end up
+    // in not selecting a subnet at all
+    dis->setGiaddr(IOAddress("192.0.5.3"));
+    EXPECT_FALSE(srv_.selectSubnet(dis));
+
+    // Finally, check that the relay override works only with relay address
+    // (GIADDR) and does not affect client address (CIADDR)
+    dis->setGiaddr(IOAddress("0.0.0.0"));
+    dis->setHops(0);
+    dis->setCiaddr(IOAddress("192.0.5.1"));
+    EXPECT_FALSE(srv_.selectSubnet(dis));
+}
+
+// Checks if relay IP address specified in the relay-info structure can be
+// used together with client-classification.
+TEST_F(Dhcpv4SrvTest, relayOverrideAndClientClass) {
+
+    // This test configures 2 subnets. They both are on the same link, so they
+    // have the same relay-ip address. Furthermore, the first subnet is
+    // reserved for clients that belong to class "foo".
+    string config = "{ \"interfaces\": [ \"*\" ],"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet4\": [ "
+        "{   \"pool\": [ \"192.0.2.2 - 192.0.2.100\" ],"
+        "    \"client-class\": \"foo\", "
+        "    \"relay\": { "
+        "        \"ip-address\": \"192.0.5.1\""
+        "    },"
+        "    \"subnet\": \"192.0.2.0/24\" }, "
+        "{   \"pool\": [ \"192.0.3.1 - 192.0.3.100\" ],"
+        "    \"relay\": { "
+        "        \"ip-address\": \"192.0.5.1\""
+        "    },"
+        "    \"subnet\": \"192.0.3.0/24\" } "
+        "],"
+        "\"valid-lifetime\": 4000 }";
+
+    // Use this config to set up the server
+    ASSERT_NO_THROW(configure(config));
+
+    const Subnet4Collection* subnets = CfgMgr::instance().getSubnets4();
+    ASSERT_EQ(2, subnets->size());
+
+    // Let's get them for easy reference
+    Subnet4Ptr subnet1 = (*subnets)[0];
+    Subnet4Ptr subnet2 = (*subnets)[1];
+    ASSERT_TRUE(subnet1);
+    ASSERT_TRUE(subnet2);
+
+    // Let's create a packet.
+    Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+    dis->setRemoteAddr(IOAddress("192.0.2.1"));
+    dis->setIface("eth0");
+    dis->setHops(1);
+    dis->setGiaddr(IOAddress("192.0.5.1"));
+    OptionPtr clientid = generateClientId();
+    dis->addOption(clientid);
+
+    // This packet does not belong to class foo, so it should be rejected in
+    // subnet[0], even though the relay-ip matches. It should be accepted in
+    // subnet[1], because the subnet matches and there are no class
+    // requirements.
+    EXPECT_TRUE(subnet2 == srv_.selectSubnet(dis));
+
+    // Now let's add this packet to class foo and recheck. This time it should
+    // be accepted in the first subnet, because both class and relay-ip match.
+    dis->addClass("foo");
+    EXPECT_TRUE(subnet1 == srv_.selectSubnet(dis));
 }
 
 // This test verifies that the direct message is dropped when it has been

+ 1 - 1
src/bin/dhcp4/tests/dhcp4_test_utils.h

@@ -195,11 +195,11 @@ public:
     using Dhcpv4Srv::sanityCheck;
     using Dhcpv4Srv::srvidToString;
     using Dhcpv4Srv::unpackOptions;
-    using Dhcpv4Srv::name_change_reqs_;
     using Dhcpv4Srv::classifyPacket;
     using Dhcpv4Srv::accept;
     using Dhcpv4Srv::acceptMessageType;
     using Dhcpv4Srv::selectSubnet;
+    using Dhcpv4Srv::VENDOR_CLASS_PREFIX;
 };
 
 class Dhcpv4SrvTest : public ::testing::Test {

+ 76 - 75
src/bin/dhcp4/tests/fqdn_unittest.cc

@@ -34,13 +34,17 @@ namespace {
 
 class NameDhcpv4SrvTest : public Dhcpv4SrvTest {
 public:
+    // Reference to D2ClientMgr singleton
+    D2ClientMgr& d2_mgr_;
+
     // Bit Constants for turning on and off DDNS configuration options.
     static const uint16_t ALWAYS_INCLUDE_FQDN = 1;
     static const uint16_t OVERRIDE_NO_UPDATE = 2;
     static const uint16_t OVERRIDE_CLIENT_UPDATE = 4;
     static const uint16_t REPLACE_CLIENT_NAME = 8;
 
-    NameDhcpv4SrvTest() : Dhcpv4SrvTest() {
+    NameDhcpv4SrvTest() : Dhcpv4SrvTest(),
+        d2_mgr_(CfgMgr::instance().getD2ClientMgr()) {
         srv_ = new NakedDhcpv4Srv(0);
         // Config DDNS to be enabled, all controls off
         enableD2();
@@ -48,6 +52,9 @@ public:
 
     virtual ~NameDhcpv4SrvTest() {
         delete srv_;
+        // CfgMgr singleton doesn't get wiped between tests, so  we'll
+        // disable D2 explictly between tests.
+        disableD2();
     }
 
     /// @brief Sets the server's DDNS configuration to ddns updates disabled.
@@ -69,15 +76,15 @@ public:
         D2ClientConfigPtr cfg;
 
         ASSERT_NO_THROW(cfg.reset(new D2ClientConfig(true,
-                                  isc::asiolink::IOAddress("192.0.2.1"), 477,
+                                  isc::asiolink::IOAddress("127.0.0.1"), 53001,
                                   dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
                                   (mask & ALWAYS_INCLUDE_FQDN),
                                   (mask & OVERRIDE_NO_UPDATE),
                                   (mask & OVERRIDE_CLIENT_UPDATE),
                                   (mask & REPLACE_CLIENT_NAME),
                                   "myhost", "example.com")));
-
-        CfgMgr::instance().setD2ClientConfig(cfg);
+        ASSERT_NO_THROW(CfgMgr::instance().setD2ClientConfig(cfg));
+        ASSERT_NO_THROW(srv_->startD2());
     }
 
     // Create a lease to be used by various tests.
@@ -111,11 +118,11 @@ public:
    }
 
     // Create an instance of the Hostname option.
-    OptionCustomPtr
+    OptionStringPtr
     createHostname(const std::string& hostname) {
-        OptionDefinition def("hostname", DHO_HOST_NAME, "string");
-        OptionCustomPtr opt_hostname(new OptionCustom(def, Option::V4));
-        opt_hostname->writeString(hostname);
+        OptionStringPtr opt_hostname(new OptionString(Option::V4,
+                                                      DHO_HOST_NAME,
+                                                      hostname));
         return (opt_hostname);
     }
 
@@ -140,9 +147,9 @@ public:
     }
 
     // get the Hostname option from the given message.
-    OptionCustomPtr getHostnameOption(const Pkt4Ptr& pkt) {
+    OptionStringPtr getHostnameOption(const Pkt4Ptr& pkt) {
         return (boost::dynamic_pointer_cast<
-                OptionCustom>(pkt->getOption(DHO_HOST_NAME)));
+                OptionString>(pkt->getOption(DHO_HOST_NAME)));
     }
 
     // Create a message holding DHCPv4 Client FQDN Option.
@@ -257,7 +264,7 @@ public:
     // the hostname option which would be sent to the client. It will
     // throw NULL pointer if the hostname option is not to be included
     // in the response.
-    OptionCustomPtr processHostname(const Pkt4Ptr& query) {
+    OptionStringPtr processHostname(const Pkt4Ptr& query) {
         if (!getHostnameOption(query)) {
             ADD_FAILURE() << "Hostname option not carried in the query";
         }
@@ -272,7 +279,7 @@ public:
         }
         srv_->processClientName(query, answer);
 
-        OptionCustomPtr hostname = getHostnameOption(answer);
+        OptionStringPtr hostname = getHostnameOption(answer);
         return (hostname);
 
     }
@@ -317,16 +324,19 @@ public:
                                  const time_t cltt,
                                  const uint16_t len,
                                  const bool not_strict_expire_check = false) {
-        NameChangeRequest ncr = srv_->name_change_reqs_.front();
-        EXPECT_EQ(type, ncr.getChangeType());
-        EXPECT_EQ(forward, ncr.isForwardChange());
-        EXPECT_EQ(reverse, ncr.isReverseChange());
-        EXPECT_EQ(addr, ncr.getIpAddress());
-        EXPECT_EQ(fqdn, ncr.getFqdn());
+        NameChangeRequestPtr ncr;
+        ASSERT_NO_THROW(ncr = d2_mgr_.peekAt(0));
+        ASSERT_TRUE(ncr);
+
+        EXPECT_EQ(type, ncr->getChangeType());
+        EXPECT_EQ(forward, ncr->isForwardChange());
+        EXPECT_EQ(reverse, ncr->isReverseChange());
+        EXPECT_EQ(addr, ncr->getIpAddress());
+        EXPECT_EQ(fqdn, ncr->getFqdn());
         // Compare dhcid if it is not empty. In some cases, the DHCID is
         // not known in advance and can't be compared.
         if (!dhcid.empty()) {
-            EXPECT_EQ(dhcid, ncr.getDhcid().toStr());
+            EXPECT_EQ(dhcid, ncr->getDhcid().toStr());
         }
         // In some cases, the test doesn't have access to the last transmission
         // time for the particular client. In such cases, the test can use the
@@ -334,13 +344,15 @@ public:
         // for equality but rather check that the lease expiration time is not
         // greater than the current time + lease lifetime.
         if (not_strict_expire_check) {
-            EXPECT_GE(cltt + len, ncr.getLeaseExpiresOn());
+            EXPECT_GE(cltt + len, ncr->getLeaseExpiresOn());
         } else {
-            EXPECT_EQ(cltt + len, ncr.getLeaseExpiresOn());
+            EXPECT_EQ(cltt + len, ncr->getLeaseExpiresOn());
         }
-        EXPECT_EQ(len, ncr.getLeaseLength());
-        EXPECT_EQ(isc::dhcp_ddns::ST_NEW, ncr.getStatus());
-        srv_->name_change_reqs_.pop();
+        EXPECT_EQ(len, ncr->getLeaseLength());
+        EXPECT_EQ(isc::dhcp_ddns::ST_NEW, ncr->getStatus());
+
+        // Process the message off the queue
+        ASSERT_NO_THROW(d2_mgr_.runReadyIO());
     }
 
 
@@ -373,20 +385,23 @@ public:
         checkResponse(reply, DHCPACK, 1234);
         checkFqdnFlags(reply, response_flags);
 
-        // There should be an NCR only if response S flag is 1.
-        /// @todo This logic will need to change if forward and reverse
-        /// updates are ever controlled independently.
-        if ((response_flags & Option4ClientFqdn::FLAG_S) == 0) {
-            ASSERT_EQ(0, srv_->name_change_reqs_.size());
-        } else {
-            // Verify that there is one NameChangeRequest generated as expected.
-            ASSERT_EQ(1, srv_->name_change_reqs_.size());
-            verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
-                                    reply->getYiaddr().toText(),
-                                    "myhost.example.com.",
-                                    "", // empty DHCID means don't check it
-                                    time(NULL) + subnet_->getValid(),
-                                    subnet_->getValid(), true);
+        // NCRs cannot be sent to the d2_mgr unless updates are enabled.
+        if (d2_mgr_.ddnsEnabled()) {
+            // There should be an NCR only if response S flag is 1.
+            /// @todo This logic will need to change if forward and reverse
+            /// updates are ever controlled independently.
+            if ((response_flags & Option4ClientFqdn::FLAG_S) == 0) {
+                ASSERT_EQ(0, d2_mgr_.getQueueSize());
+            } else {
+                // Verify that there is one NameChangeRequest as expected.
+                ASSERT_EQ(1, d2_mgr_.getQueueSize());
+                verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+                                        reply->getYiaddr().toText(),
+                                        "myhost.example.com.",
+                                        "", // empty DHCID means don't check it
+                                        time(NULL) + subnet_->getValid(),
+                                        subnet_->getValid(), true);
+            }
         }
     }
 
@@ -556,21 +571,12 @@ TEST_F(NameDhcpv4SrvTest, serverUpdateHostname) {
     Pkt4Ptr query;
     ASSERT_NO_THROW(query = generatePktWithHostname(DHCPREQUEST,
                                                     "myhost.example.com."));
-    OptionCustomPtr hostname;
+    OptionStringPtr hostname;
     ASSERT_NO_THROW(hostname = processHostname(query));
 
     ASSERT_TRUE(hostname);
-    EXPECT_EQ("myhost.example.com.", hostname->readString());
-
-}
+    EXPECT_EQ("myhost.example.com.", hostname->getValue());
 
-// Test that the server skips processing of the empty Hostname option.
-TEST_F(NameDhcpv4SrvTest, serverUpdateEmptyHostname) {
-    Pkt4Ptr query;
-    ASSERT_NO_THROW(query = generatePktWithHostname(DHCPREQUEST, ""));
-    OptionCustomPtr hostname;
-    ASSERT_NO_THROW(hostname = processHostname(query));
-    EXPECT_FALSE(hostname);
 }
 
 // Test that the server skips processing of a wrong Hostname option.
@@ -578,7 +584,7 @@ TEST_F(NameDhcpv4SrvTest, serverUpdateWrongHostname) {
     Pkt4Ptr query;
     ASSERT_NO_THROW(query = generatePktWithHostname(DHCPREQUEST,
                                                     "abc..example.com"));
-    OptionCustomPtr hostname;
+    OptionStringPtr hostname;
     ASSERT_NO_THROW(hostname = processHostname(query));
     EXPECT_FALSE(hostname);
 }
@@ -605,11 +611,11 @@ TEST_F(NameDhcpv4SrvTest, serverUpdateForwardPartialNameFqdn) {
 TEST_F(NameDhcpv4SrvTest, serverUpdateUnqualifiedHostname) {
     Pkt4Ptr query;
     ASSERT_NO_THROW(query = generatePktWithHostname(DHCPREQUEST, "myhost"));
-    OptionCustomPtr hostname;
+    OptionStringPtr hostname;
     ASSERT_NO_THROW(hostname =  processHostname(query));
 
     ASSERT_TRUE(hostname);
-    EXPECT_EQ("myhost.example.com.", hostname->readString());
+    EXPECT_EQ("myhost.example.com.", hostname->getValue());
 
 }
 
@@ -639,7 +645,7 @@ TEST_F(NameDhcpv4SrvTest, createNameChangeRequestsNewLease) {
     Lease4Ptr old_lease;
 
     ASSERT_NO_THROW(srv_->createNameChangeRequests(lease, old_lease));
-    ASSERT_EQ(1, srv_->name_change_reqs_.size());
+    ASSERT_EQ(1, d2_mgr_.getQueueSize());
 
     verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
                             "192.0.2.3", "myhost.example.com.",
@@ -658,7 +664,7 @@ TEST_F(NameDhcpv4SrvTest, createNameChangeRequestsRenewNoChange) {
     old_lease->valid_lft_ += 100;
 
     ASSERT_NO_THROW(srv_->createNameChangeRequests(lease, old_lease));
-    EXPECT_TRUE(srv_->name_change_reqs_.empty());
+    ASSERT_EQ(0, d2_mgr_.getQueueSize());
 }
 
 // Test that no NameChangeRequest is generated when forward and reverse
@@ -671,7 +677,7 @@ TEST_F(NameDhcpv4SrvTest, createNameChangeRequestsNoUpdate) {
                                    "lease2.example.com.",
                                    false, false);
     ASSERT_NO_THROW(srv_->createNameChangeRequests(lease2, lease1));
-    EXPECT_EQ(1, srv_->name_change_reqs_.size());
+    EXPECT_EQ(1, d2_mgr_.getQueueSize());
 
     verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, true,
                             "192.0.2.3", "lease1.example.com.",
@@ -683,7 +689,7 @@ TEST_F(NameDhcpv4SrvTest, createNameChangeRequestsNoUpdate) {
     lease2->fqdn_rev_ = true;
     lease2->fqdn_fwd_ = true;
     ASSERT_NO_THROW(srv_->createNameChangeRequests(lease2, lease1));
-    EXPECT_EQ(1, srv_->name_change_reqs_.size());
+    EXPECT_EQ(1, d2_mgr_.getQueueSize());
 
 }
 
@@ -697,7 +703,7 @@ TEST_F(NameDhcpv4SrvTest, createNameChangeRequestsRenew) {
                                    "lease2.example.com.",
                                    true, true);
     ASSERT_NO_THROW(srv_->createNameChangeRequests(lease2, lease1));
-    ASSERT_EQ(2, srv_->name_change_reqs_.size());
+    ASSERT_EQ(2, d2_mgr_.getQueueSize());
 
     verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, true,
                             "192.0.2.3", "lease1.example.com.",
@@ -742,7 +748,7 @@ TEST_F(NameDhcpv4SrvTest, processDiscover) {
     ASSERT_NO_THROW(reply = srv_->processDiscover(req));
     checkResponse(reply, DHCPOFFER, 1234);
 
-    EXPECT_TRUE(srv_->name_change_reqs_.empty());
+    EXPECT_EQ(0, d2_mgr_.getQueueSize());
 }
 
 // Test that server generates client's hostname from the IP address assigned
@@ -761,7 +767,7 @@ TEST_F(NameDhcpv4SrvTest, processRequestFqdnEmptyDomainName) {
     checkResponse(reply, DHCPACK, 1234);
 
     // Verify that there is one NameChangeRequest generated.
-    ASSERT_EQ(1, srv_->name_change_reqs_.size());
+    ASSERT_EQ(1, d2_mgr_.getQueueSize());
 
     // The hostname is generated from the IP address acquired (yiaddr).
     std::string hostname = generatedNameFromAddress(reply->getYiaddr());
@@ -803,7 +809,7 @@ TEST_F(NameDhcpv4SrvTest, processRequestEmptyDomainNameDisabled) {
 
 // Test that server generates client's hostname from the IP address assigned
 // to it when Hostname option carries the top level domain-name.
-TEST_F(NameDhcpv4SrvTest, processRequestEmptyHostname) {
+TEST_F(NameDhcpv4SrvTest, processRequestTopLevelHostname) {
     IfaceMgrTestConfig test_config(true);
     IfaceMgr::instance().openSockets4();
 
@@ -818,7 +824,7 @@ TEST_F(NameDhcpv4SrvTest, processRequestEmptyHostname) {
     checkResponse(reply, DHCPACK, 1234);
 
     // Verify that there is one NameChangeRequest generated.
-    ASSERT_EQ(1, srv_->name_change_reqs_.size());
+    ASSERT_EQ(1, d2_mgr_.getQueueSize());
 
     // The hostname is generated from the IP address acquired (yiaddr).
     std::string hostname = generatedNameFromAddress(reply->getYiaddr());
@@ -848,7 +854,7 @@ TEST_F(NameDhcpv4SrvTest, processTwoRequestsFqdn) {
     checkResponse(reply, DHCPACK, 1234);
 
     // Verify that there is one NameChangeRequest generated.
-    ASSERT_EQ(1, srv_->name_change_reqs_.size());
+    ASSERT_EQ(1, d2_mgr_.getQueueSize());
     verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
                             reply->getYiaddr().toText(), "myhost.example.com.",
                             "00010132E91AA355CFBB753C0F0497A5A940436"
@@ -868,7 +874,7 @@ TEST_F(NameDhcpv4SrvTest, processTwoRequestsFqdn) {
     checkResponse(reply, DHCPACK, 1234);
 
     // There should be two NameChangeRequests. Verify that they are valid.
-    ASSERT_EQ(2, srv_->name_change_reqs_.size());
+    ASSERT_EQ(2, d2_mgr_.getQueueSize());
     verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, true,
                             reply->getYiaddr().toText(),
                             "myhost.example.com.",
@@ -905,7 +911,7 @@ TEST_F(NameDhcpv4SrvTest, processTwoRequestsHostname) {
     checkResponse(reply, DHCPACK, 1234);
 
     // Verify that there is one NameChangeRequest generated.
-    ASSERT_EQ(1, srv_->name_change_reqs_.size());
+    ASSERT_EQ(1, d2_mgr_.getQueueSize());
     verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
                             reply->getYiaddr().toText(), "myhost.example.com.",
                             "00010132E91AA355CFBB753C0F0497A5A940436"
@@ -926,7 +932,7 @@ TEST_F(NameDhcpv4SrvTest, processTwoRequestsHostname) {
     checkResponse(reply, DHCPACK, 1234);
 
     // There should be two NameChangeRequests. Verify that they are valid.
-    ASSERT_EQ(2, srv_->name_change_reqs_.size());
+    ASSERT_EQ(2, d2_mgr_.getQueueSize());
     verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, true,
                             reply->getYiaddr().toText(),
                             "myhost.example.com.",
@@ -962,7 +968,7 @@ TEST_F(NameDhcpv4SrvTest, processRequestRelease) {
     checkResponse(reply, DHCPACK, 1234);
 
     // Verify that there is one NameChangeRequest generated for lease.
-    ASSERT_EQ(1, srv_->name_change_reqs_.size());
+    ASSERT_EQ(1, d2_mgr_.getQueueSize());
     verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
                             reply->getYiaddr().toText(), "myhost.example.com.",
                             "00010132E91AA355CFBB753C0F0497A5A940436"
@@ -979,7 +985,7 @@ TEST_F(NameDhcpv4SrvTest, processRequestRelease) {
 
     // The lease has been removed, so there should be a NameChangeRequest to
     // remove corresponding DNS entries.
-    ASSERT_EQ(1, srv_->name_change_reqs_.size());
+    ASSERT_EQ(1, d2_mgr_.getQueueSize());
     verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, true,
                             reply->getYiaddr().toText(), "myhost.example.com.",
                             "00010132E91AA355CFBB753C0F0497A5A940436"
@@ -990,6 +996,9 @@ TEST_F(NameDhcpv4SrvTest, processRequestRelease) {
 // Test that when the Release message is sent for a previously acquired lease
 // and DDNS updates are disabled that server does NOT generate a
 // NameChangeRequest to remove entries corresponding to the released lease.
+// Queue size is not available when updates are not enabled, however,
+// attempting to send a NCR when updates disabled will result in a throw.
+// If no throws are experienced then no attempt was made to send a NCR.
 TEST_F(NameDhcpv4SrvTest, processRequestReleaseUpdatesDisabled) {
     // Create fake interfaces and open fake sockets.
     IfaceMgrTestConfig test_config(true);
@@ -1008,10 +1017,6 @@ TEST_F(NameDhcpv4SrvTest, processRequestReleaseUpdatesDisabled) {
     ASSERT_NO_THROW(reply = srv_->processRequest(req));
     checkResponse(reply, DHCPACK, 1234);
 
-    // With DDNS updates disabled, there should be not be a NameChangeRequest
-    // for the add.
-    ASSERT_EQ(0, srv_->name_change_reqs_.size());
-
     // Create and process the Release message.
     Pkt4Ptr rel = Pkt4Ptr(new Pkt4(DHCPRELEASE, 1234));
     rel->setCiaddr(reply->getYiaddr());
@@ -1019,10 +1024,6 @@ TEST_F(NameDhcpv4SrvTest, processRequestReleaseUpdatesDisabled) {
     rel->addOption(generateClientId());
     rel->addOption(srv_->getServerID());
     ASSERT_NO_THROW(srv_->processRelease(rel));
-
-    // With DDNS updates disabled, there should be not be a NameChangeRequest
-    // for the remove.
-    ASSERT_EQ(0, srv_->name_change_reqs_.size());
 }
 
 

+ 2 - 0
src/bin/dhcp6/config_parser.cc

@@ -640,6 +640,8 @@ DhcpConfigParser* createGlobal6DhcpConfigParser(const std::string& config_id) {
         parser = new DbAccessParser(config_id);
     } else if (config_id.compare("hooks-libraries") == 0) {
         parser = new HooksLibrariesParser(config_id);
+    } else if (config_id.compare("dhcp-ddns") == 0) {
+        parser = new D2ClientConfigParser(config_id);
     } else {
         isc_throw(NotImplemented,
                 "Parser error: Global configuration parameter not supported: "

+ 14 - 0
src/bin/dhcp6/ctrl_dhcp6_srv.cc

@@ -114,6 +114,16 @@ ControlledDhcpv6Srv::dhcp6ConfigHandler(ConstElementPtr new_config) {
         return (answer);
     }
 
+    // Server will start DDNS communications if its enabled.
+    try {
+        server_->startD2();
+    } catch (const std::exception& ex) {
+        std::ostringstream err;
+        err << "error starting DHCP_DDNS client "
+                " after server reconfiguration: " << ex.what();
+        return (isc::config::createAnswer(1, err.str()));
+    }
+
     // Configuration may change active interfaces. Therefore, we have to reopen
     // sockets according to new configuration. This operation is not exception
     // safe and we really don't want to emit exceptions to the callback caller.
@@ -212,6 +222,10 @@ void ControlledDhcpv6Srv::establishSession() {
     try {
         // Pull the full configuration out from the session.
         configureDhcp6Server(*this, config_session_->getFullConfig());
+
+        // Server will start DDNS communications if its enabled.
+        server_->startD2();
+
         // Configuration may disable or enable interfaces so we have to
         // reopen sockets according to new configuration.
         openActiveSockets(getPort());

+ 88 - 1
src/bin/dhcp6/dhcp6.spec

@@ -352,7 +352,94 @@
                   }
                 } ]
             }
-       }
+      },
+      { "item_name": "dhcp-ddns",
+        "item_type": "map",
+        "item_optional": false,
+        "item_default": {"enable-updates": false},
+        "item_description" : "Contains parameters pertaining DHCP-driven DDNS updates",
+        "map_item_spec": [
+            {
+                "item_name": "enable-updates",
+                "item_type": "boolean",
+                "item_optional": false,
+                "item_default": false,
+                "item_description" : "Enables DDNS update processing"
+            },
+            {
+                "item_name": "server-ip",
+                "item_type": "string",
+                "item_optional": true,
+                "item_default": "127.0.0.1",
+                "item_description" : "IP address of b10-dhcp-ddns (IPv4 or IPv6)"
+            },
+            {
+                "item_name": "server-port",
+                "item_type": "integer",
+                "item_optional": true,
+                "item_default": 53001,
+                "item_description" : "port number of b10-dhcp-ddns"
+            },
+            {
+                "item_name": "ncr-protocol",
+                "item_type": "string",
+                "item_optional": true,
+                "item_default": "UDP",
+                "item_description" : "Socket protocol to use with b10-dhcp-ddns"
+            },
+            {
+                "item_name": "ncr-format",
+                "item_type": "string",
+                "item_optional": true,
+                "item_default": "JSON",
+                "item_description" : "Format of the update request packet"
+            },
+            {
+
+                "item_name": "always-include-fqdn",
+                "item_type": "boolean",
+                "item_optional": true,
+                "item_default": false,
+                "item_description": "Enable always including the FQDN option in its response"
+            },
+            {
+                "item_name": "override-no-update",
+                "item_type": "boolean",
+                "item_optional": true,
+                "item_default": false,
+                "item_description": "Do update, even if client requested no updates with N flag"
+            },
+            {
+                "item_name": "override-client-update",
+                "item_type": "boolean",
+                "item_optional": true,
+                "item_default": false,
+                "item_description": "Server performs an update even if client requested delegation"
+            },
+            {
+                "item_name": "replace-client-name",
+                "item_type": "boolean",
+                "item_optional": true,
+                "item_default": false,
+                "item_description": "Should server replace the domain-name supplied by the client"
+            },
+            {
+                "item_name": "generated-prefix",
+                "item_type": "string",
+                "item_optional": true,
+                "item_default": "myhost",
+                "item_description": "Prefix to use when generating the client's name"
+            },
+
+            {
+                "item_name": "qualifying-suffix",
+                "item_type": "string",
+                "item_optional": true,
+                "item_default": "example.com",
+                "item_description": "Fully qualified domain-name suffix if partial name provided by client"
+            },
+        ]
+      },
     ],
     "commands": [
         {

+ 76 - 65
src/bin/dhcp6/dhcp6_messages.mes

@@ -91,15 +91,6 @@ New  values: hostname = %2, reverse mapping = %3, forward mapping = %4
 This debug message is logged when FQDN mapping for a particular lease has been
 changed by the recent Renew message. This mapping will be changed in DNS.
 
-% DHCP6_DEACTIVATE_INTERFACE deactivate interface %1
-This message is printed when DHCPv6 server disables an interface from being
-used to receive DHCPv6 traffic. Sockets on this interface will not be opened
-by the Interface Manager until interface is enabled.
-
-% DHCP6_DDNS_SEND_FQDN sending DHCPv6 Client FQDN Option to the client: %1
-This debug message is logged when server includes an DHCPv6 Client FQDN Option
-in its response to the client.
-
 % DHCP6_DDNS_RECEIVE_FQDN received DHCPv6 Client FQDN Option: %1
 This debug message is logged when server has found the DHCPv6 Client FQDN Option
 sent by a client and started processing it.
@@ -110,6 +101,25 @@ that the DNS Update has been performed for it, but the FQDN held in the lease
 database has invalid format and can't be transformed to the canonical on-wire
 format.
 
+% DHCP6_DDNS_REQUEST_SEND_FAILED failed sending a request to b10-dhcp-ddns, error: %1,  ncr: %2
+This error message indicates that IPv6 DHCP server failed to send a DDNS
+update reqeust to the DHCP-DDNS server. This is most likely a configuration or
+networking error.
+
+% DHCP6_DDNS_SEND_FQDN sending DHCPv6 Client FQDN Option to the client: %1
+This debug message is logged when server includes an DHCPv6 Client FQDN Option
+in its response to the client.
+
+% DHCP6_DEACTIVATE_INTERFACE deactivate interface %1
+This message is printed when DHCPv6 server disables an interface from being
+used to receive DHCPv6 traffic. Sockets on this interface will not be opened
+by the Interface Manager until interface is enabled.
+
+% DHCP6_HOOKS_LIBS_RELOAD_FAIL reload of hooks libraries failed
+A "libreload" command was issued to reload the hooks libraries but for
+some reason the reload failed.  Other error messages issued from the
+hooks framework will indicate the nature of the problem.
+
 % DHCP6_EXTEND_LEASE_SUBNET_SELECTED the %1 subnet was selected for client extending its lease
 This is a debug message informing that a given subnet was selected. It will
 be used for extending lifetime of the lease. This is one of the early steps
@@ -172,13 +182,11 @@ Server completed all the processing (e.g. may have assigned, updated
 or released leases), but the response will not be send to the client.
 
 % DHCP6_HOOK_LEASE6_EXTEND_SKIP DHCPv6 lease lifetime was not extended because a callout set the skip flag for message %1
-This debug message is printed when a callout installed on lease6_renew
 or lease6_rebind hook point set the skip flag. For this particular hook
 point, the setting of the flag by a callout instructs the server to not
 extend the lifetime for a lease. If client requested renewal of multiples
 leases (by sending multiple IA options), the server will skip the renewal
 of the one in question and will proceed with other renewals as usual.
-
 % DHCP6_HOOK_LEASE6_RELEASE_NA_SKIP DHCPv6 address lease was not released because a callout set the skip flag
 This debug message is printed when a callout installed on the
 lease6_release hook point set the skip flag. For this particular hook
@@ -195,6 +203,14 @@ a lease. If client requested release of multiples leases (by sending
 multiple IA options), the server will retains this particular lease and
 will proceed with other renewals as usual.
 
+% DHCP6_HOOK_LEASE6_RENEW_SKIP DHCPv6 lease was not renewed because a callout set the skip flag
+This debug message is printed when a callout installed on lease6_renew
+hook point set the skip flag. For this particular hook point, the setting
+of the flag by a callout instructs the server to not renew a lease. If
+client requested renewal of multiples leases (by sending multiple IA
+options), the server will skip the renewal of the one in question and
+will proceed with other renewals as usual.
+
 % DHCP6_HOOK_PACKET_RCVD_SKIP received DHCPv6 packet was dropped because a callout set the skip flag
 This debug message is printed when a callout installed on the pkt6_receive
 hook point set the skip flag. For this particular hook point, the
@@ -216,56 +232,29 @@ subnet, an action that severely limits further processing; the server
 will be only able to offer global options - no addresses or prefixes
 will be assigned.
 
-% DHCP6_HOOKS_LIBS_RELOAD_FAIL reload of hooks libraries failed
-A "libreload" command was issued to reload the hooks libraries but for
-some reason the reload failed.  Other error messages issued from the
-hooks framework will indicate the nature of the problem.
-
 % DHCP6_LEASE_ADVERT address lease %1 advertised (client duid=%2, iaid=%3)
 This debug message indicates that the server successfully advertised
 an address lease. It is up to the client to choose one server out of the
 advertised servers and continue allocation with that server. This
 is a normal behavior and indicates successful operation.
 
-% DHCP6_PD_LEASE_ADVERT prefix lease %1/%2 advertised (client duid=%3, iaid=%4)
-This debug message indicates that the server successfully advertised
-a prefix lease. It is up to the client to choose one server out of the
-advertised servers and continue allocation with that server. This
-is a normal behavior and indicates successful operation.
-
 % DHCP6_LEASE_ADVERT_FAIL failed to advertise an address lease for client duid=%1, iaid=%2
 This message indicates that in response to a received SOLICIT, the server
 failed to advertise a non-temporary lease for a given client. There may
 be many reasons for such failure. Each failure is logged in a separate
 log entry.
 
-% DHCP6_PD_LEASE_ADVERT_FAIL failed to advertise a prefix lease for client duid=%1, iaid=%2
-This message indicates that in response to a received SOLICIT, the
-server failed to advertise a prefix lease for the client. There may
-be many reasons for such failure. Each failure is logged in a separate
-log entry.
-
 % DHCP6_LEASE_ALLOC address lease %1 has been allocated (client duid=%2, iaid=%3)
 This debug message indicates that in response to a client's REQUEST
 message, the server successfully granted an non-temporary address
 lease. This is a normal behavior and indicates successful operation.
 
-% DHCP6_PD_LEASE_ALLOC prefix lease %1/%2 has been allocated (client duid=%3, iaid=%4)
-This debug message indicates that in response to a client's REQUEST
-message, the server successfully granted a prefix delegation lease. This
-is a normal behavior and indicates successful operation.
-
 % DHCP6_LEASE_ALLOC_FAIL failed to grant an address lease for client duid=%1, iaid=%2
 This message indicates that in response to a received REQUEST, the server
 failed to grant a non-temporary address lease for the client. There may
 be many reasons for such failure. Each failure is logged in a separate
 log entry.
 
-% DHCP6_PD_LEASE_ALLOC_FAIL failed to grant a prefix lease for client duid=%1, iaid=%2
-This message indicates that the server failed to grant (in response to
-received REQUEST) a prefix lease for a given client. There may be many reasons
-for such failure. Each failure is logged in a separate log entry.
-
 % DHCP6_LEASE_NA_WITHOUT_DUID address lease for address %1 does not have a DUID
 This error message indicates a database consistency problem. The lease
 database has an entry indicating that the given address is in use,
@@ -306,6 +295,10 @@ server is about to open sockets on the specified port.
 A warning message issued when IfaceMgr fails to open and bind a socket. The reason
 for the failure is appended as an argument of the log message.
 
+% DHCP6_PACKET_MISMATCH_SERVERID_DROP dropping packet %1 (transid=%2, interface=%3) having mismatched server identifier
+A debug message noting that server has received message with server identifier
+option that not matching server identifier that server is using.
+
 % DHCP6_PACKET_PARSE_FAIL failed to parse incoming packet
 The IPv6 DHCP server has received a packet that it is unable to interpret.
 
@@ -320,10 +313,6 @@ of packet.  Note that a packet marked as UNKNOWN may well be a valid
 DHCP packet, just a type not expected by the server (e.g. it will report
 a received OFFER packet as UNKNOWN).
 
-% DHCP6_PACKET_MISMATCH_SERVERID_DROP dropping packet %1 (transid=%2, interface=%3) having mismatched server identifier
-A debug message noting that server has received message with server identifier
-option that not matching server identifier that server is using.
-
 % DHCP6_PACKET_RECEIVE_FAIL error on attempt to receive packet: %1
 The IPv6 DHCP server tried to receive a packet but an error
 occurred during this attempt. The reason for the error is included in
@@ -376,6 +365,28 @@ of the named configuration element, or the creation succeeded but the
 parsing actions and committal of changes failed.  The reason for the
 failure is given in the message.
 
+% DHCP6_PD_LEASE_ADVERT prefix lease %1/%2 advertised (client duid=%3, iaid=%4)
+This debug message indicates that the server successfully advertised
+a prefix lease. It is up to the client to choose one server out of the
+advertised servers and continue allocation with that server. This
+is a normal behavior and indicates successful operation.
+
+% DHCP6_PD_LEASE_ADVERT_FAIL failed to advertise a prefix lease for client duid=%1, iaid=%2
+This message indicates that in response to a received SOLICIT, the
+server failed to advertise a prefix lease for the client. There may
+be many reasons for such failure. Each failure is logged in a separate
+log entry.
+
+% DHCP6_PD_LEASE_ALLOC prefix lease %1/%2 has been allocated (client duid=%3, iaid=%4)
+This debug message indicates that in response to a client's REQUEST
+message, the server successfully granted a prefix delegation lease. This
+is a normal behavior and indicates successful operation.
+
+% DHCP6_PD_LEASE_ALLOC_FAIL failed to grant a prefix lease for client duid=%1, iaid=%2
+This message indicates that the server failed to grant (in response to
+received REQUEST) a prefix lease for a given client. There may be many reasons
+for such failure. Each failure is logged in a separate log entry.
+
 % DHCP6_PROCESS_IA_NA_REQUEST server is processing IA_NA option (duid=%1, iaid=%2, hint=%3)
 This is a debug message that indicates the processing of a received
 IA_NA option. It may optionally contain an address that may be used by
@@ -389,14 +400,16 @@ as a hint for possible requested prefix.
 % DHCP6_QUERY_DATA received packet length %1, data length %2, data is %3
 A debug message listing the data received from the client or relay.
 
+% DHCP6_RELEASE_MISSING_CLIENTID client (address=%1) sent RELEASE message without mandatory client-id
+This warning message indicates that client sent RELEASE message without
+mandatory client-id option. This is most likely caused by a buggy client
+(or a relay that malformed forwarded message). This request will not be
+processed and a response with error status code will be sent back.
+
 % DHCP6_RELEASE_NA address %1 belonging to client duid=%2, iaid=%3 was released properly
 This debug message indicates that an address was released properly. It
 is a normal operation during client shutdown.
 
-% DHCP6_RELEASE_PD prefix %1 belonging to client duid=%2, iaid=%3 was released properly
-This debug message indicates that a prefix was released properly. It
-is a normal operation during client shutdown.
-
 % DHCP6_RELEASE_NA_FAIL failed to remove address lease for address %1 for duid=%2, iaid=%3
 This error message indicates that the software failed to remove an address
 lease from the lease database.  It probably due to an error during a
@@ -405,6 +418,23 @@ intervention (e.g. check if DHCP process has sufficient privileges to
 update the database). It may also be triggered if a lease was manually
 removed from the database during RELEASE message processing.
 
+% DHCP6_RELEASE_NA_FAIL_WRONG_DUID client (duid=%1) tried to release address %2, but it belongs to another client (duid=%3)
+This warning message indicates that a client tried to release an address
+that belongs to a different client. This should not happen in normal
+circumstances and may indicate a misconfiguration of the client.  However,
+since the client releasing the address will stop using it anyway, there
+is a good chance that the situation will correct itself.
+
+% DHCP6_RELEASE_NA_FAIL_WRONG_IAID client (duid=%1) tried to release address %2, but it used wrong IAID (expected %3, but got %4)
+This warning message indicates that client tried to release an address
+that does belong to it, but the address was expected to be in a different
+IA (identity association) container. This probably means that the client's
+support for multiple addresses is flawed.
+
+% DHCP6_RELEASE_PD prefix %1 belonging to client duid=%2, iaid=%3 was released properly
+This debug message indicates that a prefix was released properly. It
+is a normal operation during client shutdown.
+
 % DHCP6_RELEASE_PD_FAIL failed to remove prefix lease for address %1 for duid=%2, iaid=%3
 This error message indicates that the software failed to remove a prefix
 lease from the lease database.  It probably due to an error during a
@@ -413,13 +443,6 @@ intervention (e.g. check if DHCP process has sufficient privileges to
 update the database). It may also be triggered if a lease was manually
 removed from the database during RELEASE message processing.
 
-% DHCP6_RELEASE_NA_FAIL_WRONG_DUID client (duid=%1) tried to release address %2, but it belongs to another client (duid=%3)
-This warning message indicates that a client tried to release an address
-that belongs to a different client. This should not happen in normal
-circumstances and may indicate a misconfiguration of the client.  However,
-since the client releasing the address will stop using it anyway, there
-is a good chance that the situation will correct itself.
-
 % DHCP6_RELEASE_PD_FAIL_WRONG_DUID client (duid=%1) tried to release prefix %2, but it belongs to another client (duid=%3)
 This warning message indicates that client tried to release a prefix
 that belongs to a different client. This should not happen in normal
@@ -427,24 +450,12 @@ circumstances and may indicate a misconfiguration of the client.  However,
 since the client releasing the prefix will stop using it anyway, there
 is a good chance that the situation will correct itself.
 
-% DHCP6_RELEASE_NA_FAIL_WRONG_IAID client (duid=%1) tried to release address %2, but it used wrong IAID (expected %3, but got %4)
-This warning message indicates that client tried to release an address
-that does belong to it, but the address was expected to be in a different
-IA (identity association) container. This probably means that the client's
-support for multiple addresses is flawed.
-
 % DHCP6_RELEASE_PD_FAIL_WRONG_IAID client (duid=%1) tried to release prefix %2, but it used wrong IAID (expected %3, but got %4)
 This warning message indicates that client tried to release a prefix
 that does belong to it, but the address was expected to be in a different
 IA (identity association) container. This probably means that the client's
 support for multiple prefixes is flawed.
 
-% DHCP6_RELEASE_MISSING_CLIENTID client (address=%1) sent RELEASE message without mandatory client-id
-This warning message indicates that client sent RELEASE message without
-mandatory client-id option. This is most likely caused by a buggy client
-(or a relay that malformed forwarded message). This request will not be
-processed and a response with error status code will be sent back.
-
 % DHCP6_REQUIRED_OPTIONS_CHECK_FAIL %1 message received from %2 failed the following check: %3
 This message indicates that received DHCPv6 packet is invalid.  This may be due
 to a number of reasons, e.g. the mandatory client-id option is missing,

+ 84 - 163
src/bin/dhcp6/dhcp6_srv.cc

@@ -28,6 +28,7 @@
 #include <dhcp/option6_iaprefix.h>
 #include <dhcp/option_custom.h>
 #include <dhcp/option_vendor.h>
+#include <dhcp/option_vendor_class.h>
 #include <dhcp/option_int_array.h>
 #include <dhcp/pkt6.h>
 #include <dhcp6/dhcp6_log.h>
@@ -54,6 +55,7 @@
 #include <time.h>
 #include <iomanip>
 #include <fstream>
+#include <sstream>
 
 using namespace isc;
 using namespace isc::asiolink;
@@ -100,30 +102,7 @@ Dhcp6Hooks Hooks;
 namespace isc {
 namespace dhcp {
 
-namespace {
-
-// The following constants describe server's behavior with respect to the
-// DHCPv6 Client FQDN Option sent by a client. They will be removed
-// when DDNS parameters for DHCPv6 are implemented with the ticket #3034.
-
-// Enable AAAA RR update delegation to the client (Disabled).
-const bool FQDN_ALLOW_CLIENT_UPDATE = false;
-// Globally enable updates (Enabled).
-const bool FQDN_ENABLE_UPDATE = true;
-// The partial name generated for the client if empty name has been
-// supplied.
-const char* FQDN_GENERATED_PARTIAL_NAME = "myhost";
-// Do update, even if client requested no updates with N flag (Disabled).
-const bool FQDN_OVERRIDE_NO_UPDATE = false;
-// Server performs an update when client requested delegation (Enabled).
-const bool FQDN_OVERRIDE_CLIENT_UPDATE = true;
-// The fully qualified domain-name suffix if partial name provided by
-// a client.
-const char* FQDN_PARTIAL_SUFFIX = "example.com";
-// Should server replace the domain-name supplied by the client (Disabled).
-const bool FQDN_REPLACE_CLIENT_NAME = false;
-
-}
+const std::string Dhcpv6Srv::VENDOR_CLASS_PREFIX("VENDOR_CLASS_");
 
 /// @brief file name of a server-id file
 ///
@@ -543,9 +522,6 @@ bool Dhcpv6Srv::run() {
                 LOG_ERROR(dhcp6_logger, DHCP6_PACKET_SEND_FAIL)
                     .arg(e.what());
             }
-
-            // Send NameChangeRequests to the b10-dhcp-ddns module.
-            sendNameChangeRequests();
         }
     }
 
@@ -913,7 +889,7 @@ Dhcpv6Srv::selectSubnet(const Pkt6Ptr& question) {
             // if relay filled in link_addr field, then let's use it
             if (link_addr != IOAddress("::")) {
                 subnet = CfgMgr::instance().getSubnet6(link_addr,
-                                                       question->classes_);
+                                                       question->classes_, true);
             }
         }
     }
@@ -1040,69 +1016,18 @@ Dhcpv6Srv::processClientFqdn(const Pkt6Ptr& question, const Pkt6Ptr& answer) {
 
     LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL,
               DHCP6_DDNS_RECEIVE_FQDN).arg(fqdn->toText());
-
-
-    // Prepare the FQDN option which will be included in the response to
-    // the client.
+    // Create the DHCPv6 Client FQDN Option to be included in the server's
+    // response to a client.
     Option6ClientFqdnPtr fqdn_resp(new Option6ClientFqdn(*fqdn));
-    // RFC 4704, section 6. - all flags set to 0.
-    fqdn_resp->resetFlags();
-
-    // Conditions when N flag has to be set to indicate that server will not
-    // perform DNS updates:
-    // 1. Updates are globally disabled,
-    // 2. Client requested no update and server respects it,
-    // 3. Client requested that the AAAA update is delegated to the client but
-    //    server neither respects delegation of updates nor it is configured
-    //    to send update on its own when client requested delegation.
-    if (!FQDN_ENABLE_UPDATE ||
-        (fqdn->getFlag(Option6ClientFqdn::FLAG_N) &&
-         !FQDN_OVERRIDE_NO_UPDATE) ||
-        (!fqdn->getFlag(Option6ClientFqdn::FLAG_S) &&
-         !FQDN_ALLOW_CLIENT_UPDATE && !FQDN_OVERRIDE_CLIENT_UPDATE)) {
-        fqdn_resp->setFlag(Option6ClientFqdn::FLAG_N, true);
-
-    // Conditions when S flag is set to indicate that server will perform
-    // DNS update on its own:
-    // 1. Client requested that server performs DNS update and DNS updates are
-    //    globally enabled
-    // 2. Client requested that server delegates AAAA update to the client but
-    //    server doesn't respect delegation and it is configured to perform
-    //    an update on its own when client requested delegation.
-    } else if (fqdn->getFlag(Option6ClientFqdn::FLAG_S) ||
-               (!fqdn->getFlag(Option6ClientFqdn::FLAG_S) &&
-                !FQDN_ALLOW_CLIENT_UPDATE && FQDN_OVERRIDE_CLIENT_UPDATE)) {
-        fqdn_resp->setFlag(Option6ClientFqdn::FLAG_S, true);
-    }
-
-    // Server MUST set the O flag if it has overridden the client's setting
-    // of S flag.
-    if (fqdn->getFlag(Option6ClientFqdn::FLAG_S) !=
-        fqdn_resp->getFlag(Option6ClientFqdn::FLAG_S)) {
-        fqdn_resp->setFlag(Option6ClientFqdn::FLAG_O, true);
-    }
-
-    // If client supplied partial or empty domain-name, server should
-    // generate one.
-    if (fqdn->getDomainNameType() == Option6ClientFqdn::PARTIAL) {
-        std::ostringstream name;
-        if (fqdn->getDomainName().empty() || FQDN_REPLACE_CLIENT_NAME) {
-            fqdn->setDomainName("", Option6ClientFqdn::PARTIAL);
-
-        } else {
-            name << fqdn->getDomainName();
-            name << "." << FQDN_PARTIAL_SUFFIX;
-            fqdn_resp->setDomainName(name.str(), Option6ClientFqdn::FULL);
-        }
 
-    // Server may be configured to replace a name supplied by a client,
-    // even if client supplied fully qualified domain-name.
-    } else if (FQDN_REPLACE_CLIENT_NAME) {
-        std::ostringstream name;
-        name << FQDN_GENERATED_PARTIAL_NAME << "." << FQDN_PARTIAL_SUFFIX;
-        fqdn_resp->setDomainName(name.str(), Option6ClientFqdn::FULL);
+    // Set the server S, N, and O flags based on client's flags and
+    // current configuration.
+    D2ClientMgr& d2_mgr = CfgMgr::instance().getD2ClientMgr();
+    d2_mgr.adjustFqdnFlags<Option6ClientFqdn>(*fqdn, *fqdn_resp);
 
-    }
+    // Adjust the domain name based on domain name value and type sent by the
+    // client and current configuration.
+    d2_mgr.adjustDomainName<Option6ClientFqdn>(*fqdn, *fqdn_resp);
 
     // The FQDN has been processed successfully. Let's append it to the
     // response to be sent to a client. Note that the Client FQDN option is
@@ -1114,7 +1039,7 @@ Dhcpv6Srv::processClientFqdn(const Pkt6Ptr& question, const Pkt6Ptr& answer) {
 void
 Dhcpv6Srv::createNameChangeRequests(const Pkt6Ptr& answer) {
     // Don't create NameChangeRequests if DNS updates are disabled.
-    if (!FQDN_ENABLE_UPDATE) {
+    if (!CfgMgr::instance().ddnsEnabled()) {
         return;
     }
 
@@ -1177,18 +1102,19 @@ Dhcpv6Srv::createNameChangeRequests(const Pkt6Ptr& answer) {
         // Get the IP address from the lease. Also, use the S flag to determine
         // if forward change should be performed. This flag will always be
         // set if server has taken responsibility for the forward update.
-        NameChangeRequest ncr(isc::dhcp_ddns::CHG_ADD,
-                              opt_fqdn->getFlag(Option6ClientFqdn::FLAG_S),
-                              true, opt_fqdn->getDomainName(),
-                              iaaddr->getAddress().toText(),
-                              dhcid, 0, iaaddr->getValid());
-        // Add the request to the queue. This queue will be read elsewhere in
-        // the code and all requests from this queue will be sent to the
-        // D2 module.
-        name_change_reqs_.push(ncr);
+        NameChangeRequestPtr ncr;
+        ncr.reset(new NameChangeRequest(isc::dhcp_ddns::CHG_ADD,
+                                        opt_fqdn->getFlag(Option6ClientFqdn::
+                                                          FLAG_S),
+                                        true, opt_fqdn->getDomainName(),
+                                        iaaddr->getAddress().toText(),
+                                        dhcid, 0, iaaddr->getValid()));
 
         LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL,
-                  DHCP6_DDNS_CREATE_ADD_NAME_CHANGE_REQUEST).arg(ncr.toText());
+                  DHCP6_DDNS_CREATE_ADD_NAME_CHANGE_REQUEST).arg(ncr->toText());
+
+        // Post the NCR to the D2ClientMgr.
+        CfgMgr::instance().getD2ClientMgr().sendRequest(ncr);
 
         /// @todo Currently we create NCR with the first IPv6 address that
         /// is carried in one of the IA_NAs. In the future, the NCR API should
@@ -1201,7 +1127,7 @@ Dhcpv6Srv::createNameChangeRequests(const Pkt6Ptr& answer) {
 void
 Dhcpv6Srv::createRemovalNameChangeRequest(const Lease6Ptr& lease) {
     // Don't create NameChangeRequests if DNS updates are disabled.
-    if (!FQDN_ENABLE_UPDATE) {
+    if (!CfgMgr::instance().ddnsEnabled()) {
         return;
     }
 
@@ -1238,31 +1164,21 @@ Dhcpv6Srv::createRemovalNameChangeRequest(const Lease6Ptr& lease) {
 
     }
     isc::dhcp_ddns::D2Dhcid dhcid(*lease->duid_, hostname_wire);
-
     // Create a NameChangeRequest to remove the entry.
-    NameChangeRequest ncr(isc::dhcp_ddns::CHG_REMOVE,
-                          lease->fqdn_fwd_, lease->fqdn_rev_,
-                          lease->hostname_,
-                          lease->addr_.toText(),
-                          dhcid, 0, lease->valid_lft_);
-    name_change_reqs_.push(ncr);
+    NameChangeRequestPtr ncr;
+    ncr.reset(new NameChangeRequest(isc::dhcp_ddns::CHG_REMOVE,
+                                    lease->fqdn_fwd_, lease->fqdn_rev_,
+                                    lease->hostname_,
+                                    lease->addr_.toText(),
+                                    dhcid, 0, lease->valid_lft_));
 
     LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL,
-              DHCP6_DDNS_CREATE_REMOVE_NAME_CHANGE_REQUEST).arg(ncr.toText());
-
-}
+              DHCP6_DDNS_CREATE_REMOVE_NAME_CHANGE_REQUEST).arg(ncr->toText());
 
-void
-Dhcpv6Srv::sendNameChangeRequests() {
-    while (!name_change_reqs_.empty()) {
-        // @todo Once next NameChangeRequest is picked from the queue
-        // we should send it to the b10-dhcp_ddns module. Currently we
-        // just drop it.
-        name_change_reqs_.pop();
-    }
+    // Post the NCR to the D2ClientMgr.
+    CfgMgr::instance().getD2ClientMgr().sendRequest(ncr);
 }
 
-
 OptionPtr
 Dhcpv6Srv::assignIA_NA(const Subnet6Ptr& subnet, const DuidPtr& duid,
                        const Pkt6Ptr& query, const Pkt6Ptr& answer,
@@ -1323,14 +1239,11 @@ Dhcpv6Srv::assignIA_NA(const Subnet6Ptr& subnet, const DuidPtr& duid,
     Option6ClientFqdnPtr fqdn = boost::dynamic_pointer_cast<
         Option6ClientFqdn>(answer->getOption(D6O_CLIENT_FQDN));
     if (fqdn) {
-        // Flag S must not coexist with flag N being set to 1, so if S=1
-        // server takes responsibility for both reverse and forward updates.
-        // Otherwise, we have to check N.
+        /// @todo For now, we assert that if we are doing forward we are also
+        /// doing reverse.
         if (fqdn->getFlag(Option6ClientFqdn::FLAG_S)) {
             do_fwd = true;
             do_rev = true;
-        } else if (!fqdn->getFlag(Option6ClientFqdn::FLAG_N)) {
-            do_rev = true;
         }
     }
     // Set hostname only in case any of the updates is being performed.
@@ -1611,11 +1524,11 @@ Dhcpv6Srv::extendIA_NA(const Subnet6Ptr& subnet, const DuidPtr& duid,
         Option6ClientFqdnPtr fqdn = boost::dynamic_pointer_cast<
             Option6ClientFqdn>(answer->getOption(D6O_CLIENT_FQDN));
         if (fqdn) {
+        // For now, we assert that if we are doing forward we are also
+        // doing reverse.
             if (fqdn->getFlag(Option6ClientFqdn::FLAG_S)) {
                 do_fwd = true;
                 do_rev = true;
-            } else if (!fqdn->getFlag(Option6ClientFqdn::FLAG_N)) {
-                do_rev = true;
             }
         }
 
@@ -2589,36 +2502,30 @@ Dhcpv6Srv::ifaceMgrSocket6ErrorHandler(const std::string& errmsg) {
 }
 
 void Dhcpv6Srv::classifyPacket(const Pkt6Ptr& pkt) {
+    OptionVendorClassPtr vclass = boost::dynamic_pointer_cast<
+        OptionVendorClass>(pkt->getOption(D6O_VENDOR_CLASS));
 
-    boost::shared_ptr<OptionCustom> vclass =
-        boost::dynamic_pointer_cast<OptionCustom>(pkt->getOption(D6O_VENDOR_CLASS));
-
-    if (!vclass) {
+    if (!vclass || vclass->getTuplesNum() == 0) {
         return;
     }
 
-    string classes = "";
+    std::ostringstream classes;
+    if (vclass->hasTuple(DOCSIS3_CLASS_MODEM)) {
+        classes << VENDOR_CLASS_PREFIX << DOCSIS3_CLASS_MODEM;
+
+    } else if (vclass->hasTuple(DOCSIS3_CLASS_EROUTER)) {
+        classes << VENDOR_CLASS_PREFIX << DOCSIS3_CLASS_EROUTER;
+
+    } else {
+        classes << vclass->getTuple(0).getText();
 
-    // DOCSIS specific section
-    if (vclass->readString(VENDOR_CLASS_STRING_INDEX)
-        .find(DOCSIS3_CLASS_MODEM) != std::string::npos) {
-        pkt->addClass(DOCSIS3_CLASS_MODEM);
-        classes += string(DOCSIS3_CLASS_MODEM) + " ";
-    } else
-    if (vclass->readString(VENDOR_CLASS_STRING_INDEX)
-        .find(DOCSIS3_CLASS_EROUTER) != std::string::npos) {
-        pkt->addClass(DOCSIS3_CLASS_EROUTER);
-        classes += string(DOCSIS3_CLASS_EROUTER) + " ";
-    }else
-    {
-        // Otherwise use the string as is
-        classes += vclass->readString(VENDOR_CLASS_STRING_INDEX);
-        pkt->addClass(vclass->readString(VENDOR_CLASS_STRING_INDEX));
     }
 
-    if (!classes.empty()) {
+    // If there is no class identified, leave.
+    if (!classes.str().empty()) {
+        pkt->addClass(classes.str());
         LOG_DEBUG(dhcp6_logger, DBG_DHCP6_BASIC, DHCP6_CLASS_ASSIGNED)
-            .arg(classes);
+            .arg(classes.str());
     }
 }
 
@@ -2652,17 +2559,8 @@ Dhcpv6Srv::generateFqdn(const Pkt6Ptr& answer) {
     }
     // Get the IPv6 address acquired by the client.
     IOAddress addr = iaaddr->getAddress();
-    std::string hostname = addr.toText();
-    // Colons may not be ok for FQDNs so let's replace them with hyphens.
-    std::replace(hostname.begin(), hostname.end(), ':', '-');
-    std::ostringstream stream;
-    // The final FQDN consists of the partial domain name and the suffix.
-    // For example, if the acquired address is 2001:db8:1::2, the generated
-    // FQDN may be:
-    //     host-2001-db8:1--2.example.com.
-    // where prefix 'host' should be configurable. The domain name suffix
-    // should also be configurable.
-    stream << "host-" << hostname << "." << FQDN_PARTIAL_SUFFIX << ".";
+    std::string generated_name =
+        CfgMgr::instance().getD2ClientMgr().generateFqdn(addr);
     try {
         // The lease has been acquired but the FQDN for this lease hasn't
         // been updated in the lease database. We now have new FQDN
@@ -2673,7 +2571,7 @@ Dhcpv6Srv::generateFqdn(const Pkt6Ptr& answer) {
             Lease6Ptr lease =
                 LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA, addr);
             if (lease) {
-                lease->hostname_ = stream.str();
+                lease->hostname_ = generated_name;
                 LeaseMgrFactory::instance().updateLease6(lease);
 
             } else {
@@ -2684,16 +2582,39 @@ Dhcpv6Srv::generateFqdn(const Pkt6Ptr& answer) {
                           " client");
             }
         }
-
         // Set the generated FQDN in the Client FQDN option.
-        fqdn->setDomainName(stream.str(), Option6ClientFqdn::FULL);
+        fqdn->setDomainName(generated_name, Option6ClientFqdn::FULL);
 
     } catch (const Exception& ex) {
         LOG_ERROR(dhcp6_logger, DHCP6_NAME_GEN_UPDATE_FAIL)
-            .arg(hostname)
+            .arg(addr.toText())
             .arg(ex.what());
     }
 }
 
+void
+Dhcpv6Srv::startD2() {
+    D2ClientMgr& d2_mgr = CfgMgr::instance().getD2ClientMgr();
+    if (d2_mgr.ddnsEnabled()) {
+        // Updates are enabled, so lets start the sender, passing in
+        // our error handler.
+        // This may throw so wherever this is called needs to ready.
+        d2_mgr.startSender(boost::bind(&Dhcpv6Srv::d2ClientErrorHandler,
+                                       this, _1, _2));
+    }
+}
+
+void
+Dhcpv6Srv::d2ClientErrorHandler(const
+                                dhcp_ddns::NameChangeSender::Result result,
+                                dhcp_ddns::NameChangeRequestPtr& ncr) {
+    LOG_ERROR(dhcp6_logger, DHCP6_DDNS_REQUEST_SEND_FAILED).
+              arg(result).arg((ncr ? ncr->toText() : " NULL "));
+    // We cannot communicate with b10-dhcp-ddns, suspend futher updates.
+    /// @todo We may wish to revisit this, but for now we will simpy turn
+    /// them off.
+    CfgMgr::instance().getD2ClientMgr().suspendUpdates();
+}
+
 };
 };

+ 37 - 11
src/bin/dhcp6/dhcp6_srv.h

@@ -24,6 +24,7 @@
 #include <dhcp/option_definition.h>
 #include <dhcp/pkt6.h>
 #include <dhcpsrv/alloc_engine.h>
+#include <dhcpsrv/d2_client_mgr.h>
 #include <dhcpsrv/subnet.h>
 #include <hooks/callout_handle.h>
 
@@ -125,6 +126,31 @@ public:
     /// @param port UDP port on which server should listen.
     static void openActiveSockets(const uint16_t port);
 
+    /// @brief Starts DHCP_DDNS client IO if DDNS updates are enabled.
+    ///
+    /// If updates are enabled, it Instructs the D2ClientMgr singleton to
+    /// enter send mode.  If D2ClientMgr encounters errors it may throw
+    /// D2ClientErrors. This method does not catch exceptions.
+    void startD2();
+
+    /// @brief Implements the error handler for DHCP_DDNS IO errors
+    ///
+    /// Invoked when a NameChangeRequest send to b10-dhcp-ddns completes with
+    /// a failed status.  These are communications errors, not data related
+    /// failures.
+    ///
+    /// This method logs the failure and then suspends all further updates.
+    /// Updating can only be restored by reconfiguration or restarting the
+    /// server.  There is currently no retry logic so the first IO error that
+    /// occurs will suspend updates.
+    /// @todo We may wish to make this more robust or sophisticated.
+    ///
+    /// @param result Result code of the send operation.
+    /// @param ncr NameChangeRequest which failed to send.
+    virtual void d2ClientErrorHandler(const dhcp_ddns::
+                                      NameChangeSender::Result result,
+                                      dhcp_ddns::NameChangeRequestPtr& ncr);
+
 protected:
 
     /// @brief Compare received server id with our server id
@@ -447,6 +473,7 @@ protected:
     /// objects and store them in the internal queue. Requests created by this
     /// function are only adding or updating DNS records. In order to generate
     /// requests for DNS records removal, use @c createRemovalNameChangeRequest.
+    /// If ddns updates are disabled, this method returns immediately.
     ///
     /// @todo Add support for multiple IAADDR options in the IA_NA.
     ///
@@ -464,22 +491,12 @@ protected:
     /// Note that this function will not remove the entries which server hadn't
     /// added. This is the case, when client performs forward DNS update on its
     /// own.
+    /// If ddns updates are disabled, this method returns immediately.
     ///
     /// @param lease A lease for which the the removal of corresponding DNS
     /// records will be performed.
     void createRemovalNameChangeRequest(const Lease6Ptr& lease);
 
-    /// @brief Sends all outstanding NameChangeRequests to bind10-d2 module.
-    ///
-    /// The purpose of this function is to pick all outstanding
-    /// NameChangeRequests from the FIFO queue and send them to b10-dhcp-ddns
-    /// module.
-    ///
-    /// @todo Currently this function simply removes all requests from the
-    /// queue but doesn't send them anywhere. In the future, the
-    /// NameChangeSender will be used to deliver requests to the other module.
-    void sendNameChangeRequests();
-
     /// @brief Attempts to extend the lifetime of IAs.
     ///
     /// This function is called when a client sends Renew or Rebind message.
@@ -580,6 +597,15 @@ protected:
     /// @param pkt packet to be classified
     void classifyPacket(const Pkt6Ptr& pkt);
 
+
+    /// @brief this is a prefix added to the contend of vendor-class option
+    ///
+    /// If incoming packet has a vendor class option, its content is
+    /// prepended with this prefix and then interpreted as a class.
+    /// For example, a packet that sends vendor class with value of "FOO"
+    /// will cause the packet to be assigned to class VENDOR_CLASS_FOO.
+    static const std::string VENDOR_CLASS_PREFIX;
+
 private:
 
     /// @brief Implements the error handler for socket open failure.

+ 8 - 4
src/bin/dhcp6/tests/Makefile.am

@@ -54,18 +54,20 @@ if HAVE_GTEST
 # to unexpected errors. For this reason, the --enable-static-link option is
 # ignored for unit tests built here.
 
-nodistdir=$(abs_top_builddir)/src/bin/dhcp6/tests
-nodist_LTLIBRARIES = libco1.la libco2.la
+noinst_LTLIBRARIES = libco1.la libco2.la
+
+# -rpath /nowhere is a hack to trigger libtool to not create a
+# convenience archive, resulting in shared modules
 
 libco1_la_SOURCES  = callout_library_1.cc callout_library_common.h
 libco1_la_CXXFLAGS = $(AM_CXXFLAGS)
 libco1_la_CPPFLAGS = $(AM_CPPFLAGS)
-libco1_la_LDFLAGS = -avoid-version -export-dynamic -module
+libco1_la_LDFLAGS = -avoid-version -export-dynamic -module -rpath /nowhere
 
 libco2_la_SOURCES  = callout_library_2.cc callout_library_common.h
 libco2_la_CXXFLAGS = $(AM_CXXFLAGS)
 libco2_la_CPPFLAGS = $(AM_CPPFLAGS)
-libco2_la_LDFLAGS = -avoid-version -export-dynamic -module
+libco2_la_LDFLAGS = -avoid-version -export-dynamic -module -rpath /nowhere
 
 TESTS += dhcp6_unittests
 dhcp6_unittests_SOURCES  = dhcp6_unittests.cc
@@ -75,6 +77,7 @@ dhcp6_unittests_SOURCES += hooks_unittest.cc
 dhcp6_unittests_SOURCES += dhcp6_test_utils.cc dhcp6_test_utils.h
 dhcp6_unittests_SOURCES += ctrl_dhcp6_srv_unittest.cc
 dhcp6_unittests_SOURCES += config_parser_unittest.cc
+dhcp6_unittests_SOURCES += d2_unittest.cc d2_unittest.h
 dhcp6_unittests_SOURCES += marker_file.cc
 dhcp6_unittests_SOURCES += ../dhcp6_srv.h ../dhcp6_srv.cc
 dhcp6_unittests_SOURCES += ../dhcp6_log.h ../dhcp6_log.cc
@@ -95,6 +98,7 @@ dhcp6_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/cc/libb10-cc.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/config/libb10-cfgclient.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libb10-dhcp++.la
+dhcp6_unittests_LDADD += $(top_builddir)/src/lib/dhcp/tests/libdhcptest.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libb10-dhcp_ddns.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/libb10-dhcpsrv.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/hooks/libb10-hooks.la

+ 109 - 0
src/bin/dhcp6/tests/config_parser_unittest.cc

@@ -347,6 +347,7 @@ public:
             "\"renew-timer\": 1000, "
             "\"valid-lifetime\": 4000, "
             "\"subnet6\": [ ], "
+            "\"dhcp-ddns\": { \"enable-updates\" : false }, "
             "\"option-def\": [ ], "
             "\"option-data\": [ ] }";
         static_cast<void>(executeConfiguration(config,
@@ -3091,5 +3092,113 @@ TEST_F(Dhcp6ParserTest, classifySubnets) {
     EXPECT_TRUE (subnets->at(3)->clientSupported(classes));
 }
 
+// This test checks the ability of the server to parse a configuration
+// containing a full, valid dhcp-ddns (D2ClientConfig) entry.
+TEST_F(Dhcp6ParserTest, d2ClientConfig) {
+    ConstElementPtr status;
+
+    // Verify that the D2 configuraiton can be fetched and is set to disabled.
+    D2ClientConfigPtr d2_client_config = CfgMgr::instance().getD2ClientConfig();
+    EXPECT_FALSE(d2_client_config->getEnableUpdates());
+
+    // Verify that the convenience method agrees.
+    ASSERT_FALSE(CfgMgr::instance().ddnsEnabled());
+
+    string config_str = "{ \"interfaces\": [ \"*\" ],"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet6\": [ { "
+        "    \"pool\": [ \"2001:db8:1::/80\" ],"
+        "    \"subnet\": \"2001:db8:1::/64\" } ], "
+        " \"dhcp-ddns\" : {"
+        "     \"enable-updates\" : true, "
+        "     \"server-ip\" : \"192.168.2.1\", "
+        "     \"server-port\" : 777, "
+        "     \"ncr-protocol\" : \"UDP\", "
+        "     \"ncr-format\" : \"JSON\", "
+        "     \"always-include-fqdn\" : true, "
+        "     \"allow-client-update\" : true, "
+        "     \"override-no-update\" : true, "
+        "     \"override-client-update\" : true, "
+        "     \"replace-client-name\" : true, "
+        "     \"generated-prefix\" : \"test.prefix\", "
+        "     \"qualifying-suffix\" : \"test.suffix.\" },"
+        "\"valid-lifetime\": 4000 }";
+
+    // Convert the JSON string to configuration elements.
+    ElementPtr config;
+    ASSERT_NO_THROW(config = Element::fromJSON(config_str));
+
+    // Pass the configuration in for parsing.
+    EXPECT_NO_THROW(status = configureDhcp6Server(srv_, config));
+
+    // check if returned status is OK
+    checkResult(status, 0);
+
+    // Verify that DHCP-DDNS updating is enabled.
+    EXPECT_TRUE(CfgMgr::instance().ddnsEnabled());
+
+    // Verify that the D2 configuration can be retrieved.
+    d2_client_config = CfgMgr::instance().getD2ClientConfig();
+    ASSERT_TRUE(d2_client_config);
+
+    // Verify that the configuration values are correct.
+    EXPECT_TRUE(d2_client_config->getEnableUpdates());
+    EXPECT_EQ("192.168.2.1", d2_client_config->getServerIp().toText());
+    EXPECT_EQ(777, d2_client_config->getServerPort());
+    EXPECT_EQ(dhcp_ddns::NCR_UDP, d2_client_config->getNcrProtocol());
+    EXPECT_EQ(dhcp_ddns::FMT_JSON, d2_client_config->getNcrFormat());
+    EXPECT_TRUE(d2_client_config->getAlwaysIncludeFqdn());
+    EXPECT_TRUE(d2_client_config->getOverrideNoUpdate());
+    EXPECT_TRUE(d2_client_config->getOverrideClientUpdate());
+    EXPECT_TRUE(d2_client_config->getReplaceClientName());
+    EXPECT_EQ("test.prefix", d2_client_config->getGeneratedPrefix());
+    EXPECT_EQ("test.suffix.", d2_client_config->getQualifyingSuffix());
+}
+
+// This test checks the ability of the server to handle a configuration
+// containing an invalid dhcp-ddns (D2ClientConfig) entry.
+TEST_F(Dhcp6ParserTest, invalidD2ClientConfig) {
+    ConstElementPtr status;
+
+    // Configuration string with an invalid D2 client config,
+    // "server-ip" is missing.
+    string config_str = "{ \"interfaces\": [ \"*\" ],"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet6\": [ { "
+        "    \"pool\": [ \"2001:db8:1::/80\" ],"
+        "    \"subnet\": \"2001:db8:1::/64\" } ], "
+        " \"dhcp-ddns\" : {"
+        "     \"enable-updates\" : true, "
+        "     \"server-port\" : 5301, "
+        "     \"ncr-protocol\" : \"UDP\", "
+        "     \"ncr-format\" : \"JSON\", "
+        "     \"always-include-fqdn\" : true, "
+        "     \"allow-client-update\" : true, "
+        "     \"override-no-update\" : true, "
+        "     \"override-client-update\" : true, "
+        "     \"replace-client-name\" : true, "
+        "     \"generated-prefix\" : \"test.prefix\", "
+        "     \"qualifying-suffix\" : \"test.suffix.\" },"
+        "\"valid-lifetime\": 4000 }";
+
+    // Convert the JSON string to configuration elements.
+    ElementPtr config;
+    ASSERT_NO_THROW(config = Element::fromJSON(config_str));
+
+    // Configuration should not throw, but should fail.
+    EXPECT_NO_THROW(status = configureDhcp6Server(srv_, config));
+
+    // check if returned status is failed.
+    checkResult(status, 1);
+
+    // Verify that the D2 configuraiton can be fetched and is set to disabled.
+    D2ClientConfigPtr d2_client_config = CfgMgr::instance().getD2ClientConfig();
+    EXPECT_FALSE(d2_client_config->getEnableUpdates());
+
+    // Verify that the convenience method agrees.
+    ASSERT_FALSE(CfgMgr::instance().ddnsEnabled());
+}
 
 };

+ 383 - 0
src/bin/dhcp6/tests/d2_unittest.cc

@@ -0,0 +1,383 @@
+// Copyright (C) 2014 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/iface_mgr.h>
+#include <dhcp6/config_parser.h>
+#include <dhcp6/tests/d2_unittest.h>
+#include <dhcpsrv/cfgmgr.h>
+
+#include <gtest/gtest.h>
+
+#include <string>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::data;
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @todo
+void
+D2Dhcpv6Srv::d2ClientErrorHandler(const
+                                dhcp_ddns::NameChangeSender::Result result,
+                                dhcp_ddns::NameChangeRequestPtr& ncr) {
+    ++error_count_;
+    // call base class error handler
+    Dhcpv6Srv::d2ClientErrorHandler(result, ncr);
+}
+
+const bool Dhcp6SrvD2Test::SHOULD_PASS;
+const bool Dhcp6SrvD2Test::SHOULD_FAIL;
+
+Dhcp6SrvD2Test::Dhcp6SrvD2Test() : rcode_(-1) {
+}
+
+Dhcp6SrvD2Test::~Dhcp6SrvD2Test() {
+    reset();
+}
+
+dhcp_ddns::NameChangeRequestPtr
+Dhcp6SrvD2Test::buildTestNcr(uint32_t dhcid_id_num) {
+    // Build an NCR from json string.
+    std::ostringstream stream;
+
+    stream <<
+        "{"
+        " \"change_type\" : 0 , "
+        " \"forward_change\" : true , "
+        " \"reverse_change\" : false , "
+        " \"fqdn\" : \"myhost.example.com.\" , "
+        " \"ip_address\" : \"192.168.2.1\" , "
+        " \"dhcid\" : \""
+
+        << std::hex << std::setfill('0') << std::setw(16)
+        << dhcid_id_num << "\" , "
+
+        " \"lease_expires_on\" : \"20140121132405\" , "
+        " \"lease_length\" : 1300 "
+        "}";
+
+    return (dhcp_ddns::NameChangeRequest::fromJSON(stream.str()));
+}
+
+void
+Dhcp6SrvD2Test::reset() {
+    std::string config = "{ \"interfaces\": [ \"all\" ],"
+            "\"hooks-libraries\": [ ],"
+            "\"preferred-lifetime\": 3000,"
+            "\"rebind-timer\": 2000, "
+            "\"renew-timer\": 1000, "
+            "\"valid-lifetime\": 4000, "
+            "\"subnet6\": [ ], "
+            "\"dhcp-ddns\": { \"enable-updates\" : false }, "
+            "\"option-def\": [ ], "
+            "\"option-data\": [ ] }";
+    configure(config, SHOULD_PASS);
+}
+
+void
+Dhcp6SrvD2Test::configureD2(bool enable_d2, const bool exp_result,
+                            const std::string& ip_address,
+                            const uint32_t port) {
+    std::ostringstream config;
+    config <<
+        "{ \"interfaces\": [ \"*\" ],"
+        "\"hooks-libraries\": [ ],"
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet6\": [ { "
+        "    \"pool\": [ \"2001:db8:1::1 - 2001:db8:1::ffff\" ],"
+        "    \"subnet\": \"2001:db8:1::/64\" } ],"
+        " \"dhcp-ddns\" : {"
+        "     \"enable-updates\" : " << (enable_d2 ? "true" : "false") <<  ", "
+        "     \"server-ip\" : \"" << ip_address << "\", "
+        "     \"server-port\" : " << port << ", "
+        "     \"ncr-protocol\" : \"UDP\", "
+        "     \"ncr-format\" : \"JSON\", "
+        "     \"always-include-fqdn\" : true, "
+        "     \"allow-client-update\" : true, "
+        "     \"override-no-update\" : true, "
+        "     \"override-client-update\" : true, "
+        "     \"replace-client-name\" : true, "
+        "     \"generated-prefix\" : \"test.prefix\", "
+        "     \"qualifying-suffix\" : \"test.suffix.\" },"
+        "\"valid-lifetime\": 4000 }";
+
+    configure(config.str(), exp_result);
+}
+
+void
+Dhcp6SrvD2Test::configure(const std::string& config, bool exp_result) {
+    ElementPtr json = Element::fromJSON(config);
+    ConstElementPtr status;
+
+    // Configure the server and make sure the config is accepted
+    EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+    ASSERT_TRUE(status);
+
+    int rcode;
+    ConstElementPtr comment = config::parseAnswer(rcode, status);
+    if (exp_result == SHOULD_PASS) {
+        ASSERT_EQ(0, rcode);
+    } else {
+        ASSERT_EQ(1, rcode);
+    }
+}
+
+// Tests ability to turn on and off ddns updates by submitting
+// by submitting the appropriate configuration to Dhcp6 server
+// and then invoking its startD2() method.
+TEST_F(Dhcp6SrvD2Test, enableDisable) {
+    // Grab the manager and verify that be default ddns is off
+    // and a sender was not started.
+    dhcp::D2ClientMgr& mgr = CfgMgr::instance().getD2ClientMgr();
+    ASSERT_FALSE(mgr.ddnsEnabled());
+    ASSERT_FALSE(mgr.amSending());
+
+    // Verify a valid config with ddns enabled configures ddns properly,
+    // but does not start the sender.
+    ASSERT_NO_FATAL_FAILURE(configureD2(true));
+    ASSERT_TRUE(mgr.ddnsEnabled());
+    ASSERT_FALSE(mgr.amSending());
+
+    // Verify that calling start does not throw and starts the sender.
+    ASSERT_NO_THROW(srv_.startD2());
+    ASSERT_TRUE(mgr.amSending());
+
+    // Verify a valid config with ddns disabled configures ddns properly.
+    // Sender should not have been started.
+    ASSERT_NO_FATAL_FAILURE(configureD2(false));
+    ASSERT_FALSE(mgr.ddnsEnabled());
+    ASSERT_FALSE(mgr.amSending());
+
+    // Verify that the sender does NOT get started when ddns is disabled.
+    srv_.startD2();
+    ASSERT_FALSE(mgr.amSending());
+}
+
+// Tests Dhcp6 server's ability to correctly handle a flawed dhcp-ddns configuration.
+// It does so by first enabling updates by submitting a valid configuration and then
+// ensuring they remain on after submitting a flawed configuration.
+// and then invoking its startD2() method.
+TEST_F(Dhcp6SrvD2Test, badConfig) {
+    // Grab the manager and verify that be default ddns is off
+    // and a sender was not started.
+    dhcp::D2ClientMgr& mgr = CfgMgr::instance().getD2ClientMgr();
+    ASSERT_FALSE(mgr.ddnsEnabled());
+
+    // Configure it enabled and start it.
+    ASSERT_NO_FATAL_FAILURE(configureD2(true));
+    ASSERT_TRUE(mgr.ddnsEnabled());
+    ASSERT_NO_THROW(srv_.startD2());
+    ASSERT_TRUE(mgr.amSending());
+
+    // Now attempt to give it an invalid configuration.
+    // Result should indicate failure.
+    ASSERT_NO_FATAL_FAILURE(configureD2(false, SHOULD_FAIL, "bogus_ip"));
+
+    // Configure was not altered, so ddns should be enabled and still sending.
+    ASSERT_TRUE(mgr.ddnsEnabled());
+    ASSERT_TRUE(mgr.amSending());
+
+    // Verify that calling start does not throw or stop the sender.
+    ASSERT_NO_THROW(srv_.startD2());
+    ASSERT_TRUE(mgr.amSending());
+
+}
+
+// Checks that submitting an identical dhcp-ddns configuration
+// is handled properly.  Not effect should be no change in
+// status for ddns updating.  Updates should still enabled and
+// in send mode.  This indicates that the sender was not stopped.
+TEST_F(Dhcp6SrvD2Test, sameConfig) {
+    // Grab the manager and verify that be default ddns is off
+    // and a sender was not started.
+    dhcp::D2ClientMgr& mgr = CfgMgr::instance().getD2ClientMgr();
+    ASSERT_FALSE(mgr.ddnsEnabled());
+
+    // Configure it enabled and start it.
+    ASSERT_NO_FATAL_FAILURE(configureD2(true));
+    ASSERT_TRUE(mgr.ddnsEnabled());
+    ASSERT_NO_THROW(srv_.startD2());
+    ASSERT_TRUE(mgr.amSending());
+
+    // Now submit an identical configuration.
+    ASSERT_NO_FATAL_FAILURE(configureD2(true));
+
+    // Configuration was not altered, so ddns should still enabled and sending.
+    ASSERT_TRUE(mgr.ddnsEnabled());
+    ASSERT_TRUE(mgr.amSending());
+
+    // Verify that calling start does not throw or stop the sender.
+    ASSERT_NO_THROW(srv_.startD2());
+    ASSERT_TRUE(mgr.amSending());
+}
+
+// Checks that submitting an different, but valid dhcp-ddns configuration
+// is handled properly.  Updates should be enabled, however they should
+// not yet be running.  This indicates that the sender was stopped and
+// replaced, but not yet started.
+TEST_F(Dhcp6SrvD2Test, differentConfig) {
+    // Grab the manager and verify that be default ddns is off
+    // and a sender was not started.
+    dhcp::D2ClientMgr& mgr = CfgMgr::instance().getD2ClientMgr();
+    ASSERT_FALSE(mgr.ddnsEnabled());
+
+    // Configure it enabled and start it.
+    ASSERT_NO_FATAL_FAILURE(configureD2(true));
+    ASSERT_TRUE(mgr.ddnsEnabled());
+    ASSERT_NO_THROW(srv_.startD2());
+    ASSERT_TRUE(mgr.amSending());
+
+    // Now enable it on a different port.
+    ASSERT_NO_FATAL_FAILURE(configureD2(true, SHOULD_PASS, "127.0.0.1", 54001));
+
+    // Configuration was altered, so ddns should still enabled but not sending.
+    ASSERT_TRUE(mgr.ddnsEnabled());
+    ASSERT_FALSE(mgr.amSending());
+
+    // Verify that calling start starts the sender.
+    ASSERT_NO_THROW(srv_.startD2());
+    ASSERT_TRUE(mgr.amSending());
+}
+
+// Checks that given a valid, enabled configuration and placing
+// sender in send mode, permits NCR requests to be sent via UPD
+// socket.  Note this test does not employ any sort of receiving
+// client to verify actual transmission.  These types of tests
+// are including under dhcp_ddns and d2 unit testing.
+TEST_F(Dhcp6SrvD2Test, simpleUDPSend) {
+    // Grab the manager and verify that be default ddns is off
+    // and a sender was not started.
+    dhcp::D2ClientMgr& mgr = CfgMgr::instance().getD2ClientMgr();
+    ASSERT_FALSE(mgr.ddnsEnabled());
+
+    // Configure it enabled and start it.
+    ASSERT_NO_FATAL_FAILURE(configureD2(true));
+    ASSERT_TRUE(mgr.ddnsEnabled());
+    ASSERT_NO_THROW(srv_.startD2());
+    ASSERT_TRUE(mgr.amSending());
+
+    // Verify that we can queue up a message.
+    dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr();
+    ASSERT_NO_THROW(mgr.sendRequest(ncr));
+    EXPECT_EQ(1, mgr.getQueueSize());
+
+    // Calling receive should detect the ready IO on the sender's select-fd,
+    // and invoke callback, which should complete the send.
+    ASSERT_NO_THROW(IfaceMgr::instance().receive4(0,0));
+
+    // Verify the queue is now empty.
+    EXPECT_EQ(0, mgr.getQueueSize());
+}
+
+// Checks that an IO error in sending a request to D2, results in ddns updates
+// being suspended.  This indicates that Dhcp6Srv's error handler has been
+// invoked as expected.  Note that this unit test relies on an attempt to send
+// to a server address of 0.0.0.0 port 0 fails under all OSs.
+TEST_F(Dhcp6SrvD2Test, forceUDPSendFailure) {
+    // Grab the manager and verify that be default ddns is off
+    // and a sender was not started.
+    dhcp::D2ClientMgr& mgr = CfgMgr::instance().getD2ClientMgr();
+    ASSERT_FALSE(mgr.ddnsEnabled());
+
+    // Configure it enabled and start it.
+    // Using server address of 0.0.0.0/0 should induce failure on send.
+    ASSERT_NO_FATAL_FAILURE(configureD2(true, SHOULD_PASS, "0.0.0.0", 0));
+    ASSERT_TRUE(mgr.ddnsEnabled());
+    ASSERT_NO_THROW(srv_.startD2());
+    ASSERT_TRUE(mgr.amSending());
+
+    // Queue up 3 messages.
+    for (int i = 0; i < 3; i++) {
+        dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr(i + 1);
+        ASSERT_NO_THROW(mgr.sendRequest(ncr));
+    }
+    EXPECT_EQ(3, mgr.getQueueSize());
+
+    // Calling receive should detect the ready IO on the sender's select-fd,
+    // and invoke callback, which should complete the send, which should
+    // fail.
+    ASSERT_NO_THROW(IfaceMgr::instance().receive4(0,0));
+
+    // Verify the error handler was invoked.
+    EXPECT_EQ(1, srv_.error_count_);
+
+    // Verify that updates are disabled and we are no longer sending.
+    ASSERT_FALSE(mgr.ddnsEnabled());
+    ASSERT_FALSE(mgr.amSending());
+
+    // Verify message is still in the queue.
+    EXPECT_EQ(3, mgr.getQueueSize());
+
+    // Verify that we can't just restart it.
+    /// @todo This may change if we add ability to resume.
+    ASSERT_NO_THROW(srv_.startD2());
+    ASSERT_FALSE(mgr.amSending());
+
+    // Configure it enabled and start it.
+    ASSERT_NO_FATAL_FAILURE(configureD2(true));
+    ASSERT_TRUE(mgr.ddnsEnabled());
+    ASSERT_NO_THROW(srv_.startD2());
+    ASSERT_TRUE(mgr.amSending());
+
+    // Verify message is still in the queue.
+    EXPECT_EQ(3, mgr.getQueueSize());
+
+    // This will finish sending the 1st message in queue
+    // and initiate send of 2nd message.
+    ASSERT_NO_THROW(IfaceMgr::instance().receive4(0,0));
+    EXPECT_EQ(1, srv_.error_count_);
+
+    // First message is off the queue.
+    EXPECT_EQ(2, mgr.getQueueSize());
+}
+
+// Tests error handling of D2ClientMgr::sendRequest() failure
+// by attempting to queue maximum number of messages.
+TEST_F(Dhcp6SrvD2Test, queueMaxError) {
+    // Configure it enabled and start it.
+    dhcp::D2ClientMgr& mgr = CfgMgr::instance().getD2ClientMgr();
+    ASSERT_NO_FATAL_FAILURE(configureD2(true));
+    ASSERT_TRUE(mgr.ddnsEnabled());
+    ASSERT_NO_THROW(srv_.startD2());
+    ASSERT_TRUE(mgr.amSending());
+
+    // Attempt to queue more then the maximum allowed.
+    int max_msgs = mgr.getQueueMaxSize();
+    for (int i = 0; i < max_msgs + 1; i++) {
+        dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr(i + 1);
+        ASSERT_NO_THROW(mgr.sendRequest(ncr));
+    }
+
+    // Stopping sender will complete the first message so there
+    // should be max less one.
+    EXPECT_EQ(max_msgs - 1, mgr.getQueueSize());
+
+    // Verify the error handler was invoked.
+    EXPECT_EQ(1, srv_.error_count_);
+
+    // Verify that updates are disabled and we are no longer sending.
+    ASSERT_FALSE(mgr.ddnsEnabled());
+    ASSERT_FALSE(mgr.amSending());
+}
+
+
+} // namespace test
+} // namespace dhcp
+} // namespace isc
+

+ 117 - 0
src/bin/dhcp6/tests/d2_unittest.h

@@ -0,0 +1,117 @@
+// Copyright (C) 2014 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 d2_unittest.h Defines classes for testing Dhcpv6srv with D2ClientMgr
+
+#ifndef D2_UNITTEST_H
+#define D2_UNITTEST_H
+
+#include <dhcp6/dhcp6_srv.h>
+#include <config/ccsession.h>
+
+#include <gtest/gtest.h>
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @brief Test derivation of Dhcpv6Srv class used in D2 testing.
+/// Use of this class allows the intervention at strategic points in testing
+/// by permitting overridden methods and access to scope protected members.
+class D2Dhcpv6Srv : public  Dhcpv6Srv {
+public:
+    /// @brief Counts the number of times the client error handler is called.
+    int error_count_;
+
+    /// @brief Constructor
+    D2Dhcpv6Srv()
+        : Dhcpv6Srv(0), error_count_(0) {
+    }
+
+    /// @brief virtual Destructor.
+    virtual ~D2Dhcpv6Srv() {
+    }
+
+    /// @brief Override the error handler.
+    virtual void d2ClientErrorHandler(const dhcp_ddns::NameChangeSender::
+                                      Result result,
+                                      dhcp_ddns::NameChangeRequestPtr& ncr);
+};
+
+/// @brief Test fixture which permits testing the interaction between the
+/// D2ClientMgr and Dhcpv6Srv.
+class Dhcp6SrvD2Test : public ::testing::Test {
+public:
+    /// @brief Mnemonic constants for calls to configuration methods.
+    static const bool SHOULD_PASS = true;
+    static const bool SHOULD_FAIL = false;
+
+    /// @brief Constructor
+    Dhcp6SrvD2Test();
+
+    /// @brief virtual Destructor
+    virtual ~Dhcp6SrvD2Test();
+
+    /// @brief Resets the CfgMgr singleton to defaults.
+    /// Primarily used in the test destructor as gtest doesn't exit between
+    /// tests.
+    /// @todo CfgMgr should provide a method to reset everything or maybe
+    /// reconstruct the singleton.
+    void reset();
+
+    /// @brief Configures the server with D2 enabled or disabled
+    ///
+    /// Constructs a configuration string including dhcp-ddns with the
+    /// parameters given and passes it into the server's configuration handler.
+    ///
+    /// @param enable_updates value to assign to the enable-updates parameter
+    /// @param exp_result indicates if configuration should pass or fail
+    /// @param ip_address IP address for the D2 server
+    /// @param port  port for the D2 server
+    void configureD2(bool enable_updates, bool exp_result = SHOULD_PASS,
+                     const std::string& ip_address = "127.0.0.1",
+                     const uint32_t port = 53001);
+
+    /// @brief Configures the server with the given configuration
+    ///
+    /// Passes the given configuration string into the server's configuration
+    /// handler.  It accepts a flag indicating whether or not the configuration
+    /// is expected to succeed or fail.  This permits testing the server's
+    /// response to both valid and invalid configurations.
+    ///
+    /// @param config JSON string containing the configuration
+    /// @param exp_result indicates if configuration should pass or fail
+    void configure(const std::string& config, bool exp_result = SHOULD_PASS);
+
+    /// @brief Contructs a NameChangeRequest message from a fixed JSON string.
+    ///
+    /// @param dhcid_id_num Integer value to use as the DHCID.
+    dhcp_ddns::NameChangeRequestPtr buildTestNcr(uint32_t
+                                                 dhcid_id_num = 0xdeadbeef);
+
+    /// @brief Stores the return code of the last configuration attempt.
+    int rcode_;
+
+    /// @brief Stores the message component of the last configuration tattempt.
+    isc::data::ConstElementPtr comment_;
+
+    /// @brief Server object under test.
+    D2Dhcpv6Srv srv_;
+};
+
+}; // end of isc::dhcp::test namespace
+}; // end of isc::dhcp namespace
+}; // end of isc namespace
+
+#endif // D2_UNITTEST_H

+ 213 - 52
src/bin/dhcp6/tests/dhcp6_srv_unittest.cc

@@ -29,6 +29,7 @@
 #include <dhcp6/config_parser.h>
 #include <dhcp/dhcp6.h>
 #include <dhcp/docsis3_option_defs.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
 #include <dhcpsrv/cfgmgr.h>
 #include <dhcpsrv/lease_mgr.h>
 #include <dhcpsrv/lease_mgr_factory.h>
@@ -52,6 +53,7 @@ using namespace isc::config;
 using namespace isc::test;
 using namespace isc::asiolink;
 using namespace isc::dhcp;
+using namespace isc::dhcp::test;
 using namespace isc::util;
 using namespace isc::hooks;
 using namespace std;
@@ -283,6 +285,9 @@ TEST_F(Dhcpv6SrvTest, DUID) {
 // This test checks if Option Request Option (ORO) is parsed correctly
 // and the requested options are actually assigned.
 TEST_F(Dhcpv6SrvTest, advertiseOptions) {
+
+    IfaceMgrTestConfig test_config(true);
+
     ConstElementPtr x;
     string config = "{ \"interfaces\": [ \"all\" ],"
         "\"preferred-lifetime\": 3000,"
@@ -291,6 +296,7 @@ TEST_F(Dhcpv6SrvTest, advertiseOptions) {
         "\"subnet6\": [ { "
         "    \"pool\": [ \"2001:db8:1::/64\" ],"
         "    \"subnet\": \"2001:db8:1::/48\", "
+        "    \"interface\": \"eth0\", "
         "    \"option-data\": [ {"
         "          \"name\": \"dns-servers\","
         "          \"space\": \"dhcp6\","
@@ -307,25 +313,17 @@ TEST_F(Dhcpv6SrvTest, advertiseOptions) {
         "        } ]"
         " } ],"
         "\"valid-lifetime\": 4000 }";
-
-    ElementPtr json = Element::fromJSON(config);
-
-    NakedDhcpv6Srv srv(0);
-
-    EXPECT_NO_THROW(x = configureDhcp6Server(srv, json));
-    ASSERT_TRUE(x);
-    comment_ = parseAnswer(rcode_, x);
-
-    ASSERT_EQ(0, rcode_);
+    ASSERT_NO_THROW(configure(config));
 
     Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
     sol->setRemoteAddr(IOAddress("fe80::abcd"));
+    sol->setIface("eth0");
     sol->addOption(generateIA(D6O_IA_NA, 234, 1500, 3000));
     OptionPtr clientid = generateClientId();
     sol->addOption(clientid);
 
     // Pass it to the server and get an advertise
-    Pkt6Ptr adv = srv.processSolicit(sol);
+    Pkt6Ptr adv = srv_.processSolicit(sol);
 
     // check if we get response at all
     ASSERT_TRUE(adv);
@@ -349,7 +347,7 @@ TEST_F(Dhcpv6SrvTest, advertiseOptions) {
     sol->addOption(option_oro);
 
     // Need to process SOLICIT again after requesting new option.
-    adv = srv.processSolicit(sol);
+    adv = srv_.processSolicit(sol);
     ASSERT_TRUE(adv);
 
     OptionPtr tmp = adv->getOption(D6O_NAME_SERVERS);
@@ -404,6 +402,7 @@ TEST_F(Dhcpv6SrvTest, SolicitBasic) {
 
     Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
     sol->setRemoteAddr(IOAddress("fe80::abcd"));
+    sol->setIface("eth0");
     sol->addOption(generateIA(D6O_IA_NA, 234, 1500, 3000));
     OptionPtr clientid = generateClientId();
     sol->addOption(clientid);
@@ -447,6 +446,7 @@ TEST_F(Dhcpv6SrvTest, pdSolicitBasic) {
 
     Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
     sol->setRemoteAddr(IOAddress("fe80::abcd"));
+    sol->setIface("eth0");
     sol->addOption(generateIA(D6O_IA_PD, 234, 1500, 3000));
     OptionPtr clientid = generateClientId();
     sol->addOption(clientid);
@@ -492,6 +492,7 @@ TEST_F(Dhcpv6SrvTest, SolicitHint) {
     // Let's create a SOLICIT
     Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
     sol->setRemoteAddr(IOAddress("fe80::abcd"));
+    sol->setIface("eth0");
     boost::shared_ptr<Option6IA> ia = generateIA(D6O_IA_NA, 234, 1500, 3000);
 
     // with a valid hint
@@ -546,6 +547,7 @@ TEST_F(Dhcpv6SrvTest, SolicitInvalidHint) {
     // Let's create a SOLICIT
     Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
     sol->setRemoteAddr(IOAddress("fe80::abcd"));
+    sol->setIface("eth0");
     boost::shared_ptr<Option6IA> ia = generateIA(D6O_IA_NA, 234, 1500, 3000);
     IOAddress hint("2001:db8:1::cafe:babe");
     ASSERT_FALSE(subnet_->inPool(Lease::TYPE_NA, hint));
@@ -596,6 +598,10 @@ TEST_F(Dhcpv6SrvTest, ManySolicits) {
     sol2->setRemoteAddr(IOAddress("fe80::1223"));
     sol3->setRemoteAddr(IOAddress("fe80::3467"));
 
+    sol1->setIface("eth0");
+    sol2->setIface("eth0");
+    sol3->setIface("eth0");
+
     sol1->addOption(generateIA(D6O_IA_NA, 1, 1500, 3000));
     sol2->addOption(generateIA(D6O_IA_NA, 2, 1500, 3000));
     sol3->addOption(generateIA(D6O_IA_NA, 3, 1500, 3000));
@@ -673,6 +679,7 @@ TEST_F(Dhcpv6SrvTest, RequestBasic) {
     // Let's create a REQUEST
     Pkt6Ptr req = Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234));
     req->setRemoteAddr(IOAddress("fe80::abcd"));
+    req->setIface("eth0");
     boost::shared_ptr<Option6IA> ia = generateIA(D6O_IA_NA, 234, 1500, 3000);
 
     // with a valid hint
@@ -737,6 +744,7 @@ TEST_F(Dhcpv6SrvTest, pdRequestBasic) {
     // Let's create a REQUEST
     Pkt6Ptr req = Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234));
     req->setRemoteAddr(IOAddress("fe80::abcd"));
+    req->setIface("eth0");
     boost::shared_ptr<Option6IA> ia = generateIA(D6O_IA_PD, 234, 1500, 3000);
 
     // with a valid hint
@@ -800,6 +808,10 @@ TEST_F(Dhcpv6SrvTest, ManyRequests) {
     req2->setRemoteAddr(IOAddress("fe80::1223"));
     req3->setRemoteAddr(IOAddress("fe80::3467"));
 
+    req1->setIface("eth0");
+    req2->setIface("eth0");
+    req3->setIface("eth0");
+
     req1->addOption(generateIA(D6O_IA_NA, 1, 1500, 3000));
     req2->addOption(generateIA(D6O_IA_NA, 2, 1500, 3000));
     req3->addOption(generateIA(D6O_IA_NA, 3, 1500, 3000));
@@ -1078,9 +1090,9 @@ TEST_F(Dhcpv6SrvTest, sanityCheck) {
 // Check that the server is testing if server identifier received in the
 // query, matches server identifier used by the server.
 TEST_F(Dhcpv6SrvTest, testServerID) {
-	NakedDhcpv6Srv srv(0);
+        NakedDhcpv6Srv srv(0);
 
-	Pkt6Ptr req = Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234));
+        Pkt6Ptr req = Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234));
     std::vector<uint8_t> bin;
 
     // diud_llt constructed with: time = 0, macaddress = 00:00:00:00:00:00
@@ -1168,15 +1180,16 @@ TEST_F(Dhcpv6SrvTest, selectSubnetAddr) {
     Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:3::"), 48, 1, 2, 3, 4));
 
     // CASE 1: We have only one subnet defined and we received local traffic.
-    // The only available subnet should be selected
+    // The only available subnet used to be picked, but not anymore
     CfgMgr::instance().deleteSubnets6();
     CfgMgr::instance().addSubnet6(subnet1); // just a single subnet
 
     Pkt6Ptr pkt = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
     pkt->setRemoteAddr(IOAddress("fe80::abcd"));
 
-    Subnet6Ptr selected = srv.selectSubnet(pkt);
-    EXPECT_EQ(selected, subnet1);
+    // The clause for assuming local subnet if there is only one subnet is was
+    // removed.
+    EXPECT_FALSE(srv.selectSubnet(pkt));
 
     // CASE 2: We have only one subnet defined and we received relayed traffic.
     // We should NOT select it.
@@ -1185,7 +1198,7 @@ TEST_F(Dhcpv6SrvTest, selectSubnetAddr) {
     CfgMgr::instance().deleteSubnets6();
     CfgMgr::instance().addSubnet6(subnet1); // just a single subnet
     pkt->setRemoteAddr(IOAddress("2001:db8:abcd::2345"));
-    selected = srv.selectSubnet(pkt);
+    Subnet6Ptr selected = srv.selectSubnet(pkt);
     EXPECT_FALSE(selected);
 
     // CASE 3: We have three subnets defined and we received local traffic.
@@ -1215,9 +1228,7 @@ TEST_F(Dhcpv6SrvTest, selectSubnetAddr) {
     CfgMgr::instance().addSubnet6(subnet2);
     CfgMgr::instance().addSubnet6(subnet3);
     pkt->setRemoteAddr(IOAddress("2001:db8:4::baca"));
-    selected = srv.selectSubnet(pkt);
-    EXPECT_FALSE(selected);
-
+    EXPECT_FALSE(srv.selectSubnet(pkt));
 }
 
 // This test verifies if selectSubnet() selects proper subnet for a given
@@ -1543,7 +1554,9 @@ TEST_F(Dhcpv6SrvTest, docsisVendorORO) {
 // This test checks if Option Request Option (ORO) in docsis (vendor-id=4491)
 // vendor options is parsed correctly and the requested options are actually assigned.
 TEST_F(Dhcpv6SrvTest, vendorOptionsORO) {
-    ConstElementPtr x;
+
+    IfaceMgrTestConfig test_config(true);
+
     string config = "{ \"interfaces\": [ \"all\" ],"
         "\"preferred-lifetime\": 3000,"
         "\"rebind-timer\": 2000, "
@@ -1572,28 +1585,21 @@ TEST_F(Dhcpv6SrvTest, vendorOptionsORO) {
         "    \"preferred-lifetime\": 3000,"
         "    \"valid-lifetime\": 4000,"
         "    \"interface-id\": \"\","
-        "    \"interface\": \"\""
+        "    \"interface\": \"eth0\""
         " } ],"
         "\"valid-lifetime\": 4000 }";
 
-    ElementPtr json = Element::fromJSON(config);
-
-    NakedDhcpv6Srv srv(0);
-
-    EXPECT_NO_THROW(x = configureDhcp6Server(srv, json));
-    ASSERT_TRUE(x);
-    comment_ = parseAnswer(rcode_, x);
-
-    ASSERT_EQ(0, rcode_);
+    ASSERT_NO_THROW(configure(config));
 
     Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
     sol->setRemoteAddr(IOAddress("fe80::abcd"));
+    sol->setIface("eth0");
     sol->addOption(generateIA(D6O_IA_NA, 234, 1500, 3000));
     OptionPtr clientid = generateClientId();
     sol->addOption(clientid);
 
     // Pass it to the server and get an advertise
-    Pkt6Ptr adv = srv.processSolicit(sol);
+    Pkt6Ptr adv = srv_.processSolicit(sol);
 
     // check if we get response at all
     ASSERT_TRUE(adv);
@@ -1612,7 +1618,7 @@ TEST_F(Dhcpv6SrvTest, vendorOptionsORO) {
     sol->addOption(vendor);
 
     // Need to process SOLICIT again after requesting new option.
-    adv = srv.processSolicit(sol);
+    adv = srv_.processSolicit(sol);
     ASSERT_TRUE(adv);
 
     // Check if thre is vendor option response
@@ -1771,7 +1777,7 @@ TEST_F(Dhcpv6SrvTest, clientClassification) {
     srv.classifyPacket(sol1);
 
     // It should belong to docsis3.0 class. It should not belong to eRouter1.0
-    EXPECT_TRUE(sol1->inClass("docsis3.0"));
+    EXPECT_TRUE(sol1->inClass("VENDOR_CLASS_docsis3.0"));
     EXPECT_FALSE(sol1->inClass("eRouter1.0"));
 
     // Let's get a relayed SOLICIT. This particular relayed SOLICIT has
@@ -1782,8 +1788,8 @@ TEST_F(Dhcpv6SrvTest, clientClassification) {
 
     srv.classifyPacket(sol2);
 
-    EXPECT_TRUE(sol2->inClass("eRouter1.0"));
-    EXPECT_FALSE(sol2->inClass("docsis3.0"));
+    EXPECT_TRUE(sol2->inClass(srv.VENDOR_CLASS_PREFIX + "eRouter1.0"));
+    EXPECT_FALSE(sol2->inClass(srv.VENDOR_CLASS_PREFIX + "docsis3.0"));
 }
 
 // Checks if the client-class field is indeed used for subnet selection.
@@ -1791,10 +1797,6 @@ TEST_F(Dhcpv6SrvTest, clientClassification) {
 // .clientClassification above.
 TEST_F(Dhcpv6SrvTest, clientClassify2) {
 
-    NakedDhcpv6Srv srv(0);
-
-    ConstElementPtr status;
-
     // This test configures 2 subnets. We actually only need the
     // first one, but since there's still this ugly hack that picks
     // the pool if there is only one, we must use more than one
@@ -1820,14 +1822,7 @@ TEST_F(Dhcpv6SrvTest, clientClassify2) {
         "],"
         "\"valid-lifetime\": 4000 }";
 
-    ElementPtr json = Element::fromJSON(config);
-
-    EXPECT_NO_THROW(status = configureDhcp6Server(srv, json));
-
-    // check if returned status is OK
-    ASSERT_TRUE(status);
-    comment_ = config::parseAnswer(rcode_, status);
-    ASSERT_EQ(0, rcode_);
+    ASSERT_NO_THROW(configure(config));
 
     Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
     sol->setRemoteAddr(IOAddress("2001:db8:1::3"));
@@ -1837,21 +1832,187 @@ TEST_F(Dhcpv6SrvTest, clientClassify2) {
 
     // This discover does not belong to foo class, so it will not
     // be serviced
-    EXPECT_FALSE(srv.selectSubnet(sol));
+    EXPECT_FALSE(srv_.selectSubnet(sol));
 
     // Let's add the packet to bar class and try again.
     sol->addClass("bar");
 
     // Still not supported, because it belongs to wrong class.
-    EXPECT_FALSE(srv.selectSubnet(sol));
+    EXPECT_FALSE(srv_.selectSubnet(sol));
 
     // Let's add it to maching class.
     sol->addClass("foo");
 
     // This time it should work
-    EXPECT_TRUE(srv.selectSubnet(sol));
+    EXPECT_TRUE(srv_.selectSubnet(sol));
+}
+
+// This test checks that the server will handle a Solicit with the Vendor Class
+// having a length of 4 (enterprise-id only).
+TEST_F(Dhcpv6SrvTest, cableLabsShortVendorClass) {
+    NakedDhcpv6Srv srv(0);
+
+    // Create a simple Solicit with the 4-byte long vendor class option.
+    Pkt6Ptr sol = captureCableLabsShortVendorClass();
+
+    // Simulate that we have received that traffic
+    srv.fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive6(), it will read all packets from the list set by
+    // fakeReceive()
+    srv.run();
+
+    // Get Advertise...
+    ASSERT_FALSE(srv.fake_sent_.empty());
+    Pkt6Ptr adv = srv.fake_sent_.front();
+    ASSERT_TRUE(adv);
+
+    // This is sent back to client, so port is 546
+    EXPECT_EQ(DHCP6_CLIENT_PORT, adv->getRemotePort());
+}
+
+// Checks if relay IP address specified in the relay-info structure in
+// subnet6 is being used properly.
+TEST_F(Dhcpv6SrvTest, relayOverride) {
+
+    // We have 2 subnets defined. Note that both have a relay address
+    // defined. Both are not belonging to the subnets. That is
+    // important, because if the relay belongs to the subnet, there's
+    // no need to specify relay override.
+    string config = "{ \"interfaces\": [ \"*\" ],"
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet6\": [ "
+        " {  \"pool\": [ \"2001:db8:1::/64\" ],"
+        "    \"subnet\": \"2001:db8:1::/48\", "
+        "    \"relay\": { "
+        "        \"ip-address\": \"2001:db8:3::1\""
+        "    }"
+        " }, "
+        " {  \"pool\": [ \"2001:db8:2::/64\" ],"
+        "    \"subnet\": \"2001:db8:2::/48\", "
+        "    \"relay\": { "
+        "        \"ip-address\": \"2001:db8:3::2\""
+        "    }"
+        " } "
+        "],"
+        "\"valid-lifetime\": 4000 }";
+
+    // Use this config to set up the server
+    ASSERT_NO_THROW(configure(config));
+
+    // Let's get the subnet configuration objects
+    const Subnet6Collection* subnets = CfgMgr::instance().getSubnets6();
+    ASSERT_EQ(2, subnets->size());
+
+    // Let's get them for easy reference
+    Subnet6Ptr subnet1 = (*subnets)[0];
+    Subnet6Ptr subnet2 = (*subnets)[1];
+    ASSERT_TRUE(subnet1);
+    ASSERT_TRUE(subnet2);
+
+    Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
+    sol->setRemoteAddr(IOAddress("2001:db8:1::3"));
+    sol->addOption(generateIA(D6O_IA_NA, 234, 1500, 3000));
+    OptionPtr clientid = generateClientId();
+    sol->addOption(clientid);
+
+    // Now pretend the packet came via one relay.
+    Pkt6::RelayInfo relay;
+    relay.linkaddr_ = IOAddress("2001:db8:1::1");
+    relay.peeraddr_ = IOAddress("fe80::1");
+
+    sol->relay_info_.push_back(relay);
+
+    // This is just a sanity check, we're using regular method: the relay
+    // belongs to the first (2001:db8:1::/64) subnet, so it's an easy decision.
+    EXPECT_TRUE(subnet1 == srv_.selectSubnet(sol));
+
+    // Relay belongs to the second subnet, so it should be selected.
+    sol->relay_info_.back().linkaddr_ = IOAddress("2001:db8:2::1");
+    EXPECT_TRUE(subnet2 == srv_.selectSubnet(sol));
+
+    // Now let's check if the relay override for the first subnets works
+    sol->relay_info_.back().linkaddr_ = IOAddress("2001:db8:3::1");
+    EXPECT_TRUE(subnet1 == srv_.selectSubnet(sol));
+
+    // Now repeat that for relay matching the second subnet.
+    sol->relay_info_.back().linkaddr_ = IOAddress("2001:db8:3::2");
+    EXPECT_TRUE(subnet2 == srv_.selectSubnet(sol));
+
+    // Finally, let's check that completely mismatched relay will not get us
+    // anything
+    sol->relay_info_.back().linkaddr_ = IOAddress("2001:db8:1234::1");
+    EXPECT_FALSE(srv_.selectSubnet(sol));
 }
 
+// Checks if relay IP address specified in the relay-info structure can be
+// used together with client-classification.
+TEST_F(Dhcpv6SrvTest, relayOverrideAndClientClass) {
+
+    // This test configures 2 subnets. They both are on the same link, so they
+    // have the same relay-ip address. Furthermore, the first subnet is
+    // reserved for clients that belong to class "foo".
+    string config = "{ \"interfaces\": [ \"*\" ],"
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet6\": [ "
+        " {  \"pool\": [ \"2001:db8:1::/64\" ],"
+        "    \"subnet\": \"2001:db8:1::/48\", "
+        "    \"client-class\": \"foo\", "
+        "    \"relay\": { "
+        "        \"ip-address\": \"2001:db8:3::1\""
+        "    }"
+        " }, "
+        " {  \"pool\": [ \"2001:db8:2::/64\" ],"
+        "    \"subnet\": \"2001:db8:2::/48\", "
+        "    \"relay\": { "
+        "        \"ip-address\": \"2001:db8:3::1\""
+        "    }"
+        " } "
+        "],"
+        "\"valid-lifetime\": 4000 }";
+
+    // Use this config to set up the server
+    ASSERT_NO_THROW(configure(config));
+
+    // Let's get the subnet configuration objects
+    const Subnet6Collection* subnets = CfgMgr::instance().getSubnets6();
+    ASSERT_EQ(2, subnets->size());
+
+    // Let's get them for easy reference
+    Subnet6Ptr subnet1 = (*subnets)[0];
+    Subnet6Ptr subnet2 = (*subnets)[1];
+    ASSERT_TRUE(subnet1);
+    ASSERT_TRUE(subnet2);
+
+    Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
+    sol->setRemoteAddr(IOAddress("2001:db8:1::3"));
+    sol->addOption(generateIA(D6O_IA_NA, 234, 1500, 3000));
+    OptionPtr clientid = generateClientId();
+    sol->addOption(clientid);
+
+    // Now pretend the packet came via one relay.
+    Pkt6::RelayInfo relay;
+    relay.linkaddr_ = IOAddress("2001:db8:3::1");
+    relay.peeraddr_ = IOAddress("fe80::1");
+
+    sol->relay_info_.push_back(relay);
+
+    // This packet does not belong to class foo, so it should be rejected in
+    // subnet[0], even though the relay-ip matches. It should be accepted in
+    // subnet[1], because the subnet matches and there are no class
+    // requirements.
+    EXPECT_TRUE(subnet2 == srv_.selectSubnet(sol));
+
+    // Now let's add this packet to class foo and recheck. This time it should
+    // be accepted in the first subnet, because both class and relay-ip match.
+    sol->addClass("foo");
+    EXPECT_TRUE(subnet1 == srv_.selectSubnet(sol));
+}
 
 /// @todo: Add more negative tests for processX(), e.g. extend sanityCheck() test
 /// to call processX() methods.

+ 40 - 0
src/bin/dhcp6/tests/dhcp6_test_utils.cc

@@ -14,13 +14,39 @@
 
 #include <gtest/gtest.h>
 #include <dhcp6/tests/dhcp6_test_utils.h>
+#include <dhcp6/config_parser.h>
 
+using namespace isc::data;
 using namespace isc::dhcp;
 using namespace isc::asiolink;
 
 namespace isc {
 namespace test {
 
+Dhcpv6SrvTest::Dhcpv6SrvTest()
+:srv_(0) {
+    subnet_ = isc::dhcp::Subnet6Ptr
+        (new isc::dhcp::Subnet6(isc::asiolink::IOAddress("2001:db8:1::"),
+                                48, 1000, 2000, 3000, 4000));
+    subnet_->setIface("eth0");
+
+    pool_ = isc::dhcp::Pool6Ptr
+        (new isc::dhcp::Pool6(isc::dhcp::Lease::TYPE_NA,
+                              isc::asiolink::IOAddress("2001:db8:1:1::"),
+                              64));
+    subnet_->addPool(pool_);
+
+    isc::dhcp::CfgMgr::instance().deleteSubnets6();
+    isc::dhcp::CfgMgr::instance().addSubnet6(subnet_);
+
+    // configure PD pool
+    pd_pool_ = isc::dhcp::Pool6Ptr
+        (new isc::dhcp::Pool6(isc::dhcp::Lease::TYPE_PD,
+                              isc::asiolink::IOAddress("2001:db8:1:2::"),
+                              64, 80));
+    subnet_->addPool(pd_pool_);
+}
+
 // Checks that server response (ADVERTISE or REPLY) contains proper IA_NA option
 // It returns IAADDR option for each chaining with checkIAAddr method.
 boost::shared_ptr<Option6IAAddr>
@@ -136,6 +162,7 @@ Dhcpv6SrvTest::createMessage(uint8_t message_type, Lease::Type lease_type,
                              const uint32_t iaid) {
     Pkt6Ptr msg = Pkt6Ptr(new Pkt6(message_type, 1234));
     msg->setRemoteAddr(IOAddress("fe80::abcd"));
+    msg->setIface("eth0");
     msg->addOption(createIA(lease_type, addr, prefix_len, iaid));
     return (msg);
 }
@@ -561,6 +588,19 @@ Dhcpv6SrvTest::testReleaseReject(Lease::Type type, const IOAddress& addr) {
     EXPECT_TRUE(LeaseMgrFactory::instance().deleteLease(addr));
 }
 
+void
+Dhcpv6SrvTest::configure(const std::string& config) {
+    ElementPtr json = data::Element::fromJSON(config);
+    ConstElementPtr status;
+
+    // Configure the server and make sure the config is accepted
+    EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+    ASSERT_TRUE(status);
+    int rcode;
+    ConstElementPtr comment = config::parseAnswer(rcode, status);
+    ASSERT_EQ(0, rcode);
+}
+
 
 // Generate IA_NA option with specified parameters
 boost::shared_ptr<Option6IA>

+ 12 - 20
src/bin/dhcp6/tests/dhcp6_test_utils.h

@@ -112,6 +112,7 @@ public:
     using Dhcpv6Srv::unpackOptions;
     using Dhcpv6Srv::shutdown_;
     using Dhcpv6Srv::name_change_reqs_;
+    using Dhcpv6Srv::VENDOR_CLASS_PREFIX;
 
     /// @brief packets we pretend to receive
     ///
@@ -341,26 +342,7 @@ public:
     ///
     /// Sets up a single subnet6 with one pool for addresses and second
     /// pool for prefixes.
-    Dhcpv6SrvTest() {
-        subnet_ = isc::dhcp::Subnet6Ptr
-            (new isc::dhcp::Subnet6(isc::asiolink::IOAddress("2001:db8:1::"),
-                                    48, 1000, 2000, 3000, 4000));
-        pool_ = isc::dhcp::Pool6Ptr
-            (new isc::dhcp::Pool6(isc::dhcp::Lease::TYPE_NA,
-                                  isc::asiolink::IOAddress("2001:db8:1:1::"),
-                                  64));
-        subnet_->addPool(pool_);
-
-        isc::dhcp::CfgMgr::instance().deleteSubnets6();
-        isc::dhcp::CfgMgr::instance().addSubnet6(subnet_);
-
-        // configure PD pool
-        pd_pool_ = isc::dhcp::Pool6Ptr
-            (new isc::dhcp::Pool6(isc::dhcp::Lease::TYPE_PD,
-                                  isc::asiolink::IOAddress("2001:db8:1:2::"),
-                                  64, 80));
-        subnet_->addPool(pd_pool_);
-    }
+    Dhcpv6SrvTest();
 
     /// @brief destructor
     ///
@@ -369,6 +351,12 @@ public:
         isc::dhcp::CfgMgr::instance().deleteSubnets6();
     };
 
+    /// @brief Runs DHCPv6 configuration from the JSON string.
+    ///
+    /// @param config String holding server configuration in JSON format.
+    void
+    configure(const std::string& config);
+
     /// @brief Checks that server response (ADVERTISE or REPLY) contains proper
     ///        IA_NA option
     ///
@@ -535,6 +523,7 @@ public:
     isc::dhcp::Pkt6Ptr captureRelayedSolicit();
     isc::dhcp::Pkt6Ptr captureDocsisRelayedSolicit();
     isc::dhcp::Pkt6Ptr captureeRouterRelayedSolicit();
+    isc::dhcp::Pkt6Ptr captureCableLabsShortVendorClass();
 
     /// @brief Auxiliary method that sets Pkt6 fields
     ///
@@ -551,6 +540,9 @@ public:
 
     /// A prefix pool used in most tests
     isc::dhcp::Pool6Ptr pd_pool_;
+
+    /// @brief Server object under test.
+    NakedDhcpv6Srv srv_;
 };
 
 }; // end of isc::test namespace

+ 194 - 132
src/bin/dhcp6/tests/fqdn_unittest.cc

@@ -43,20 +43,68 @@ namespace {
 /// @brief A test fixture class for testing DHCPv6 Client FQDN Option handling.
 class FqdnDhcpv6SrvTest : public Dhcpv6SrvTest {
 public:
+    /// Pointer to Dhcpv6Srv that is used in tests
+    boost::scoped_ptr<NakedDhcpv6Srv> srv_;
+
+    // Reference to D2ClientMgr singleton
+    D2ClientMgr& d2_mgr_;
+
+    // Bit Constants for turning on and off DDNS configuration options.
+    // (Defined here as these are only meaningful to this class.)
+    static const uint16_t ALWAYS_INCLUDE_FQDN = 1;
+    static const uint16_t OVERRIDE_NO_UPDATE = 2;
+    static const uint16_t OVERRIDE_CLIENT_UPDATE = 4;
+    static const uint16_t REPLACE_CLIENT_NAME = 8;
 
     /// @brief Constructor
     FqdnDhcpv6SrvTest()
-        : Dhcpv6SrvTest() {
+        : Dhcpv6SrvTest(), srv_(new NakedDhcpv6Srv(0)),
+          d2_mgr_(CfgMgr::instance().getD2ClientMgr()) {
         // generateClientId assigns DUID to duid_.
         generateClientId();
         lease_.reset(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::1"),
                                 duid_, 1234, 501, 502, 503,
                                 504, 1, 0));
-
+        // Config DDNS to be enabled, all controls off
+        enableD2();
     }
 
     /// @brief Destructor
     virtual ~FqdnDhcpv6SrvTest() {
+        // Default constructor creates a config with DHCP-DDNS updates
+        // disabled.
+        D2ClientConfigPtr cfg(new D2ClientConfig());
+        CfgMgr::instance().setD2ClientConfig(cfg);
+    }
+
+    /// @brief Sets the server's DDNS configuration to ddns updates disabled.
+    void disableD2() {
+        // Default constructor creates a config with DHCP-DDNS updates
+        // disabled.
+        D2ClientConfigPtr cfg(new D2ClientConfig());
+        CfgMgr::instance().setD2ClientConfig(cfg);
+    }
+
+    /// @brief Enables DHCP-DDNS updates with the given options enabled.
+    ///
+    /// Replaces the current D2ClientConfiguration with a configuration
+    /// which as updates enabled and the control options set based upon
+    /// the bit mask of options.
+    ///
+    /// @param mask Bit mask of configuration options that should be enabled.
+    void enableD2(const uint16_t mask = 0) {
+        D2ClientConfigPtr cfg;
+
+        ASSERT_NO_THROW(cfg.reset(new D2ClientConfig(true,
+                                  isc::asiolink::IOAddress("127.0.0.1"), 53001,
+                                  dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
+                                  (mask & ALWAYS_INCLUDE_FQDN),
+                                  (mask & OVERRIDE_NO_UPDATE),
+                                  (mask & OVERRIDE_CLIENT_UPDATE),
+                                  (mask & REPLACE_CLIENT_NAME),
+                                  "myhost", "example.com")));
+        ASSERT_NO_THROW(CfgMgr::instance().setD2ClientConfig(cfg));
+        ASSERT_NO_THROW(srv_->startD2());
     }
 
     /// @brief Construct the DHCPv6 Client FQDN option using flags and
@@ -98,6 +146,7 @@ public:
                             OptionPtr srvid = OptionPtr()) {
         Pkt6Ptr pkt = Pkt6Ptr(new Pkt6(msg_type, 1234));
         pkt->setRemoteAddr(IOAddress("fe80::abcd"));
+        pkt->setIface("eth0");
         Option6IAPtr ia = generateIA(D6O_IA_NA, 234, 1500, 3000);
 
         if (msg_type != DHCPV6_REPLY) {
@@ -130,12 +179,9 @@ public:
     /// server id.
     ///
     /// @param msg_type A type of the message to be created.
-    /// @param srv An object representing the DHCPv6 server, which
-    /// is used to generate the client identifier.
     ///
     /// @return An object representing the created message.
-    Pkt6Ptr generateMessageWithIds(const uint8_t msg_type,
-                                   NakedDhcpv6Srv& srv) {
+    Pkt6Ptr generateMessageWithIds(const uint8_t msg_type) {
         Pkt6Ptr pkt = Pkt6Ptr(new Pkt6(msg_type, 1234));
         // Generate client-id.
         OptionPtr opt_clientid = generateClientId();
@@ -143,7 +189,7 @@ public:
 
         if (msg_type != DHCPV6_SOLICIT) {
             // Generate server-id.
-            pkt->addOption(srv.getServerID());
+            pkt->addOption(srv_->getServerID());
         }
 
         return (pkt);
@@ -242,7 +288,7 @@ public:
                   const Option6ClientFqdn::DomainNameType in_domain_type,
                   const uint8_t exp_flags,
                   const std::string& exp_domain_name) {
-        NakedDhcpv6Srv srv(0);
+
         Pkt6Ptr question = generateMessage(msg_type,
                                            in_flags,
                                            in_domain_name,
@@ -252,7 +298,7 @@ public:
 
         Pkt6Ptr answer(new Pkt6(msg_type == DHCPV6_SOLICIT ? DHCPV6_ADVERTISE :
                                 DHCPV6_REPLY, question->getTransid()));
-        ASSERT_NO_THROW(srv.processClientFqdn(question, answer));
+        ASSERT_NO_THROW(srv_->processClientFqdn(question, answer));
         Option6ClientFqdnPtr answ_fqdn = boost::dynamic_pointer_cast<
             Option6ClientFqdn>(answer->getOption(D6O_CLIENT_FQDN));
         ASSERT_TRUE(answ_fqdn);
@@ -286,7 +332,6 @@ public:
     ///
     /// @param msg_type A type of the client's message.
     /// @param hostname A domain name in the client's FQDN.
-    /// @param srv A server object, used to process the message.
     /// @param include_oro A boolean value which indicates whether the ORO
     /// option should be included in the client's message (if true) or not
     /// (if false). In the former case, the function will expect that server
@@ -295,11 +340,10 @@ public:
     void testProcessMessage(const uint8_t msg_type,
                             const std::string& hostname,
                             const std::string& exp_hostname,
-                            NakedDhcpv6Srv& srv,
                             const bool include_oro = true) {
         // Create a message of a specified type, add server id and
         // FQDN option.
-        OptionPtr srvid = srv.getServerID();
+        OptionPtr srvid = srv_->getServerID();
         // Set the appropriate FQDN type. It must be partial if hostname is
         // empty.
         Option6ClientFqdn::DomainNameType fqdn_type = (hostname.empty() ?
@@ -312,18 +356,18 @@ public:
         // functions to generate response.
         Pkt6Ptr reply;
         if (msg_type == DHCPV6_SOLICIT) {
-            ASSERT_NO_THROW(reply = srv.processSolicit(req));
+            ASSERT_NO_THROW(reply = srv_->processSolicit(req));
 
         } else if (msg_type == DHCPV6_REQUEST) {
-            ASSERT_NO_THROW(reply = srv.processRequest(req));
+            ASSERT_NO_THROW(reply = srv_->processRequest(req));
 
         } else if (msg_type == DHCPV6_RENEW) {
-            ASSERT_NO_THROW(reply = srv.processRequest(req));
+            ASSERT_NO_THROW(reply = srv_->processRequest(req));
 
         } else if (msg_type == DHCPV6_RELEASE) {
             // For Release no lease will be acquired so we have to leave
             // function here.
-            ASSERT_NO_THROW(reply = srv.processRelease(req));
+            ASSERT_NO_THROW(reply = srv_->processRelease(req));
             return;
         } else {
             // We are not interested in testing other message types.
@@ -369,34 +413,44 @@ public:
     /// queue and checks that it holds valid parameters. The NameChangeRequest
     /// is removed from the queue.
     ///
-    /// @param srv A server object holding a queue of NameChangeRequests.
     /// @param type An expected type of the NameChangeRequest (Add or Remove).
     /// @param reverse An expected setting of the reverse update flag.
     /// @param forward An expected setting of the forward udpate flag.
     /// @param addr A string representation of the IPv6 address held in the
     /// NameChangeRequest.
     /// @param dhcid An expected DHCID value.
+    /// @note This value is the value that is produced by
+    /// dhcp_ddns::D2Dhcid::crateDigest() with the appropriate arguments. This
+    /// method uses encryption tools to produce the value which cannot be
+    /// easily duplicated by hand.  It is more or less necessary to generate
+    /// these values programmatically and place them here. Should the
+    /// underlying implementation of createDigest() change these test values
+    /// will likely need to be updated as well.
     /// @param expires A timestamp when the lease associated with the
     /// NameChangeRequest expires.
     /// @param len A valid lifetime of the lease associated with the
     /// NameChangeRequest.
-    void verifyNameChangeRequest(NakedDhcpv6Srv& srv,
-                                 const isc::dhcp_ddns::NameChangeType type,
+    void verifyNameChangeRequest(const isc::dhcp_ddns::NameChangeType type,
                                  const bool reverse, const bool forward,
                                  const std::string& addr,
                                  const std::string& dhcid,
                                  const uint16_t expires,
                                  const uint16_t len) {
-        NameChangeRequest ncr = srv.name_change_reqs_.front();
-        EXPECT_EQ(type, ncr.getChangeType());
-        EXPECT_EQ(forward, ncr.isForwardChange());
-        EXPECT_EQ(reverse, ncr.isReverseChange());
-        EXPECT_EQ(addr, ncr.getIpAddress());
-        EXPECT_EQ(dhcid, ncr.getDhcid().toStr());
-        EXPECT_EQ(expires, ncr.getLeaseExpiresOn());
-        EXPECT_EQ(len, ncr.getLeaseLength());
-        EXPECT_EQ(isc::dhcp_ddns::ST_NEW, ncr.getStatus());
-        srv.name_change_reqs_.pop();
+        NameChangeRequestPtr ncr;
+        ASSERT_NO_THROW(ncr = d2_mgr_.peekAt(0));
+        ASSERT_TRUE(ncr);
+
+        EXPECT_EQ(type, ncr->getChangeType());
+        EXPECT_EQ(forward, ncr->isForwardChange());
+        EXPECT_EQ(reverse, ncr->isReverseChange());
+        EXPECT_EQ(addr, ncr->getIpAddress());
+        EXPECT_EQ(dhcid, ncr->getDhcid().toStr());
+        EXPECT_EQ(expires, ncr->getLeaseExpiresOn());
+        EXPECT_EQ(len, ncr->getLeaseLength());
+        EXPECT_EQ(isc::dhcp_ddns::ST_NEW, ncr->getStatus());
+
+        // Process the message off the queue
+        ASSERT_NO_THROW(d2_mgr_.runReadyIO());
     }
 
     // Holds a lease used by a test.
@@ -443,6 +497,7 @@ TEST_F(FqdnDhcpv6SrvTest, noUpdate) {
 // Test server's response when client requests that server delegates the AAAA
 // update to the client and this delegation is not allowed.
 TEST_F(FqdnDhcpv6SrvTest, clientAAAAUpdateNotAllowed) {
+    enableD2(OVERRIDE_CLIENT_UPDATE);
     testFqdn(DHCPV6_SOLICIT, 0, "myhost.example.com.",
              Option6ClientFqdn::FULL,
              Option6ClientFqdn::FLAG_S | Option6ClientFqdn::FLAG_O,
@@ -452,11 +507,9 @@ TEST_F(FqdnDhcpv6SrvTest, clientAAAAUpdateNotAllowed) {
 // Test that exception is thrown if supplied NULL answer packet when
 // creating NameChangeRequests.
 TEST_F(FqdnDhcpv6SrvTest, createNameChangeRequestsNoAnswer) {
-    NakedDhcpv6Srv srv(0);
-
     Pkt6Ptr answer;
 
-    EXPECT_THROW(srv.createNameChangeRequests(answer),
+    EXPECT_THROW(srv_->createNameChangeRequests(answer),
                  isc::Unexpected);
 
 }
@@ -464,39 +517,33 @@ TEST_F(FqdnDhcpv6SrvTest, createNameChangeRequestsNoAnswer) {
 // Test that exception is thrown if supplied answer from the server
 // contains no DUID when creating NameChangeRequests.
 TEST_F(FqdnDhcpv6SrvTest, createNameChangeRequestsNoDUID) {
-    NakedDhcpv6Srv srv(0);
-
     Pkt6Ptr answer = Pkt6Ptr(new Pkt6(DHCPV6_REPLY, 1234));
     Option6ClientFqdnPtr fqdn = createClientFqdn(Option6ClientFqdn::FLAG_S,
                                                  "myhost.example.com",
                                                  Option6ClientFqdn::FULL);
     answer->addOption(fqdn);
 
-    EXPECT_THROW(srv.createNameChangeRequests(answer), isc::Unexpected);
+    EXPECT_THROW(srv_->createNameChangeRequests(answer), isc::Unexpected);
 
 }
 
 // Test no NameChangeRequests if Client FQDN is not added to the server's
 // response.
 TEST_F(FqdnDhcpv6SrvTest, createNameChangeRequestsNoFQDN) {
-    NakedDhcpv6Srv srv(0);
-
     // Create Reply message with Client Id and Server id.
-    Pkt6Ptr answer = generateMessageWithIds(DHCPV6_REPLY, srv);
+    Pkt6Ptr answer = generateMessageWithIds(DHCPV6_REPLY);
 
-    ASSERT_NO_THROW(srv.createNameChangeRequests(answer));
+    ASSERT_NO_THROW(srv_->createNameChangeRequests(answer));
 
     // There should be no new NameChangeRequests.
-    EXPECT_TRUE(srv.name_change_reqs_.empty());
+    ASSERT_EQ(0, d2_mgr_.getQueueSize());
 }
 
 // Test that NameChangeRequests are not generated if an answer message
 // contains no addresses.
 TEST_F(FqdnDhcpv6SrvTest, createNameChangeRequestsNoAddr) {
-    NakedDhcpv6Srv srv(0);
-
     // Create Reply message with Client Id and Server id.
-    Pkt6Ptr answer = generateMessageWithIds(DHCPV6_REPLY, srv);
+    Pkt6Ptr answer = generateMessageWithIds(DHCPV6_REPLY);
 
     // Add Client FQDN option.
     Option6ClientFqdnPtr fqdn = createClientFqdn(Option6ClientFqdn::FLAG_S,
@@ -504,20 +551,18 @@ TEST_F(FqdnDhcpv6SrvTest, createNameChangeRequestsNoAddr) {
                                                  Option6ClientFqdn::FULL);
     answer->addOption(fqdn);
 
-    ASSERT_NO_THROW(srv.createNameChangeRequests(answer));
+    ASSERT_NO_THROW(srv_->createNameChangeRequests(answer));
 
     // We didn't add any IAs, so there should be no NameChangeRequests in th
     // queue.
-    ASSERT_TRUE(srv.name_change_reqs_.empty());
+    ASSERT_EQ(0, d2_mgr_.getQueueSize());
 }
 
 // Test that exactly one NameChangeRequest is created as a result of processing
 // the answer message which holds 3 IAs and when FQDN is specified.
 TEST_F(FqdnDhcpv6SrvTest, createNameChangeRequests) {
-    NakedDhcpv6Srv srv(0);
-
     // Create Reply message with Client Id and Server id.
-    Pkt6Ptr answer = generateMessageWithIds(DHCPV6_REPLY, srv);
+    Pkt6Ptr answer = generateMessageWithIds(DHCPV6_REPLY);
 
     // Create three IAs, each having different address.
     addIA(1234, IOAddress("2001:db8:1::1"), answer);
@@ -533,11 +578,11 @@ TEST_F(FqdnDhcpv6SrvTest, createNameChangeRequests) {
     answer->addOption(fqdn);
 
     // Create NameChangeRequest for the first allocated address.
-    ASSERT_NO_THROW(srv.createNameChangeRequests(answer));
-    ASSERT_EQ(1, srv.name_change_reqs_.size());
+    ASSERT_NO_THROW(srv_->createNameChangeRequests(answer));
+    ASSERT_EQ(1, d2_mgr_.getQueueSize());
 
     // Verify that NameChangeRequest is correct.
-    verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_ADD, true, true,
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
                             "2001:db8:1::1",
                             "000201415AA33D1187D148275136FA30300478"
                             "FAAAA3EBD29826B5C907B2C9268A6F52",
@@ -545,11 +590,34 @@ TEST_F(FqdnDhcpv6SrvTest, createNameChangeRequests) {
 
 }
 
+// Checks that NameChangeRequests to add entries are not
+// created when ddns updates are disabled.
+TEST_F(FqdnDhcpv6SrvTest, noAddRequestsWhenDisabled) {
+    // Disable DDNS udpates.
+    disableD2();
+
+    // Create Reply message with Client Id and Server id.
+    Pkt6Ptr answer = generateMessageWithIds(DHCPV6_REPLY);
+
+    // Create three IAs, each having different address.
+    addIA(1234, IOAddress("2001:db8:1::1"), answer);
+
+    // Use domain name in upper case. It should be converted to lower-case
+    // before DHCID is calculated. So, we should get the same result as if
+    // we typed domain name in lower-case.
+    Option6ClientFqdnPtr fqdn = createClientFqdn(Option6ClientFqdn::FLAG_S,
+                                                 "MYHOST.EXAMPLE.COM",
+                                                 Option6ClientFqdn::FULL);
+    answer->addOption(fqdn);
+
+    // An attempt to send a NCR would throw.
+    ASSERT_NO_THROW(srv_->createNameChangeRequests(answer));
+}
+
+
 // Test creation of the NameChangeRequest to remove both forward and reverse
 // mapping for the given lease.
 TEST_F(FqdnDhcpv6SrvTest, createRemovalNameChangeRequestFwdRev) {
-    NakedDhcpv6Srv srv(0);
-
     lease_->fqdn_fwd_ = true;
     lease_->fqdn_rev_ = true;
     // Part of the domain name is in upper case, to test that it gets converted
@@ -557,11 +625,10 @@ TEST_F(FqdnDhcpv6SrvTest, createRemovalNameChangeRequestFwdRev) {
     // as if we typed domain-name in lower case.
     lease_->hostname_ = "MYHOST.example.com.";
 
-    ASSERT_NO_THROW(srv.createRemovalNameChangeRequest(lease_));
-
-    ASSERT_EQ(1, srv.name_change_reqs_.size());
+    ASSERT_NO_THROW(srv_->createRemovalNameChangeRequest(lease_));
 
-    verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_REMOVE, true, true,
+    ASSERT_EQ(1, d2_mgr_.getQueueSize());
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, true,
                             "2001:db8:1::1",
                             "000201415AA33D1187D148275136FA30300478"
                             "FAAAA3EBD29826B5C907B2C9268A6F52",
@@ -569,20 +636,36 @@ TEST_F(FqdnDhcpv6SrvTest, createRemovalNameChangeRequestFwdRev) {
 
 }
 
+// Checks that NameChangeRequests to remove entries are not created
+// when ddns updates are disabled.
+TEST_F(FqdnDhcpv6SrvTest, noRemovalsWhenDisabled) {
+    // Disable DDNS updates.
+    disableD2();
+
+    lease_->fqdn_fwd_ = true;
+    lease_->fqdn_rev_ = true;
+    // Part of the domain name is in upper case, to test that it gets converted
+    // to lower case before DHCID is computed. So, we should get the same DHCID
+    // as if we typed domain-name in lower case.
+    lease_->hostname_ = "MYHOST.example.com.";
+
+    // When DDNS is disabled an attempt to send a request will throw.
+    ASSERT_NO_THROW(srv_->createRemovalNameChangeRequest(lease_));
+}
+
+
 // Test creation of the NameChangeRequest to remove reverse mapping for the
 // given lease.
 TEST_F(FqdnDhcpv6SrvTest, createRemovalNameChangeRequestRev) {
-    NakedDhcpv6Srv srv(0);
-
     lease_->fqdn_fwd_ = false;
     lease_->fqdn_rev_ = true;
     lease_->hostname_ = "myhost.example.com.";
 
-    ASSERT_NO_THROW(srv.createRemovalNameChangeRequest(lease_));
+    ASSERT_NO_THROW(srv_->createRemovalNameChangeRequest(lease_));
 
-    ASSERT_EQ(1, srv.name_change_reqs_.size());
+    ASSERT_EQ(1, d2_mgr_.getQueueSize());
 
-    verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_REMOVE, true, false,
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, false,
                             "2001:db8:1::1",
                             "000201415AA33D1187D148275136FA30300478"
                             "FAAAA3EBD29826B5C907B2C9268A6F52",
@@ -593,29 +676,25 @@ TEST_F(FqdnDhcpv6SrvTest, createRemovalNameChangeRequestRev) {
 // Test that NameChangeRequest to remove DNS records is not generated when
 // neither forward nor reverse DNS update has been performed for a lease.
 TEST_F(FqdnDhcpv6SrvTest, createRemovalNameChangeRequestNoUpdate) {
-    NakedDhcpv6Srv srv(0);
-
     lease_->fqdn_fwd_ = false;
     lease_->fqdn_rev_ = false;
 
-    ASSERT_NO_THROW(srv.createRemovalNameChangeRequest(lease_));
+    ASSERT_NO_THROW(srv_->createRemovalNameChangeRequest(lease_));
 
-    EXPECT_TRUE(srv.name_change_reqs_.empty());
+    ASSERT_EQ(0, d2_mgr_.getQueueSize());
 
 }
 
 // Test that NameChangeRequest is not generated if the hostname hasn't been
 // specified for a lease for which forward and reverse mapping has been set.
 TEST_F(FqdnDhcpv6SrvTest, createRemovalNameChangeRequestNoHostname) {
-    NakedDhcpv6Srv srv(0);
-
     lease_->fqdn_fwd_ = true;
     lease_->fqdn_rev_ = true;
     lease_->hostname_ = "";
 
-    ASSERT_NO_THROW(srv.createRemovalNameChangeRequest(lease_));
+    ASSERT_NO_THROW(srv_->createRemovalNameChangeRequest(lease_));
 
-    EXPECT_TRUE(srv.name_change_reqs_.empty());
+    ASSERT_EQ(0, d2_mgr_.getQueueSize());
 
 }
 
@@ -623,28 +702,24 @@ TEST_F(FqdnDhcpv6SrvTest, createRemovalNameChangeRequestNoHostname) {
 // been specified for a lease for which forward and reverse mapping has been
 // set.
 TEST_F(FqdnDhcpv6SrvTest, createRemovalNameChangeRequestWrongHostname) {
-    NakedDhcpv6Srv srv(0);
-
     lease_->fqdn_fwd_ = true;
     lease_->fqdn_rev_ = true;
     lease_->hostname_ = "myhost..example.com.";
 
-    ASSERT_NO_THROW(srv.createRemovalNameChangeRequest(lease_));
+    ASSERT_NO_THROW(srv_->createRemovalNameChangeRequest(lease_));
 
-    EXPECT_TRUE(srv.name_change_reqs_.empty());
+    ASSERT_EQ(0, d2_mgr_.getQueueSize());
 
 }
 
 // Test that Advertise message generated in a response to the Solicit will
 // not result in generation if the NameChangeRequests.
 TEST_F(FqdnDhcpv6SrvTest, processSolicit) {
-    NakedDhcpv6Srv srv(0);
-
     // Create a Solicit message with FQDN option and generate server's
     // response using processSolicit function.
     testProcessMessage(DHCPV6_SOLICIT, "myhost.example.com",
-                       "myhost.example.com.", srv);
-    EXPECT_TRUE(srv.name_change_reqs_.empty());
+                       "myhost.example.com.");
+    ASSERT_EQ(0, d2_mgr_.getQueueSize());
 }
 
 // Test that client may send two requests, each carrying FQDN option with
@@ -652,16 +727,14 @@ TEST_F(FqdnDhcpv6SrvTest, processSolicit) {
 // request but modify the DNS entries for the lease according to the contents
 // of the FQDN sent in the second request.
 TEST_F(FqdnDhcpv6SrvTest, processTwoRequests) {
-    NakedDhcpv6Srv srv(0);
-
     // Create a Request message with FQDN option and generate server's
     // response using processRequest function. This will result in the
     // creation of a new lease and the appropriate NameChangeRequest
     // to add both reverse and forward mapping to DNS.
     testProcessMessage(DHCPV6_REQUEST, "myhost.example.com",
-                       "myhost.example.com.", srv);
-    ASSERT_EQ(1, srv.name_change_reqs_.size());
-    verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_ADD, true, true,
+                       "myhost.example.com.");
+    ASSERT_EQ(1, d2_mgr_.getQueueSize());
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
                             "2001:db8:1:1::dead:beef",
                             "000201415AA33D1187D148275136FA30300478"
                             "FAAAA3EBD29826B5C907B2C9268A6F52",
@@ -675,14 +748,14 @@ TEST_F(FqdnDhcpv6SrvTest, processTwoRequests) {
     // should be added. Therefore, we expect two NameChangeRequests. One to
     // remove the existing entries, one to add new entries.
     testProcessMessage(DHCPV6_REQUEST, "otherhost.example.com",
-                       "otherhost.example.com.", srv);
-    ASSERT_EQ(2, srv.name_change_reqs_.size());
-    verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_REMOVE, true, true,
+                       "otherhost.example.com.");
+    ASSERT_EQ(2, d2_mgr_.getQueueSize());
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, true,
                             "2001:db8:1:1::dead:beef",
                             "000201415AA33D1187D148275136FA30300478"
                             "FAAAA3EBD29826B5C907B2C9268A6F52",
                             0, 4000);
-    verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_ADD, true, true,
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
                             "2001:db8:1:1::dead:beef",
                             "000201D422AA463306223D269B6CB7AFE7AAD265FC"
                             "EA97F93623019B2E0D14E5323D5A",
@@ -696,16 +769,14 @@ TEST_F(FqdnDhcpv6SrvTest, processTwoRequests) {
 // DNS if the Request was sent instead of Soicit. The code should differentiate
 // behavior depending whether Solicit or Request is sent.
 TEST_F(FqdnDhcpv6SrvTest, processRequestSolicit) {
-    NakedDhcpv6Srv srv(0);
-
     // Create a Request message with FQDN option and generate server's
     // response using processRequest function. This will result in the
     // creation of a new lease and the appropriate NameChangeRequest
     // to add both reverse and forward mapping to DNS.
     testProcessMessage(DHCPV6_REQUEST, "myhost.example.com",
-                       "myhost.example.com.", srv);
-    ASSERT_EQ(1, srv.name_change_reqs_.size());
-    verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_ADD, true, true,
+                       "myhost.example.com.");
+    ASSERT_EQ(1, d2_mgr_.getQueueSize());
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
                             "2001:db8:1:1::dead:beef",
                             "000201415AA33D1187D148275136FA30300478"
                             "FAAAA3EBD29826B5C907B2C9268A6F52",
@@ -716,8 +787,8 @@ TEST_F(FqdnDhcpv6SrvTest, processRequestSolicit) {
     // The NameChangeRequest should only be generated when a client sends
     // Request or Renew.
     testProcessMessage(DHCPV6_SOLICIT, "otherhost.example.com",
-                       "otherhost.example.com.", srv);
-    ASSERT_TRUE(srv.name_change_reqs_.empty());
+                       "otherhost.example.com.");
+    ASSERT_EQ(0, d2_mgr_.getQueueSize());
 
 }
 
@@ -728,16 +799,14 @@ TEST_F(FqdnDhcpv6SrvTest, processRequestSolicit) {
 // DNS entry added previously when Request was processed, another one to
 // add a new entry for the FQDN held in the Renew.
 TEST_F(FqdnDhcpv6SrvTest, processRequestRenew) {
-    NakedDhcpv6Srv srv(0);
-
     // Create a Request message with FQDN option and generate server's
     // response using processRequest function. This will result in the
     // creation of a new lease and the appropriate NameChangeRequest
     // to add both reverse and forward mapping to DNS.
     testProcessMessage(DHCPV6_REQUEST, "myhost.example.com",
-                       "myhost.example.com.", srv);
-    ASSERT_EQ(1, srv.name_change_reqs_.size());
-    verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_ADD, true, true,
+                       "myhost.example.com.");
+    ASSERT_EQ(1, d2_mgr_.getQueueSize());
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
                             "2001:db8:1:1::dead:beef",
                             "000201415AA33D1187D148275136FA30300478"
                             "FAAAA3EBD29826B5C907B2C9268A6F52",
@@ -751,14 +820,14 @@ TEST_F(FqdnDhcpv6SrvTest, processRequestRenew) {
     // should be added. Therefore, we expect two NameChangeRequests. One to
     // remove the existing entries, one to add new entries.
     testProcessMessage(DHCPV6_RENEW, "otherhost.example.com",
-                       "otherhost.example.com.", srv);
-    ASSERT_EQ(2, srv.name_change_reqs_.size());
-    verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_REMOVE, true, true,
+                       "otherhost.example.com.");
+    ASSERT_EQ(2, d2_mgr_.getQueueSize());
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, true,
                             "2001:db8:1:1::dead:beef",
                             "000201415AA33D1187D148275136FA30300478"
                             "FAAAA3EBD29826B5C907B2C9268A6F52",
                             0, 4000);
-    verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_ADD, true, true,
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
                             "2001:db8:1:1::dead:beef",
                             "000201D422AA463306223D269B6CB7AFE7AAD265FC"
                             "EA97F93623019B2E0D14E5323D5A",
@@ -767,16 +836,14 @@ TEST_F(FqdnDhcpv6SrvTest, processRequestRenew) {
 }
 
 TEST_F(FqdnDhcpv6SrvTest, processRequestRelease) {
-    NakedDhcpv6Srv srv(0);
-
     // Create a Request message with FQDN option and generate server's
     // response using processRequest function. This will result in the
     // creation of a new lease and the appropriate NameChangeRequest
     // to add both reverse and forward mapping to DNS.
     testProcessMessage(DHCPV6_REQUEST, "myhost.example.com",
-                       "myhost.example.com.", srv);
-    ASSERT_EQ(1, srv.name_change_reqs_.size());
-    verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_ADD, true, true,
+                       "myhost.example.com.");
+    ASSERT_EQ(1, d2_mgr_.getQueueSize());
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
                             "2001:db8:1:1::dead:beef",
                             "000201415AA33D1187D148275136FA30300478"
                             "FAAAA3EBD29826B5C907B2C9268A6F52",
@@ -787,9 +854,9 @@ TEST_F(FqdnDhcpv6SrvTest, processRequestRelease) {
     // also removed. Therefore, we expect that single NameChangeRequest to
     // remove DNS entries is generated.
     testProcessMessage(DHCPV6_RELEASE, "otherhost.example.com",
-                       "otherhost.example.com.", srv);
-    ASSERT_EQ(1, srv.name_change_reqs_.size());
-    verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_REMOVE, true, true,
+                       "otherhost.example.com.");
+    ASSERT_EQ(1, d2_mgr_.getQueueSize());
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, true,
                             "2001:db8:1:1::dead:beef",
                             "000201415AA33D1187D148275136FA30300478"
                             "FAAAA3EBD29826B5C907B2C9268A6F52",
@@ -800,15 +867,13 @@ TEST_F(FqdnDhcpv6SrvTest, processRequestRelease) {
 // Checks that the server include DHCPv6 Client FQDN option in its
 // response even when client doesn't request this option using ORO.
 TEST_F(FqdnDhcpv6SrvTest, processRequestWithoutFqdn) {
-    NakedDhcpv6Srv srv(0);
-
     // The last parameter disables use of the ORO to request FQDN option
     // In this case, we expect that the FQDN option will not be included
     // in the server's response. The testProcessMessage will check that.
     testProcessMessage(DHCPV6_REQUEST, "myhost.example.com",
-                       "myhost.example.com.", srv, false);
-    ASSERT_EQ(1, srv.name_change_reqs_.size());
-    verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_ADD, true, true,
+                       "myhost.example.com.", false);
+    ASSERT_EQ(1, d2_mgr_.getQueueSize());
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
                             "2001:db8:1:1::dead:beef",
                             "000201415AA33D1187D148275136FA30300478"
                             "FAAAA3EBD29826B5C907B2C9268A6F52",
@@ -818,16 +883,13 @@ TEST_F(FqdnDhcpv6SrvTest, processRequestWithoutFqdn) {
 // Checks that FQDN is generated from an ip address, when client sends an empty
 // FQDN.
 TEST_F(FqdnDhcpv6SrvTest, processRequestEmptyFqdn) {
-    NakedDhcpv6Srv srv(0);
-
     testProcessMessage(DHCPV6_REQUEST, "",
-                       "host-2001-db8-1-1--dead-beef.example.com.",
-                       srv, false);
-    ASSERT_EQ(1, srv.name_change_reqs_.size());
-    verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_ADD, true, true,
+                       "myhost-2001-db8-1-1--dead-beef.example.com.", false);
+    ASSERT_EQ(1, d2_mgr_.getQueueSize());
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
                             "2001:db8:1:1::dead:beef",
-                            "0002018D6874B105A5C92DBBD6E4F6C80A93161"
-                            "BC03996F0CD0EB75800DEF997C29961",
+                            "000201C905E54BE12DE6AF92ADE72752B9F362"
+                            "13B5A8BC9D217548CD739B4CF31AFB1B",
                             0, 4000);
 
 }
@@ -844,17 +906,17 @@ TEST_F(FqdnDhcpv6SrvTest, processRequestReuseExpiredLease) {
     CfgMgr::instance().deleteSubnets6();
     subnet_ = Subnet6Ptr(new Subnet6(IOAddress("2001:db8:1:1::"), 56, 1, 2,
                                      3, 4));
+    subnet_->setIface("eth0");
     pool_ = Pool6Ptr(new Pool6(Lease::TYPE_NA, addr, addr));
     subnet_->addPool(pool_);
     CfgMgr::instance().addSubnet6(subnet_);
 
     // Allocate a lease.
-    NakedDhcpv6Srv srv(0);
     testProcessMessage(DHCPV6_REQUEST, "myhost.example.com",
-                       "myhost.example.com.", srv);
+                       "myhost.example.com.");
     // Test that the appropriate NameChangeRequest has been generated.
-    ASSERT_EQ(1, srv.name_change_reqs_.size());
-    verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_ADD, true, true,
+    ASSERT_EQ(1, d2_mgr_.getQueueSize());
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
                             "2001:db8:1:1::dead:beef",
                             "000201415AA33D1187D148275136FA30300478"
                             "FAAAA3EBD29826B5C907B2C9268A6F52",
@@ -884,18 +946,18 @@ TEST_F(FqdnDhcpv6SrvTest, processRequestReuseExpiredLease) {
     // lease database, it is guaranteed that the allocation engine will
     // reuse this lease.
     testProcessMessage(DHCPV6_REQUEST, "myhost.example.com.",
-                       "myhost.example.com.", srv);
-    ASSERT_EQ(2, srv.name_change_reqs_.size());
+                       "myhost.example.com.");
+    ASSERT_EQ(2, d2_mgr_.getQueueSize());
     // The first name change request generated, should remove a DNS
     // mapping for an expired lease.
-    verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_REMOVE, true, true,
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, true,
                             "2001:db8:1:1::dead:beef",
                             "000201D422AA463306223D269B6CB7AFE7AAD2"
                             "65FCEA97F93623019B2E0D14E5323D5A",
                             0, 5);
     // The second name change request should add a DNS mapping for
     // a new lease.
-    verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_ADD, true, true,
+    verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
                             "2001:db8:1:1::dead:beef",
                             "000201415AA33D1187D148275136FA30300478"
                             "FAAAA3EBD29826B5C907B2C9268A6F52",

+ 3 - 0
src/bin/dhcp6/tests/hooks_unittest.cc

@@ -1066,6 +1066,7 @@ TEST_F(HooksDhcpv6SrvTest, basic_lease6_renew) {
     // Let's create a RENEW
     Pkt6Ptr req = Pkt6Ptr(new Pkt6(DHCPV6_RENEW, 1234));
     req->setRemoteAddr(IOAddress("fe80::abcd"));
+    req->setIface("eth0");
     boost::shared_ptr<Option6IA> ia = generateIA(D6O_IA_NA, iaid, 1500, 3000);
 
     OptionPtr renewed_addr_opt(new Option6IAAddr(D6O_IAADDR, addr, 300, 500));
@@ -1163,6 +1164,7 @@ TEST_F(HooksDhcpv6SrvTest, leaseUpdate_lease6_renew) {
     // Let's create a RENEW
     Pkt6Ptr req = Pkt6Ptr(new Pkt6(DHCPV6_RENEW, 1234));
     req->setRemoteAddr(IOAddress("fe80::abcd"));
+    req->setIface("eth0");
     boost::shared_ptr<Option6IA> ia = generateIA(D6O_IA_NA, iaid, 1500, 3000);
 
     OptionPtr renewed_addr_opt(new Option6IAAddr(D6O_IAADDR, addr, 300, 500));
@@ -1254,6 +1256,7 @@ TEST_F(HooksDhcpv6SrvTest, skip_lease6_renew) {
     // Let's create a RENEW
     Pkt6Ptr req = Pkt6Ptr(new Pkt6(DHCPV6_RENEW, 1234));
     req->setRemoteAddr(IOAddress("fe80::abcd"));
+    req->setIface("eth0");
     boost::shared_ptr<Option6IA> ia = generateIA(D6O_IA_NA, iaid, 1500, 3000);
 
     OptionPtr renewed_addr_opt(new Option6IAAddr(D6O_IAADDR, addr, 300, 500));

+ 25 - 1
src/bin/dhcp6/tests/wireshark.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2013  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2014 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
@@ -301,5 +301,29 @@ DHCPv6
     return (pkt);
 }
 
+Pkt6Ptr isc::test::Dhcpv6SrvTest::captureCableLabsShortVendorClass() {
+    // This is a simple non-relayed Solicit:
+    // - client-identifier
+    // - IA_NA
+    // - Vendor Class (4 bytes)
+    //   - enterprise-id 4491
+    // - Vendor-specific Information
+    //   - enterprise-id 4491
+    std::string hex_string =
+        "01671cb90001000e0001000152ea903a08002758f1e80003000c00004bd10000000000"
+        "000000001000040000118b0011000a0000118b000100020020";
+
+    std::vector<uint8_t> bin;
+
+    // Decode the hex string and store it in bin (which happens
+    // to be OptionBuffer format)
+    isc::util::encode::decodeHex(hex_string, bin);
+
+    Pkt6Ptr pkt(new Pkt6(&bin[0], bin.size()));
+    captureSetDefaultFields(pkt);
+    return (pkt);
+
+}
+
 }; // end of isc::test namespace
 }; // end of isc namespace

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

@@ -129,6 +129,12 @@ Debug message. The msgq daemon accepted a session request on the
 shown descriptor of socket and assigned a unique identifier (lname)
 for the client on that socket.
 
+% 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.
+
 % MSGQ_SOCK_CLOSE Closing socket fd %1
 Debug message. Closing the mentioned socket.
 
@@ -146,9 +152,3 @@ data structure.
 % MSGQ_SUBS_NEW_TARGET Creating new target for subscription to group '%1' for instance '%2'
 Debug message. Creating a new subscription. Also creating a new data structure
 to hold it.
-
-% MSGQ_SOCKET_TIMEOUT_ERROR Killing socket %1 because timeout exceeded (%2)
-Outgoing data was queued up on a socket connected to msgq, but the other
-side is not reading it. It could be deadlocked, or may not be monitoring
-it. Both cases are programming errors and should be corrected. The socket
-is closed on the msgq side.

+ 2 - 2
src/bin/resolver/resolver.cc

@@ -418,7 +418,7 @@ Resolver::processMessage(const IOMessage& io_message,
             server->resume(false);
             return;
         }
-    } catch (const Exception& ex) {
+    } catch (const isc::Exception& ex) {
         LOG_DEBUG(resolver_logger, RESOLVER_DBG_IO,
                   RESOLVER_HEADER_PROCESSING_FAILED).arg(ex.what());
         server->resume(false);
@@ -436,7 +436,7 @@ Resolver::processMessage(const IOMessage& io_message,
                          buffer, error.getRcode());
         server->resume(true);
         return;
-    } catch (const Exception& ex) {
+    } catch (const isc::Exception& ex) {
         LOG_DEBUG(resolver_logger, RESOLVER_DBG_IO,
                   RESOLVER_MESSAGE_PROCESSING_FAILED)
                   .arg(ex.what()).arg(Rcode::SERVFAIL());

+ 4 - 2
src/hooks/dhcp/user_chk/Makefile.am

@@ -31,8 +31,7 @@ EXTRA_DIST = libdhcp_user_chk.dox
 #CLEANFILES = *.gcno *.gcda user_chk_messages.h user_chk_messages.cc s-messages
 CLEANFILES = *.gcno *.gcda
 
-nodistdir=$(abs_top_builddir)/src/hooks/dhcp/user_chk
-nodist_LTLIBRARIES = libdhcp_user_chk.la
+noinst_LTLIBRARIES = libdhcp_user_chk.la
 libdhcp_user_chk_la_SOURCES  =
 libdhcp_user_chk_la_SOURCES += load_unload.cc
 libdhcp_user_chk_la_SOURCES += pkt_receive_co.cc
@@ -54,6 +53,9 @@ 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
+# -rpath /nowhere is a hack to trigger libtool to not create a
+# convenience archive, resulting in shared modules
+libdhcp_user_chk_la_LDFLAGS  += -rpath /nowhere
 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

+ 31 - 6
src/lib/Makefile.am

@@ -1,8 +1,33 @@
-if BUILD_EXPERIMENTAL_RESOLVER
-# Build resolver only with --enable-experimental-resolver
-experimental_resolver = resolve
+if WANT_DHCP
+
+want_dhcp = dhcp
+want_dhcp_ddns = dhcp_ddns
+want_dhcpsrv = dhcpsrv
+want_hooks = hooks
+
+endif # WANT_DHCP
+
+if WANT_DNS
+
+want_acl = acl
+want_bench = bench
+want_datasrc = datasrc
+want_nsas = nsas
+want_server_common = server_common
+want_statistics = statistics
+want_xfr = xfr
+
+if WANT_EXPERIMENTAL_RESOLVER
+want_cache = cache
+want_resolve = resolve
 endif
 
-SUBDIRS = exceptions util log hooks cryptolink dns cc config acl xfr bench \
-          asiolink asiodns nsas cache $(experimental_resolver) testutils \
-          datasrc server_common python dhcp dhcp_ddns dhcpsrv statistics
+endif # WANT_DNS
+
+# The following build order must be maintained. So we create the
+# variables above and add directories in that order to SUBDIRS.
+SUBDIRS = exceptions util log $(want_hooks) cryptolink dns cc config \
+          $(want_acl) $(want_xfr) $(want_bench) asiolink asiodns \
+          $(want_nsas) $(want_cache) $(want_resolve) testutils \
+          $(want_datasrc) $(want_server_common) python $(want_dhcp) \
+          $(want_dhcp_ddns) $(want_dhcpsrv) $(want_statistics)

+ 0 - 1
src/lib/cache/Makefile.am

@@ -6,7 +6,6 @@ AM_CPPFLAGS += -I$(top_srcdir)/src/lib/util -I$(top_builddir)/src/lib/util
 AM_CPPFLAGS += -I$(top_srcdir)/src/lib/dns -I$(top_builddir)/src/lib/dns
 AM_CPPFLAGS += -I$(top_srcdir)/src/lib/nsas -I$(top_builddir)/src/lib/nsas
 AM_CPPFLAGS += -I$(top_srcdir)/src/lib/cache -I$(top_builddir)/src/lib/cache
-AM_CPPFLAGS += $(SQLITE_CFLAGS)
 AM_CXXFLAGS = $(B10_CXXFLAGS)
 
 # Some versions of GCC warn about some versions of Boost regarding

+ 20 - 3
src/lib/cc/data.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2010  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2010-2014 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
@@ -964,9 +964,26 @@ merge(ElementPtr element, ConstElementPtr other) {
 
     const std::map<std::string, ConstElementPtr>& m = other->mapValue();
     for (std::map<std::string, ConstElementPtr>::const_iterator it = m.begin();
-         it != m.end() ; ++it) {
+        it != m.end() ; ++it) {
         if ((*it).second && (*it).second->getType() != Element::null) {
-            element->set((*it).first, (*it).second);
+            if (((*it).second->getType() == Element::map) &&
+                element->contains((*it).first)) {
+                // Sub-element is a map and is also in the original config,
+                // so we need to merge them too.
+                boost::shared_ptr<MapElement> merged_map(new MapElement());
+                ConstElementPtr orig_map = element->get((*it).first);
+                ConstElementPtr other_map = (*it).second;
+                if (orig_map->getType() ==  Element::map) {
+                    merged_map->setValue(orig_map->mapValue());
+                }
+
+                // Now go recursive to merge the map sub-elements.
+                merge(merged_map, other_map);
+                element->set((*it).first, merged_map);
+            }
+            else {
+                element->set((*it).first, (*it).second);
+            }
         } else if (element->contains((*it).first)) {
             element->remove((*it).first);
         }

+ 41 - 14
src/lib/cc/tests/data_unittests.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2009  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2009-2014  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
@@ -848,7 +848,7 @@ TEST(Element, merge) {
     ElementPtr a = Element::createMap();
     ElementPtr b = Element::createMap();
     ConstElementPtr c = Element::createMap();
-    merge(a, b);
+    ASSERT_NO_THROW(merge(a, b));
     EXPECT_EQ(*a, *c);
 
     a = Element::fromJSON("1");
@@ -858,75 +858,102 @@ TEST(Element, merge) {
     a = Element::createMap();
     b = Element::fromJSON("{ \"a\": 1 }");
     c = Element::fromJSON("{ \"a\": 1 }");
-    merge(a, b);
+    ASSERT_NO_THROW(merge(a, b));
     EXPECT_EQ(*a, *c);
 
     a = Element::createMap();
     b = Element::fromJSON("{ \"a\": 1 }");
     c = Element::fromJSON("{ \"a\": 1 }");
-    merge(b, a);
+    ASSERT_NO_THROW(merge(b, a));
     EXPECT_EQ(*b, *c);
 
     a = Element::fromJSON("{ \"a\": 1 }");
     b = Element::fromJSON("{ \"a\": 2 }");
     c = Element::fromJSON("{ \"a\": 2 }");
-    merge(a, b);
+    ASSERT_NO_THROW(merge(a, b));
     EXPECT_EQ(*a, *c);
 
     a = Element::fromJSON("{ \"a\": 1 }");
     b = Element::fromJSON("{ \"a\": 2 }");
     c = Element::fromJSON("{ \"a\": 1 }");
-    merge(b, a);
+    ASSERT_NO_THROW(merge(b, a));
     EXPECT_EQ(*b, *c);
 
     a = Element::fromJSON("{ \"a\": { \"b\": \"c\" } }");
     b = Element::fromJSON("{ \"a\": { \"b\": \"d\" } }");
     c = Element::fromJSON("{ \"a\": { \"b\": \"d\" } }");
-    merge(a, b);
+    ASSERT_NO_THROW(merge(a, b));
     EXPECT_EQ(*a, *c);
 
     a = Element::fromJSON("{ \"a\": { \"b\": \"c\" } }");
     b = Element::fromJSON("{ \"a\": { \"b\": \"d\" } }");
     c = Element::fromJSON("{ \"a\": { \"b\": \"c\" } }");
-    merge(b, a);
+    ASSERT_NO_THROW(merge(b, a));
     EXPECT_EQ(*b, *c);
 
     a = Element::fromJSON("{ \"a\": { \"b\": \"c\" } }");
     b = Element::fromJSON("{ \"a\": null }");
     c = Element::fromJSON("{  }");
-    merge(a, b);
+    ASSERT_NO_THROW(merge(a, b));
     EXPECT_EQ(*a, *c);
 
     a = Element::fromJSON("{ \"a\": { \"b\": \"c\" } }");
     b = Element::fromJSON("{ \"a\": null }");
     c = Element::fromJSON("{ \"a\": { \"b\": \"c\" } }");
-    merge(b, a);
+    ASSERT_NO_THROW(merge(b, a));
     EXPECT_EQ(*b, *c);
 
     // And some tests with multiple values
     a = Element::fromJSON("{ \"a\": 1, \"b\": true, \"c\": null }");
     b = Element::fromJSON("{ \"a\": 1, \"b\": null, \"c\": \"a string\" }");
     c = Element::fromJSON("{ \"a\": 1, \"c\": \"a string\" }");
-    merge(a, b);
+    ASSERT_NO_THROW(merge(a, b));
     EXPECT_EQ(*a, *c);
 
     a = Element::fromJSON("{ \"a\": 1, \"b\": true, \"c\": null }");
     b = Element::fromJSON("{ \"a\": 1, \"b\": null, \"c\": \"a string\" }");
     c = Element::fromJSON("{ \"a\": 1, \"b\": true }");
-    merge(b, a);
+    ASSERT_NO_THROW(merge(b, a));
     EXPECT_EQ(*b, *c);
 
     a = Element::fromJSON("{ \"a\": 1, \"b\": 2, \"c\": 3 }");
     b = Element::fromJSON("{ \"a\": 3, \"b\": 2, \"c\": 1 }");
     c = Element::fromJSON("{ \"a\": 3, \"b\": 2, \"c\": 1 }");
-    merge(a, b);
+    ASSERT_NO_THROW(merge(a, b));
     EXPECT_EQ(*a, *c);
 
     a = Element::fromJSON("{ \"a\": 1, \"b\": 2, \"c\": 3 }");
     b = Element::fromJSON("{ \"a\": 3, \"b\": 2, \"c\": 1 }");
     c = Element::fromJSON("{ \"a\": 1, \"b\": 2, \"c\": 3 }");
-    merge(b, a);
+    ASSERT_NO_THROW(merge(b, a));
     EXPECT_EQ(*b, *c);
 
+    // Map sub-elements: original map element is null
+    a = Element::fromJSON("{ \"a\": 1, \"m\": null }");
+    b = Element::fromJSON("{ \"a\": 3, \"m\": {  \"b\": 9 } }");
+    c = Element::fromJSON("{ \"a\": 3, \"m\": {  \"b\": 9 } }");
+    ASSERT_NO_THROW(merge(a, b));
+    EXPECT_EQ(*a, *c);
+
+    // Map sub-elements new map element has less elements than original
+    a = Element::fromJSON("{ \"a\": 1, \"m\": { \"b\": 2, \"c\": 3 } }");
+    b = Element::fromJSON("{ \"a\": 3, \"m\": { \"b\": 9 } }");
+    c = Element::fromJSON("{ \"a\": 3, \"m\": { \"b\": 9, \"c\": 3 } }");
+    ASSERT_NO_THROW(merge(a, b));
+    EXPECT_EQ(*a, *c);
+
+    // Map sub-elements new map element is null
+    a = Element::fromJSON("{ \"a\": 1, \"m\": { \"b\": 2, \"c\": 3 } }");
+    b = Element::fromJSON("{ \"a\": 3, \"m\": null }");
+    c = Element::fromJSON("{ \"a\": 3 }");
+    ASSERT_NO_THROW(merge(a, b));
+    EXPECT_EQ(*a, *c);
+
+    // Map sub-elements new map element has more elments than origina
+    a = Element::fromJSON("{ \"a\": 1, \"m\": { \"b\": 2, \"c\": 3 } }");
+    b = Element::fromJSON("{ \"a\": 3, \"m\": { \"b\": 2, \"c\": 3, \"d\": 4} }");
+    c = Element::fromJSON("{ \"a\": 3, \"m\": { \"b\": 2, \"c\": 3, \"d\": 4} }");
+    ASSERT_NO_THROW(merge(a, b));
+    EXPECT_EQ(*a, *c);
 }
 }

+ 5 - 5
src/lib/datasrc/datasrc_messages.mes

@@ -356,6 +356,11 @@ not contain RRs the requested type.  AN NXRRSET indication is returned.
 A debug message indicating that a query for the given name and RR type is being
 processed.
 
+% DATASRC_LIBRARY_ERROR failure loading %1 datasource library for class %2: %3
+There was a problem loading the dynamic library for a data source. This
+backend is hence not available, and any data sources that use this
+backend will not be available.
+
 % DATASRC_LIST_CACHE_PENDING in-memory cache for data source '%1' is not yet writable, pending load
 While (re)configuring data source clients, zone data of the shown data
 source cannot be loaded to in-memory cache at that point because the
@@ -370,11 +375,6 @@ Therefore, the entire data source will not be available for this process. If
 this is a problem, you should configure the zones of that data source to some
 database backend (sqlite3, for example) and use it from there.
 
-% DATASRC_LIBRARY_ERROR failure loading %1 datasource library for class %2: %3
-There was a problem loading the dynamic library for a data source. This
-backend is hence not available, and any data sources that use this
-backend will not be available.
-
 % DATASRC_LOAD_ZONE_ERROR Error loading zone %1/%2 on data source '%3': %4
 During data source configuration, an error was found in the zone data
 when it was being loaded in to memory on the shown data source.  This

+ 4 - 0
src/lib/dhcp/Makefile.am

@@ -14,6 +14,7 @@ CLEANFILES = *.gcno *.gcda
 
 lib_LTLIBRARIES = libb10-dhcp++.la
 libb10_dhcp___la_SOURCES  =
+libb10_dhcp___la_SOURCES += classify.h
 libb10_dhcp___la_SOURCES += dhcp6.h dhcp4.h
 libb10_dhcp___la_SOURCES += duid.cc duid.h
 libb10_dhcp___la_SOURCES += hwaddr.cc hwaddr.h
@@ -23,6 +24,7 @@ libb10_dhcp___la_SOURCES += iface_mgr_error_handler.h
 libb10_dhcp___la_SOURCES += iface_mgr_linux.cc
 libb10_dhcp___la_SOURCES += iface_mgr_sun.cc
 libb10_dhcp___la_SOURCES += libdhcp++.cc libdhcp++.h
+libb10_dhcp___la_SOURCES += opaque_data_tuple.cc opaque_data_tuple.h
 libb10_dhcp___la_SOURCES += option4_addrlst.cc option4_addrlst.h
 libb10_dhcp___la_SOURCES += option4_client_fqdn.cc option4_client_fqdn.h
 libb10_dhcp___la_SOURCES += option6_ia.cc option6_ia.h
@@ -31,6 +33,7 @@ libb10_dhcp___la_SOURCES += option6_iaprefix.cc option6_iaprefix.h
 libb10_dhcp___la_SOURCES += option6_addrlst.cc option6_addrlst.h
 libb10_dhcp___la_SOURCES += option6_client_fqdn.cc option6_client_fqdn.h
 libb10_dhcp___la_SOURCES += option_vendor.cc option_vendor.h
+libb10_dhcp___la_SOURCES += option_vendor_class.cc option_vendor_class.h
 libb10_dhcp___la_SOURCES += option_int.h
 libb10_dhcp___la_SOURCES += option_int_array.h
 libb10_dhcp___la_SOURCES += option.cc option.h
@@ -67,6 +70,7 @@ EXTRA_DIST  = README libdhcp++.dox
 # written libraries may need access to all libdhcp++ headers.
 libb10_dhcp___includedir = $(pkgincludedir)/dhcp
 libb10_dhcp___include_HEADERS = \
+    classify.h \
     dhcp4.h \
     dhcp6.h \
     duid.h \

+ 123 - 0
src/lib/dhcp/opaque_data_tuple.cc

@@ -0,0 +1,123 @@
+// Copyright (C) 2014 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/opaque_data_tuple.h>
+
+namespace isc {
+namespace dhcp {
+
+OpaqueDataTuple::OpaqueDataTuple(LengthFieldType length_field_type)
+    : length_field_type_(length_field_type) {
+}
+
+void
+OpaqueDataTuple::append(const std::string& text) {
+    // Don't do anything if text is empty.
+    if (!text.empty()) {
+        append(&text[0], text.size());
+    }
+}
+
+void
+OpaqueDataTuple::assign(const std::string& text) {
+    // If empty string is being assigned, reset the buffer.
+    if (text.empty()) {
+        clear();
+    } else {
+        assign(&text[0], text.size());
+    }
+}
+
+void
+OpaqueDataTuple::clear() {
+    data_.clear();
+}
+
+bool
+OpaqueDataTuple::equals(const std::string& other) const {
+    return (getText() == other);
+}
+
+std::string
+OpaqueDataTuple::getText() const {
+    // Convert the data carried in the buffer to a string.
+    return (std::string(data_.begin(), data_.end()));
+}
+
+void
+OpaqueDataTuple::pack(isc::util::OutputBuffer& buf) const {
+    if (getLength() == 0) {
+        isc_throw(OpaqueDataTupleError, "failed to create on-wire format of the"
+                  " opaque data field, because the field appears to be empty");
+    } else if ((1 << (getDataFieldSize() * 8)) <= getLength()) {
+        isc_throw(OpaqueDataTupleError, "failed to create on-wire format of the"
+                  " opaque data field, because current data length "
+                  << getLength() << " exceeds the maximum size for the length"
+                  << " field size " << getDataFieldSize());
+    }
+
+    if (getDataFieldSize() == 1) {
+        buf.writeUint8(static_cast<uint8_t>(getLength()));
+    } else {
+        buf.writeUint16(getLength());
+    }
+
+    buf.writeData(&getData()[0], getLength());
+}
+
+int
+OpaqueDataTuple::getDataFieldSize() const {
+    return (length_field_type_ == LENGTH_1_BYTE ? 1 : 2);
+}
+
+OpaqueDataTuple&
+OpaqueDataTuple::operator=(const std::string& other) {
+    // Replace existing data with the new data converted from a string.
+    assign(&other[0], other.length());
+    return (*this);
+}
+
+bool
+OpaqueDataTuple::operator==(const std::string& other) const {
+    return (equals(other));
+}
+
+bool
+OpaqueDataTuple::operator!=(const std::string& other) {
+    return (!equals(other));
+}
+
+std::ostream& operator<<(std::ostream& os, const OpaqueDataTuple& tuple) {
+    os << tuple.getText();
+    return (os);
+}
+
+std::istream& operator>>(std::istream& is, OpaqueDataTuple& tuple) {
+    // We will replace the current data with new data.
+    tuple.clear();
+    char buf[256];
+    // Read chunks of data as long as we haven't reached the end of the stream.
+    while (!is.eof()) {
+        is.read(buf, sizeof(buf));
+        // Append the chunk of data we have just read. It is fine if the
+        // gcount is 0, because append() function will check that.
+        tuple.append(buf, is.gcount());
+    }
+    // Clear eof flag, so as the stream can be read again.
+    is.clear();
+    return (is);
+}
+
+}
+}

+ 319 - 0
src/lib/dhcp/opaque_data_tuple.h

@@ -0,0 +1,319 @@
+// Copyright (C) 2014 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 OPAQUE_DATA_TUPLE_H
+#define OPAQUE_DATA_TUPLE_H
+
+#include <util/buffer.h>
+#include <util/io_utilities.h>
+#include <iostream>
+#include <iterator>
+#include <string>
+#include <vector>
+
+namespace isc {
+namespace dhcp {
+
+/// @brief Exception to be thrown when the operation on @c OpaqueDataTuple
+/// object results in an error.
+class OpaqueDataTupleError : public Exception {
+public:
+    OpaqueDataTupleError(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
+
+/// @brief Represents a single instance of the opaque data preceded by length.
+///
+/// Some of the DHCP options, such as Vendor Class option (16) in DHCPv6 or
+/// V-I Vendor Class option (124) in DHCPv4 may carry multiple pairs of
+/// opaque-data preceded by its length. Such pairs are called tuples. This class
+/// represents a single instance of the tuple in the DHCPv4 or DHCPv6 option.
+///
+/// Although, the primary purpose of this class is to represent data tuples in
+/// Vendor Class options, there may be other options defined in the future that
+/// may have similar structure and this class can be used to represent the data
+/// tuples in these new options too.
+///
+/// This class exposes a set of convenience methods to assign and retrieve the
+/// opaque data from the tuple. It also implements a method to render the tuple
+/// data into a wire format, as well as a method to create an instance of the
+/// tuple from the wire format.
+class OpaqueDataTuple {
+public:
+
+    /// @brief Size of the length field in the tuple.
+    ///
+    /// In the wire format, the tuple consists of the two fields: one holding
+    /// a length of the opaque data size, second holding opaque data. The first
+    /// field's size may be equal to 1 or 2 bytes. Usually, the tuples carried
+    /// in the DHCPv6 options have 2 byte long length fields, the tuples carried
+    /// in DHCPv4 options have 1 byte long length fields.
+    enum LengthFieldType {
+        LENGTH_1_BYTE,
+        LENGTH_2_BYTES
+    };
+
+    /// @brief Defines a type of the data buffer used to hold the opaque data.
+    typedef std::vector<uint8_t> Buffer;
+
+    /// @brief Default constructor.
+    ///
+    /// @param length_field_type Indicates a length of the field which holds
+    /// the size of the tuple.
+    OpaqueDataTuple(LengthFieldType length_field_type);
+
+    /// @brief Constructor
+    ///
+    /// Creates a tuple from on-wire data. It calls @c OpaqueDataTuple::unpack
+    /// internally.
+    ///
+    /// @param length_field_type Indicates the length of the field holding the
+    /// opaque data size.
+    /// @param begin Iterator pointing to the beginning of the buffer holding
+    /// wire data.
+    /// @param end Iterator pointing to the end of the buffer holding wire data.
+    /// @tparam InputIterator Type of the iterators passed to this function.
+    /// @throw It may throw an exception if the @c unpack throws.
+    template<typename InputIterator>
+    OpaqueDataTuple(LengthFieldType length_field_type, InputIterator begin,
+                    InputIterator end)
+        : length_field_type_(length_field_type) {
+        unpack(begin, end);
+    }
+
+    /// @brief Appends data to the tuple.
+    ///
+    /// This function appends the data of the specified length to the tuple.
+    /// If the specified buffer length is greater than the size of the buffer,
+    /// the behavior of this function is undefined.
+    ///
+    /// @param data Iterator pointing to the beginning of the buffer being
+    /// appended. The source buffer may be an STL object or an array of
+    /// characters. In the latter case, the pointer to the beginning of this
+    /// array should be passed.
+    /// @param len Length of the source buffer.
+    /// @tparam InputIterator Type of the iterator pointing to the beginning of
+    /// the source buffer.
+    template<typename InputIterator>
+    void append(InputIterator data, const size_t len) {
+        data_.insert(data_.end(), data, data + len);
+    }
+
+    /// @brief Appends string to the tuple.
+    ///
+    /// In most cases, the tuple will carry a string. This function appends the
+    /// string to the tuple.
+    ///
+    /// @param text String to be appended in the tuple.
+    void append(const std::string& text);
+
+    /// @brief Assigns data to the tuple.
+    ///
+    /// This function replaces existing data in the tuple with the new data.
+    /// If the speficified buffer length is greater than the size of the buffer,
+    /// the behavior of this function is undefined.
+    /// @param data Iterator pointing to the beginning of the buffer being
+    /// assigned. The source buffer may be an STL object or an array of
+    /// characters. In the latter case, the pointer to the beginning of this
+    /// array should be passed.
+    /// @param len Length of the source buffer.
+    /// @tparam InputIterator Type of the iterator pointing to the beginning of
+    /// the source buffer.
+    template<typename InputIterator>
+    void assign(InputIterator data, const size_t len) {
+        data_.assign(data, data + len);
+    }
+
+    /// @brief Assigns string data to the tuple.
+    ///
+    /// In most cases, the tuple will carry a string. This function sets the
+    /// string to the tuple.
+    ///
+    /// @param text String to be assigned to the tuple.
+    void assign(const std::string& text);
+
+    /// @brief Removes the contents of the tuple.
+    void clear();
+
+    /// @brief Checks if the data carried in the tuple match the string.
+    ///
+    /// @param other String to compare tuple data against.
+    bool equals(const std::string& other) const;
+
+    /// @brief Returns tuple length data field type.
+    LengthFieldType getLengthFieldType() const {
+        return (length_field_type_);
+    }
+
+    /// @brief Returns the length of the data in the tuple.
+    size_t getLength() const {
+        return (data_.size());
+    }
+
+    /// @brief Returns a total size of the tuple, including length field.
+    size_t getTotalLength() const {
+        return (getDataFieldSize() + getLength());
+    }
+
+    /// @brief Returns a reference to the buffer holding tuple data.
+    ///
+    /// @warning The returned reference is valid only within the lifetime
+    /// of the object which returned it. The use of the returned reference
+    /// after the object has been destroyed yelds undefined behavior.
+    const Buffer& getData() const {
+        return (data_);
+    }
+
+    /// @brief Return the tuple data in the textual format.
+    std::string getText() const;
+
+    /// @brief Renders the tuple to a buffer in the wire format.
+    ///
+    /// This function creates the following wire representation of the tuple:
+    /// - 1 or 2 bytes holding a length of the data.
+    /// - variable number of bytes holding data.
+    /// and writes it to the specified buffer. The new are appended to the
+    /// buffer, so as data existing in the buffer is preserved.
+    ///
+    /// The tuple is considered malformed if one of the follwing occurs:
+    /// - the size of the data is 0 (tuple is empty),
+    /// - the size of the data is greater than 255 and the size of the length
+    /// field is 1 byte (see @c LengthFieldType).
+    /// - the size of the data is greater than 65535 and the size of the length
+    /// field is 2 bytes (see @c LengthFieldType).
+    ///
+    /// Function will throw an exception if trying to render malformed tuple.
+    ///
+    /// @param [out] buf Buffer to which the data is rendered.
+    ///
+    /// @throw OpaqueDataTupleError if failed to render the data to the
+    /// buffer because the tuple is malformed.
+    void pack(isc::util::OutputBuffer& buf) const;
+
+    /// @brief Parses wire data and creates a tuple from it.
+    ///
+    /// This function parses on-wire data stored in the provided buffer and
+    /// stores it in the tuple object. The wire data must include at least the
+    /// data field of the length matching the specified @c LengthFieldType.
+    /// The remaining buffer length (excluding the length field) must be equal
+    /// or greater than the length carried in the length field. If any of these
+    /// two conditions is not met, an exception is thrown.
+    ///
+    /// This function allows opaque data with the length of 0.
+    ///
+    /// @param begin Iterator pointing to the beginning of the buffer holding
+    /// wire data.
+    /// @param end Iterator pointing to the end of the buffer holding wire data.
+    /// @tparam InputIterator Type of the iterators passed to this function.
+    template<typename InputIterator>
+    void unpack(InputIterator begin, InputIterator end) {
+        Buffer buf(begin, end);
+        // The buffer must at least hold the size of the data.
+        if (std::distance(begin, end) < getDataFieldSize()) {
+            isc_throw(OpaqueDataTupleError,
+                      "unable to parse the opaque data tuple, the buffer"
+                      " length is " << std::distance(begin, end)
+                      << ", expected at least " << getDataFieldSize());
+        }
+        // Read the data length from the length field, depending on the
+        // size of the data field (1 or 2 bytes).
+        size_t len = getDataFieldSize() == 1 ? *begin :
+            isc::util::readUint16(&(*begin), std::distance(begin, end));
+        // Now that we have the expected data size, let's check that the
+        // reminder of the buffer is long enough.
+        begin += getDataFieldSize();
+        if (std::distance(begin, end) < len) {
+            isc_throw(OpaqueDataTupleError,
+                      "unable to parse the opaque data tuple, the buffer"
+                      " length is " << std::distance(begin, end)
+                      << ", but the length of the tuple in the length field"
+                      " is " << len);
+        }
+        // The buffer length is long enough to read the desired amount of data.
+        assign(begin, len);
+    }
+
+    /// @name Assignment and comparison operators.
+    //{@
+
+    /// @brief Assignment operator.
+    ///
+    /// This operator assigns the string data to the tuple.
+    ///
+    /// @param other string to be assigned to the tuple.
+    /// @return Tuple object after assignment.
+    OpaqueDataTuple& operator=(const std::string& other);
+
+    /// @brief Equality operator.
+    ///
+    /// This operator compares the string given as an argument to the data
+    /// carried in the tuple in the textual format.
+    ///
+    /// @param other String to compare the tuple against.
+    /// @return true if data carried in the tuple is equal to the string.
+    bool operator==(const std::string& other) const;
+
+    /// @brief Inequality operator.
+    ///
+    /// This operator compares the string given as an argument to the data
+    /// carried in the tuple for inequality.
+    ///
+    /// @param other String to compare the tuple against.
+    /// @return true if the data carried in the tuple is unequal the given
+    /// string.
+    bool operator!=(const std::string& other);
+    //@}
+
+    /// @brief Returns the size of the tuple length field.
+    ///
+    /// The returned value depends on the @c LengthFieldType set for the tuple.
+    int getDataFieldSize() const;
+
+private:
+
+    /// @brief Buffer which holds the opaque tuple data.
+    Buffer data_;
+    /// @brief Holds a type of tuple size field (1 byte long or 2 bytes long).
+    LengthFieldType length_field_type_;
+};
+
+/// @brief Pointer to the @c OpaqueDataTuple object.
+typedef boost::shared_ptr<OpaqueDataTuple> OpaqueDataTuplePtr;
+
+/// @brief Inserts the @c OpaqueDataTuple as a string into stream.
+///
+/// This operator gets the tuple data in the textual format and inserts it
+/// into the output stream.
+///
+/// @param os Stream object on which insertion is performed.
+/// @param tuple Object encapsulating a tuple which data in the textual format
+/// is inserted into the stream.
+/// @return Reference to the same stream but after insertion operation.
+std::ostream& operator<<(std::ostream& os, const OpaqueDataTuple& tuple);
+
+/// @brief Inserts data carried in the stream into the tuple.
+///
+/// this operator inserts data carried in the input stream and inserts it to
+/// the @c OpaqueDataTuple object. The existing data is replaced with new data.
+///
+/// @param is Input stream from which the data will be inserted.
+/// @param tuple @c OpaqueDataTuple object to which the data will be inserted.
+/// @return Input stream after insertion to the tuple is performed.
+std::istream& operator>>(std::istream& is, OpaqueDataTuple& tuple);
+
+} // namespace isc::dhcp
+} // namespace isc
+
+#endif

+ 26 - 3
src/lib/dhcp/option_definition.cc

@@ -28,6 +28,7 @@
 #include <dhcp/option_space.h>
 #include <dhcp/option_string.h>
 #include <dhcp/option_vendor.h>
+#include <dhcp/option_vendor_class.h>
 #include <util/encode/hex.h>
 #include <util/strutil.h>
 #include <boost/algorithm/string/classification.hpp>
@@ -400,6 +401,22 @@ OptionDefinition::haveVendor6Format() const {
 }
 
 bool
+OptionDefinition::haveVendorClass4Format() const {
+    return (haveType(OPT_RECORD_TYPE) &&
+            (record_fields_.size() == 2) &&
+            (record_fields_[0] == OPT_UINT32_TYPE) &&
+            (record_fields_[1] == OPT_BINARY_TYPE));
+}
+
+bool
+OptionDefinition::haveVendorClass6Format() const {
+    return (haveType(OPT_RECORD_TYPE) &&
+            (record_fields_.size() == 2) &&
+            (record_fields_[0] == OPT_UINT32_TYPE) &&
+            (record_fields_[1] == OPT_BINARY_TYPE));
+}
+
+bool
 OptionDefinition::convertToBool(const std::string& value_str) const {
     // Case insensitve check that the input is one of: "true" or "false".
     if (boost::iequals(value_str, "true")) {
@@ -651,15 +668,21 @@ OptionDefinition::factorySpecialFormatOption(Option::Universe u,
             // a specialized class to handle it.
             return (OptionPtr(new Option6ClientFqdn(begin, end)));
         } else if (getCode() == D6O_VENDOR_OPTS && haveVendor6Format()) {
-            // Vendor-Specific Information.
+            // Vendor-Specific Information (option code 17)
             return (OptionPtr(new OptionVendor(Option::V6, begin, end)));
+        } else if (getCode() == D6O_VENDOR_CLASS && haveVendorClass6Format()) {
+            // Vendor Class (option code 16).
+            return (OptionPtr(new OptionVendorClass(Option::V6, begin, end)));
         }
     } else {
         if ((getCode() == DHO_FQDN) && haveFqdn4Format()) {
             return (OptionPtr(new Option4ClientFqdn(begin, end)));
-
+        } else if ((getCode() == DHO_VIVCO_SUBOPTIONS) &&
+                   haveVendorClass4Format()) {
+            // V-I Vendor Class (option code 124).
+            return (OptionPtr(new OptionVendorClass(Option::V4, begin, end)));
         } else if (getCode() == DHO_VIVSO_SUBOPTIONS && haveVendor4Format()) {
-            // Vendor-Specific Information.
+            // Vendor-Specific Information (option code 125).
             return (OptionPtr(new OptionVendor(Option::V4, begin, end)));
 
         }

+ 11 - 1
src/lib/dhcp/option_definition.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2014 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
@@ -324,6 +324,16 @@ public:
     /// Vendor-Specific Information %Option.
     bool haveVendor6Format() const;
 
+    /// @brief Check if the option has format of DHCPv4 V-I Vendor Class option.
+    ///
+    /// @return true if the option has the format of DHCPv4 Vendor Class option.
+    bool haveVendorClass4Format() const;
+
+    /// @brief Check if the option has format of DHCPv6 Vendor Class option.
+    ///
+    /// @return true if option has the format of DHCPv6 Vendor Class option.
+    bool haveVendorClass6Format() const;
+
     /// @brief Option factory.
     ///
     /// This function creates an instance of DHCP option using

+ 191 - 0
src/lib/dhcp/option_vendor_class.cc

@@ -0,0 +1,191 @@
+// Copyright (C) 2014 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 <dhcp/opaque_data_tuple.h>
+#include <dhcp/option_vendor_class.h>
+#include <sstream>
+
+namespace isc {
+namespace dhcp {
+
+OptionVendorClass::OptionVendorClass(Option::Universe u,
+                                     const uint32_t vendor_id)
+    : Option(u, getOptionCode(u)), vendor_id_(vendor_id) {
+    if (u == Option::V4) {
+        addTuple(OpaqueDataTuple(OpaqueDataTuple::LENGTH_1_BYTE));
+    }
+}
+
+    OptionVendorClass::OptionVendorClass(Option::Universe u,
+                                         OptionBufferConstIter begin,
+                                         OptionBufferConstIter end)
+    : Option(u, getOptionCode(u)) {
+    unpack(begin, end);
+}
+
+void
+OptionVendorClass::pack(isc::util::OutputBuffer& buf) {
+    packHeader(buf);
+
+    buf.writeUint32(getVendorId());
+
+    for (TuplesCollection::const_iterator it = tuples_.begin();
+         it != tuples_.end(); ++it) {
+        // For DHCPv4 V-I Vendor Class option, there is enterprise id before
+        // every tuple.
+        if ((getUniverse() == V4) && (it != tuples_.begin())) {
+            buf.writeUint32(getVendorId());
+        }
+        it->pack(buf);
+
+    }
+
+}
+
+void
+OptionVendorClass::unpack(OptionBufferConstIter begin,
+                          OptionBufferConstIter end) {
+    if (std::distance(begin, end) < getMinimalLength() - getHeaderLen()) {
+        isc_throw(OutOfRange, "parsed Vendor Class option data truncated to"
+                  " size " << std::distance(begin, end));
+    }
+    // Option must contain at least one enterprise id. It is ok to read 4-byte
+    // value here because we have checked that the buffer he minimal length.
+    vendor_id_ = isc::util::readUint32(&(*begin), distance(begin, end));
+    begin += sizeof(vendor_id_);
+
+    // Start reading opaque data.
+    size_t offset = 0;
+    while (offset < std::distance(begin, end)) {
+        // Parse a tuple.
+        OpaqueDataTuple tuple(getLengthFieldType(), begin + offset, end);
+        addTuple(tuple);
+        // The tuple has been parsed correctly which implies that it is safe to
+        // advance the offset by its total length.
+        offset += tuple.getTotalLength();
+        // For DHCPv4 option, there is enterprise id before every opaque data
+        // tuple. Let's read it, unless we have already reached the end of
+        // buffer.
+        if ((getUniverse() == V4) && (begin + offset != end)) {
+            // Advance the offset by the size of enterprise id.
+            offset += sizeof(vendor_id_);
+            // If the offset already ran over the buffer length or there is
+            // no space left for the empty tuple (thus we add 1), we have
+            // to signal the option truncation.
+            if (offset + 1 >= std::distance(begin, end)) {
+                isc_throw(isc::OutOfRange, "truncated DHCPv4 V-I Vendor Class"
+                          " option - it should contain enterprise id followed"
+                          " by opaque data field tuple");
+            }
+        }
+    }
+}
+
+void
+OptionVendorClass::addTuple(const OpaqueDataTuple& tuple) {
+    if (tuple.getLengthFieldType() != getLengthFieldType()) {
+        isc_throw(isc::BadValue, "attempted to add opaque data tuple having"
+                  " invalid size of the length field "
+                  << tuple.getDataFieldSize() << " to Vendor Class option");
+    }
+
+    tuples_.push_back(tuple);
+}
+
+
+void
+OptionVendorClass::setTuple(const size_t at, const OpaqueDataTuple& tuple) {
+    if (at >= getTuplesNum()) {
+        isc_throw(isc::OutOfRange, "attempted to set an opaque data for the"
+                  " vendor option at position " << at << " which is out of"
+                  " range");
+
+    } else if (tuple.getLengthFieldType() != getLengthFieldType()) {
+        isc_throw(isc::BadValue, "attempted to set opaque data tuple having"
+                  " invalid size of the length field "
+                  << tuple.getDataFieldSize() << " to Vendor Class option");
+    }
+
+    tuples_[at] = tuple;
+}
+
+OpaqueDataTuple
+OptionVendorClass::getTuple(const size_t at) const {
+    if (at >= getTuplesNum()) {
+        isc_throw(isc::OutOfRange, "attempted to get an opaque data for the"
+                  " vendor option at position " << at << " which is out of"
+                  " range. There are only " << getTuplesNum() << " tuples");
+    }
+    return (tuples_[at]);
+}
+
+bool
+OptionVendorClass::hasTuple(const std::string& tuple_str) const {
+    // Iterate over existing tuples (there shouldn't be many of them),
+    // and try to match the searched one.
+    for (TuplesCollection::const_iterator it = tuples_.begin();
+         it != tuples_.end(); ++it) {
+        if (*it == tuple_str) {
+            return (true);
+        }
+    }
+    return (false);
+}
+
+
+uint16_t
+OptionVendorClass::len() {
+    // The option starts with the header and enterprise id.
+    uint16_t length = getHeaderLen() + sizeof(uint32_t);
+    // Now iterate over existing tuples and add their size.
+    for (TuplesCollection::const_iterator it = tuples_.begin();
+         it != tuples_.end(); ++it) {
+        // For DHCPv4 V-I Vendor Class option, there is enterprise id before
+        // every tuple.
+        if ((getUniverse() == V4) && (it != tuples_.begin())) {
+            length += sizeof(uint32_t);
+        }
+        length += it->getTotalLength();
+
+    }
+
+    return (length);
+}
+
+std::string
+OptionVendorClass::toText(int indent) {
+    std::ostringstream s;
+
+    // Apply indentation
+    s << std::string(indent, ' ');
+    // Print type, length and first occurence of enterprise id.
+    s << "type=" << getType() << ", len=" << len() - getHeaderLen() << ", "
+        " enterprise id=0x" << std::hex << getVendorId() << std::dec;
+    // Iterate over all tuples and print their size and contents.
+    for (int i = 0; i < getTuplesNum(); ++i) {
+        // The DHCPv4 V-I Vendor Class has enterprise id before every tuple.
+        if ((getUniverse() == V4) && (i > 0)) {
+            s << ", enterprise id=0x" << std::hex << getVendorId() << std::dec;
+        }
+        // Print the tuple.
+        s << ", data-len" << i << "=" << getTuple(i).getLength();
+        s << ", vendor-class-data" << i << "='" << getTuple(i) << "'";
+    }
+
+    return (s.str());
+}
+
+} // namespace isc::dhcp
+} // namespace isc

+ 197 - 0
src/lib/dhcp/option_vendor_class.h

@@ -0,0 +1,197 @@
+// Copyright (C) 2014 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 OPTION_VENDOR_CLASS_H
+#define OPTION_VENDOR_CLASS_H
+
+#include <dhcp/dhcp4.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/opaque_data_tuple.h>
+#include <dhcp/option.h>
+#include <util/buffer.h>
+#include <boost/shared_ptr.hpp>
+#include <stdint.h>
+
+namespace isc {
+namespace dhcp {
+
+/// @brief This class encapsulates DHCPv6 Vendor Class and DHCPv4 V-I Vendor
+/// Class options.
+///
+/// The format of DHCPv6 Vendor Class option (16) is described in section 22.16
+/// of RFC3315 and the format of the DHCPv4 V-I Vendor Class option (124) is
+/// described in section 3 of RFC3925. Each of these options carries enterprise
+/// id followed by the collection of tuples carring opaque data. A single tuple
+/// consists of the field holding opaque data length and the actual data.
+/// In case of the DHCPv4 V-I Vendor Class each tuple is preceded by the
+/// 4-byte long enterprise id. Also, the field which carries the length of
+/// the tuple is 1-byte long for DHCPv4 V-I Vendor Class and 2-bytes long
+/// for the DHCPv6 Vendor Class option.
+///
+/// Whether the encapsulated format is DHCPv4 V-I Vendor Class or DHCPv6
+/// Vendor Class option is controlled by the @c u (universe) parameter passed
+/// to the constructor.
+///
+/// @todo Currently, the enterprise id field is set to a value of the first
+/// enterprise id occurrence in the parsed option. At some point we should
+/// be able to differentiate between enterprise ids.
+class OptionVendorClass : public Option {
+public:
+
+    /// @brief Collection of opaque data tuples carried by the option.
+    typedef std::vector<OpaqueDataTuple> TuplesCollection;
+
+    /// @brief Constructor.
+    ///
+    /// This constructor instance of the DHCPv4 V-I Vendor Class option (124)
+    /// or DHCPv6 Vendor Class option (16), depending on universe specified.
+    /// If the universe is v4, the constructor adds one empty tuple to the
+    /// option, as per RFC3925, section 3. the complete option must hold at
+    /// least one data-len field for opaque data. If the specified universe
+    /// is v6, the constructor adds no tuples.
+    ///
+    /// @param u universe (v4 or v6).
+    /// @param vendor_id vendor enterprise id (unique 32-bit integer).
+    OptionVendorClass(Option::Universe u, const uint32_t vendor_id);
+
+    /// @brief Constructor.
+    ///
+    /// This constructor creates an instance of the option using a buffer with
+    /// on-wire data. It may throw an exception if the @c unpack method throws.
+    ///
+    /// @param u universe (v4 or v6)
+    /// @param begin Iterator pointing to the beginning of the buffer holding an
+    /// option.
+    /// @param end Iterator pointing to the end of the buffer holding an option.
+    OptionVendorClass(Option::Universe u, OptionBufferConstIter begin,
+                      OptionBufferConstIter end);
+
+    /// @brief Renders option into the buffer in the wire format.
+    ///
+    /// @param [out] buf Buffer to which the option is rendered.
+    virtual void pack(isc::util::OutputBuffer& buf);
+
+    /// @brief Parses buffer holding an option.
+    ///
+    /// This function parses the buffer holding an option and initializes option
+    /// properties: enterprise ids and the collection of tuples.
+    ///
+    /// @param begin Iterator pointing to the beginning of the buffer holding an
+    /// option.
+    /// @param end Iterator pointing to the end of the buffer holding an option.
+    virtual void unpack(OptionBufferConstIter begin, OptionBufferConstIter end);
+
+    /// @brief Returns enterprise id.
+    uint32_t getVendorId() const {
+        return (vendor_id_);
+    }
+
+    /// @brief Adds a new opaque data tuple to the option.
+    ///
+    /// @param tuple Tuple to be added.
+    /// @throw isc::BadValue if the type of the tuple doesn't match the
+    /// universe this option belongs to.
+    void addTuple(const OpaqueDataTuple& tuple);
+
+    /// @brief Replaces tuple at the specified index with a new tuple.
+    ///
+    /// This function replaces an opaque data tuple at the specified position
+    /// with the new tuple. If the specified index is out of range an exception
+    /// is thrown.
+    ///
+    /// @param at Index at which the tuple should be replaced.
+    /// @param tuple Tuple to be set.
+    /// @throw isc::OutOfRange if the tuple position is out of range.
+    /// @throw isc::BadValue if the type of the tuple doesn't match the
+    /// universe this option belongs to.
+    void setTuple(const size_t at, const OpaqueDataTuple& tuple);
+
+    /// @brief Returns opaque data tuple at the specified position.
+    ///
+    ///  If the specified position is out of range an exception is thrown.
+    ///
+    /// @param at Index at which the tuple should be replaced.
+    /// @throw isc::OutOfRange if the tuple position is out of range.
+    OpaqueDataTuple getTuple(const size_t at) const;
+
+    /// @brief Returns the number of opaque data tuples added to the option.
+    size_t getTuplesNum() const {
+        return (tuples_.size());
+    }
+
+    /// @brief Returns collection of opaque data tuples carried in the option.
+    const TuplesCollection& getTuples() const {
+        return (tuples_);
+    }
+
+    /// @brief Checks if the Vendor Class holds the opaque data tuple with the
+    /// specified string.
+    ///
+    /// @param tuple_str String representation of the tuple being searched.
+    /// @return true if the specified tuple exists for this option.
+    bool hasTuple(const std::string& tuple_str) const;
+
+    /// @brief Returns the full length of the option, including option header.
+    virtual uint16_t len();
+
+    /// @brief Returns text representation of the option.
+    ///
+    /// @param indent Number of space characters before text.
+    /// @return Text representation of the option.
+    virtual std::string toText(int indent = 0);
+
+private:
+
+    /// @brief Returns option code appropriate for the specified universe.
+    ///
+    /// This function is called by the constructor to map the specified
+    /// universe to the option code.
+    ///
+    /// @param u universe (V4 or V6).
+    /// @return DHCPv4 V-I Vendor Class or DHCPv6 Vendor Class option code.
+    static uint16_t getOptionCode(Option::Universe u) {
+        return (u == V4 ? DHO_VIVCO_SUBOPTIONS : D6O_VENDOR_CLASS);
+    }
+
+    /// @brief Returns the tuple length field type for the given universe.
+    ///
+    /// This function returns the length field type which should be used
+    /// for the opaque data tuples being added to this option.
+    ///
+    /// @return Tuple length field type for the universe this option belongs to.
+    OpaqueDataTuple::LengthFieldType getLengthFieldType() const {
+        return (getUniverse() == V4 ? OpaqueDataTuple::LENGTH_1_BYTE :
+                OpaqueDataTuple::LENGTH_2_BYTES);
+    }
+
+    /// @brief Returns minimal length of the option for the given universe.
+    uint16_t getMinimalLength() const {
+        return (getUniverse() == Option::V4 ? 7 : 6);
+    }
+
+    /// @brief Enterprise ID.
+    uint32_t vendor_id_;
+
+    /// @brief Collection of opaque data tuples carried by the option.
+    TuplesCollection tuples_;
+
+};
+
+/// @brief Defines a pointer to the @c OptionVendorClass.
+typedef boost::shared_ptr<OptionVendorClass> OptionVendorClassPtr;
+
+}
+}
+
+#endif // OPTION_VENDOR_CLASS_H

+ 9 - 5
src/lib/dhcp/std_option_defs.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2014 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
@@ -68,6 +68,11 @@ struct OptionDefParams {
 RECORD_DECL(FQDN_RECORDS, OPT_UINT8_TYPE, OPT_UINT8_TYPE, OPT_UINT8_TYPE,
             OPT_FQDN_TYPE);
 
+// V-I Vendor Class record fields.
+//
+// Opaque data is represented here by the binary data field.
+RECORD_DECL(VIVCO_RECORDS, OPT_UINT32_TYPE, OPT_BINARY_TYPE);
+
 /// @brief Definitions of standard DHCPv4 options.
 const OptionDefParams OPTION_DEF_PARAMS4[] = {
     { "subnet-mask", DHO_SUBNET_MASK, OPT_IPV4_ADDRESS_TYPE, false, NO_RECORD_DEF, "" },
@@ -192,8 +197,8 @@ const OptionDefParams OPTION_DEF_PARAMS4[] = {
     // dedicated classes to handle them. Until that happens
     // let's treat them as 'binary' options.
     { "domain-search", DHO_DOMAIN_SEARCH, OPT_BINARY_TYPE, false, NO_RECORD_DEF, "" },
-    { "vivco-suboptions", DHO_VIVCO_SUBOPTIONS,
-      OPT_BINARY_TYPE, false, NO_RECORD_DEF, "" },
+    { "vivco-suboptions", DHO_VIVCO_SUBOPTIONS, OPT_RECORD_TYPE,
+      false, RECORD_DEF(VIVCO_RECORDS), "" },
     { "vivso-suboptions", DHO_VIVSO_SUBOPTIONS, OPT_BINARY_TYPE,
       false, NO_RECORD_DEF, "" }
 
@@ -231,8 +236,7 @@ RECORD_DECL(REMOTE_ID_RECORDS, OPT_UINT32_TYPE, OPT_BINARY_TYPE);
 // status-code
 RECORD_DECL(STATUS_CODE_RECORDS, OPT_UINT16_TYPE, OPT_STRING_TYPE);
 // vendor-class
-RECORD_DECL(VENDOR_CLASS_RECORDS, OPT_UINT32_TYPE, OPT_UINT16_TYPE,
-            OPT_STRING_TYPE);
+RECORD_DECL(VENDOR_CLASS_RECORDS, OPT_UINT32_TYPE, OPT_BINARY_TYPE);
 
 /// Standard DHCPv6 option definitions.
 ///

+ 4 - 0
src/lib/dhcp/tests/Makefile.am

@@ -36,6 +36,7 @@ noinst_LTLIBRARIES = libdhcptest.la
 
 libdhcptest_la_SOURCES  = iface_mgr_test_config.cc iface_mgr_test_config.h
 libdhcptest_la_SOURCES  += pkt_filter_test_stub.cc pkt_filter_test_stub.h
+libdhcptest_la_SOURCES  += pkt_filter6_test_stub.cc pkt_filter6_test_stub.h
 libdhcptest_la_CXXFLAGS  = $(AM_CXXFLAGS)
 libdhcptest_la_CPPFLAGS  = $(AM_CPPFLAGS)
 libdhcptest_la_LDFLAGS   = $(AM_LDFLAGS)
@@ -49,6 +50,7 @@ libdhcp___unittests_SOURCES += hwaddr_unittest.cc
 libdhcp___unittests_SOURCES += iface_mgr_unittest.cc
 libdhcp___unittests_SOURCES += iface_mgr_test_config.cc iface_mgr_test_config.h
 libdhcp___unittests_SOURCES += libdhcp++_unittest.cc
+libdhcp___unittests_SOURCES += opaque_data_tuple_unittest.cc
 libdhcp___unittests_SOURCES += option4_addrlst_unittest.cc
 libdhcp___unittests_SOURCES += option4_client_fqdn_unittest.cc
 libdhcp___unittests_SOURCES += option6_addrlst_unittest.cc
@@ -65,12 +67,14 @@ libdhcp___unittests_SOURCES += option_unittest.cc
 libdhcp___unittests_SOURCES += option_space_unittest.cc
 libdhcp___unittests_SOURCES += option_string_unittest.cc
 libdhcp___unittests_SOURCES += option_vendor_unittest.cc
+libdhcp___unittests_SOURCES += option_vendor_class_unittest.cc
 libdhcp___unittests_SOURCES += pkt4_unittest.cc
 libdhcp___unittests_SOURCES += pkt6_unittest.cc
 libdhcp___unittests_SOURCES += pkt_filter_unittest.cc
 libdhcp___unittests_SOURCES += pkt_filter_inet_unittest.cc
 libdhcp___unittests_SOURCES += pkt_filter_inet6_unittest.cc
 libdhcp___unittests_SOURCES += pkt_filter_test_stub.cc pkt_filter_test_stub.h
+libdhcp___unittests_SOURCES += pkt_filter6_test_stub.cc pkt_filter_test_stub.h
 libdhcp___unittests_SOURCES += pkt_filter_test_utils.h pkt_filter_test_utils.cc
 libdhcp___unittests_SOURCES += pkt_filter6_test_utils.h pkt_filter6_test_utils.cc
 

+ 5 - 0
src/lib/dhcp/tests/iface_mgr_test_config.cc

@@ -14,8 +14,10 @@
 
 #include <dhcp/pkt_filter.h>
 #include <dhcp/pkt_filter_inet.h>
+#include <dhcp/pkt_filter_inet6.h>
 #include <dhcp/tests/iface_mgr_test_config.h>
 #include <dhcp/tests/pkt_filter_test_stub.h>
+#include <dhcp/tests/pkt_filter6_test_stub.h>
 
 using namespace isc::asiolink;
 
@@ -27,7 +29,9 @@ IfaceMgrTestConfig::IfaceMgrTestConfig(const bool default_config) {
     IfaceMgr::instance().closeSockets();
     IfaceMgr::instance().clearIfaces();
     packet_filter4_ = PktFilterPtr(new PktFilterTestStub());
+    packet_filter6_ = PktFilter6Ptr(new PktFilter6TestStub());
     IfaceMgr::instance().setPacketFilter(packet_filter4_);
+    IfaceMgr::instance().setPacketFilter(packet_filter6_);
 
     // Create default set of fake interfaces: lo, eth0 and eth1.
     if (default_config) {
@@ -39,6 +43,7 @@ IfaceMgrTestConfig::~IfaceMgrTestConfig() {
     IfaceMgr::instance().closeSockets();
     IfaceMgr::instance().clearIfaces();
     IfaceMgr::instance().setPacketFilter(PktFilterPtr(new PktFilterInet()));
+    IfaceMgr::instance().setPacketFilter(PktFilter6Ptr(new PktFilterInet6()));
 
     IfaceMgr::instance().detectIfaces();
 }

+ 2 - 0
src/lib/dhcp/tests/iface_mgr_test_config.h

@@ -232,6 +232,8 @@ private:
     /// @brief Currently used packet filter for DHCPv4.
     PktFilterPtr packet_filter4_;
 
+    /// @brief Currently used packet filter for DHCPv6.
+    PktFilter6Ptr packet_filter6_;
 };
 
 };

+ 28 - 16
src/lib/dhcp/tests/libdhcp++_unittest.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2014 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
@@ -29,6 +29,7 @@
 #include <dhcp/option_int_array.h>
 #include <dhcp/option_string.h>
 #include <dhcp/option_vendor.h>
+#include <dhcp/option_vendor_class.h>
 #include <util/buffer.h>
 #include <util/encode/hex.h>
 
@@ -941,8 +942,14 @@ TEST_F(LibDhcpTest, stdOptionDefs4) {
     LibDhcpTest::testStdOptionDefs4(DHO_DOMAIN_SEARCH, begin, end,
                                     typeid(Option));
 
-    LibDhcpTest::testStdOptionDefs4(DHO_VIVCO_SUBOPTIONS, begin, end,
-                                    typeid(Option));
+    // V-I Vendor option requires specially crafted data.
+    const char vivco_data[] = {
+        1, 2, 3, 4, // enterprise id
+        3, 1, 2, 3  // first byte is opaque data length, the rest is opaque data
+    };
+    std::vector<uint8_t> vivco_buf(vivco_data, vivco_data + sizeof(vivco_data));
+    LibDhcpTest::testStdOptionDefs4(DHO_VIVCO_SUBOPTIONS, vivco_buf.begin(),
+                                    vivco_buf.end(), typeid(OptionVendorClass));
 
     LibDhcpTest::testStdOptionDefs4(DHO_VIVSO_SUBOPTIONS, begin, end,
                                     typeid(OptionVendor));
@@ -981,6 +988,14 @@ TEST_F(LibDhcpTest, stdOptionDefs6) {
     client_fqdn_buf.insert(client_fqdn_buf.end(), fqdn_buf.begin(),
                            fqdn_buf.end());
 
+    // Initialize test buffer for Vendor Class option.
+    const char vclass_data[] = {
+        0x00, 0x01, 0x02, 0x03,
+        0x00, 0x01, 0x02
+    };
+    std::vector<uint8_t> vclass_buf(vclass_data,
+                                    vclass_data + sizeof(vclass_data));;
+
     // The actual test starts here for all supported option codes.
     LibDhcpTest::testStdOptionDefs6(D6O_CLIENTID, begin, end,
                                     typeid(Option));
@@ -1018,8 +1033,9 @@ TEST_F(LibDhcpTest, stdOptionDefs6) {
     LibDhcpTest::testStdOptionDefs6(D6O_USER_CLASS, begin, end,
                                     typeid(Option));
 
-    LibDhcpTest::testStdOptionDefs6(D6O_VENDOR_CLASS, begin, end,
-                                    typeid(OptionCustom));
+    LibDhcpTest::testStdOptionDefs6(D6O_VENDOR_CLASS, vclass_buf.begin(),
+                                    vclass_buf.end(),
+                                    typeid(OptionVendorClass));
 
     LibDhcpTest::testStdOptionDefs6(D6O_VENDOR_OPTS, begin, end,
                                     typeid(OptionVendor),
@@ -1148,22 +1164,18 @@ TEST_F(LibDhcpTest, vendorClass6) {
     // Option vendor-class should be there
     ASSERT_FALSE(options.find(D6O_VENDOR_CLASS) == options.end());
 
-    // It should be of type OptionCustom
-    boost::shared_ptr<OptionCustom> vclass =
-        boost::dynamic_pointer_cast<OptionCustom> (options.begin()->second);
+    // It should be of type OptionVendorClass
+    boost::shared_ptr<OptionVendorClass> vclass =
+        boost::dynamic_pointer_cast<OptionVendorClass>(options.begin()->second);
     ASSERT_TRUE(vclass);
 
     // Let's investigate if the option content is correct
 
     // 3 fields expected: vendor-id, data-len and data
-    ASSERT_EQ(3, vclass->getDataFieldsNum());
-
-    EXPECT_EQ(4491, vclass->readInteger<uint32_t>
-              (VENDOR_CLASS_ENTERPRISE_ID_INDEX)); // vendor-id=4491
-    EXPECT_EQ(10, vclass->readInteger<uint16_t>
-              (VENDOR_CLASS_DATA_LEN_INDEX)); // data len = 10
-    EXPECT_EQ("eRouter1.0", vclass->readString
-              (VENDOR_CLASS_STRING_INDEX)); // data="eRouter1.0"
+    EXPECT_EQ(4491, vclass->getVendorId());
+    EXPECT_EQ(20, vclass->len());
+    ASSERT_EQ(1, vclass->getTuplesNum());
+    EXPECT_EQ("eRouter1.0", vclass->getTuple(0).getText());
 }
 
 } // end of anonymous space

+ 482 - 0
src/lib/dhcp/tests/opaque_data_tuple_unittest.cc

@@ -0,0 +1,482 @@
+// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+#include <dhcp/opaque_data_tuple.h>
+#include <util/buffer.h>
+#include <gtest/gtest.h>
+#include <algorithm>
+#include <sstream>
+#include <vector>
+
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::util;
+
+namespace {
+
+// This test checks that when the default constructor is called, the data buffer
+// is empty.
+TEST(OpaqueDataTuple, constructor) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    // There should be no data in the tuple.
+    EXPECT_EQ(0, tuple.getLength());
+    EXPECT_TRUE(tuple.getData().empty());
+    EXPECT_TRUE(tuple.getText().empty());
+}
+
+// Test that the constructor which takes the buffer as argument parses the
+// wire data.
+TEST(OpaqueDataTuple, constructorParse1Byte) {
+    const char wire_data[] = {
+        0x0B,                               // Length is 11
+        0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, // Hello<space>
+        0x77, 0x6F, 0x72, 0x6C, 0x64        // world
+    };
+
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_1_BYTE, wire_data,
+                          wire_data + sizeof(wire_data));
+
+    EXPECT_EQ(11, tuple.getLength());
+    EXPECT_EQ("Hello world", tuple.getText());
+
+}
+
+// Test that the constructor which takes the buffer as argument parses the
+// wire data.
+TEST(OpaqueDataTuple, constructorParse2Bytes) {
+    const char wire_data[] = {
+        0x00, 0x0B,                         // Length is 11
+        0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, // Hello<space>
+        0x77, 0x6F, 0x72, 0x6C, 0x64        // world
+    };
+
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES, wire_data,
+                          wire_data + sizeof(wire_data));
+
+    EXPECT_EQ(11, tuple.getLength());
+    EXPECT_EQ("Hello world", tuple.getText());
+
+}
+
+
+// This test checks that it is possible to set the tuple data using raw buffer.
+TEST(OpaqueDataTuple, assignData) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    // Initially the tuple buffer should be empty.
+    OpaqueDataTuple::Buffer buf = tuple.getData();
+    ASSERT_TRUE(buf.empty());
+    // Prepare some input data and assign to the tuple.
+    const uint8_t data1[] = {
+        0xCA, 0xFE, 0xBE, 0xEF
+    };
+    tuple.assign(data1, sizeof(data1));
+    // Tuple should now hold the data we assigned.
+    ASSERT_EQ(sizeof(data1), tuple.getLength());
+    buf = tuple.getData();
+    EXPECT_TRUE(std::equal(buf.begin(), buf.end(), data1));
+
+    // Prepare the other set of data and assign to the tuple.
+    const uint8_t data2[] = {
+        1, 2, 3, 4, 5, 6
+    };
+    tuple.assign(data2, sizeof(data2));
+    // The new data should have replaced the old data.
+    ASSERT_EQ(sizeof(data2), tuple.getLength());
+    buf = tuple.getData();
+    EXPECT_TRUE(std::equal(buf.begin(), buf.end(), data2));
+}
+
+// This test checks that it is possible to append the data to the tuple using
+// raw buffer.
+TEST(OpaqueDataTuple, appendData) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    // Initially the tuple buffer should be empty.
+    OpaqueDataTuple::Buffer buf = tuple.getData();
+    ASSERT_TRUE(buf.empty());
+    // Prepare some input data and append to the empty tuple.
+    const uint8_t data1[] = {
+        0xCA, 0xFE, 0xBE, 0xEF
+    };
+    tuple.append(data1, sizeof(data1));
+    // The tuple should now hold only the data we appended.
+    ASSERT_EQ(sizeof(data1), tuple.getLength());
+    buf = tuple.getData();
+    EXPECT_TRUE(std::equal(buf.begin(), buf.end(), data1));
+    // Prepare the new set of data and append.
+    const uint8_t data2[] = {
+        1, 2, 3, 4, 5, 6
+    };
+    tuple.append(data2, sizeof(data2));
+    // We expect that the tuple now has both sets of data we appended. In order
+    // to verify that, we have to concatenate the input data1 and data2.
+    std::vector<uint8_t> data12(data1, data1 + sizeof(data1));
+    data12.insert(data12.end(), data2, data2 + sizeof(data2));
+    // The size of the tuple should be a sum of data1 and data2 lengths.
+    ASSERT_EQ(sizeof(data1) + sizeof(data2), tuple.getLength());
+    buf = tuple.getData();
+    EXPECT_TRUE(std::equal(buf.begin(), buf.end(), data12.begin()));
+}
+
+// This test checks that it is possible to assign the string to the tuple.
+TEST(OpaqueDataTuple, assignString) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    // Initially, the tuple should be empty.
+    ASSERT_EQ(0, tuple.getLength());
+    // Assign some string data.
+    tuple.assign("Some string");
+    // Verify that the data has been assigned.
+    EXPECT_EQ(11, tuple.getLength());
+    EXPECT_EQ("Some string", tuple.getText());
+    // Assign some other string.
+    tuple.assign("Different string");
+    // The new string should have replaced the old string.
+    EXPECT_EQ(16, tuple.getLength());
+    EXPECT_EQ("Different string", tuple.getText());
+}
+
+// This test checks that it is possible to append the string to the tuple.
+TEST(OpaqueDataTuple, appendString) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    // Initially the tuple should be empty.
+    ASSERT_EQ(0, tuple.getLength());
+    // Append the string to it.
+    tuple.append("First part");
+    ASSERT_EQ(10, tuple.getLength());
+    ASSERT_EQ("First part", tuple.getText());
+    // Now append the other string.
+    tuple.append(" and second part");
+    EXPECT_EQ(26, tuple.getLength());
+    // The resulting data in the tuple should be a concatenation of both
+    // strings.
+    EXPECT_EQ("First part and second part", tuple.getText());
+}
+
+// This test verifies that equals function correctly checks that the tuple
+// holds a given string but it doesn't hold the other. This test also
+// checks the assignment operator for the tuple.
+TEST(OpaqueDataTuple, equals) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    // Tuple is supposed to be empty so it is not equal xyz.
+    EXPECT_FALSE(tuple.equals("xyz"));
+    // Assign xyz.
+    EXPECT_NO_THROW(tuple = "xyz");
+    // The tuple should be equal xyz, but not abc.
+    EXPECT_FALSE(tuple.equals("abc"));
+    EXPECT_TRUE(tuple.equals("xyz"));
+    // Assign abc to the tuple.
+    EXPECT_NO_THROW(tuple = "abc");
+    // It should be now opposite.
+    EXPECT_TRUE(tuple.equals("abc"));
+    EXPECT_FALSE(tuple.equals("xyz"));
+}
+
+// This test checks that the conversion of the data in the tuple to the string
+// is performed correctly.
+TEST(OpaqueDataTuple, getText) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    // Initially the tuple should be empty.
+    ASSERT_TRUE(tuple.getText().empty());
+    // ASCII representation of 'Hello world'.
+    const char as_ascii[] = {
+        0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, // Hello<space>
+        0x77, 0x6F, 0x72, 0x6C, 0x64        // world
+    };
+    // Assign it to the tuple.
+    tuple.assign(as_ascii, sizeof(as_ascii));
+    // Conversion to string should give as the original text.
+    EXPECT_EQ("Hello world", tuple.getText());
+}
+
+// This test verifies the behavior of (in)equality and assignment operators.
+TEST(OpaqueDataTuple, operators) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    // Tuple should be empty initially.
+    ASSERT_EQ(0, tuple.getLength());
+    // Check assignment.
+    EXPECT_NO_THROW(tuple = "Hello World");
+    EXPECT_EQ(11, tuple.getLength());
+    EXPECT_TRUE(tuple == "Hello World");
+    EXPECT_TRUE(tuple != "Something else");
+    // Assign something else to make sure it affects the tuple.
+    EXPECT_NO_THROW(tuple = "Something else");
+    EXPECT_EQ(14, tuple.getLength());
+    EXPECT_TRUE(tuple == "Something else");
+    EXPECT_TRUE(tuple != "Hello World");
+}
+
+// This test verifies that the tuple is inserted in the textual format to the
+// output stream.
+TEST(OpaqueDataTuple, operatorOutputStream) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    // The tuple should be empty initially.
+    ASSERT_EQ(0, tuple.getLength());
+    // The tuple is empty, so assigning its content to the output stream should
+    // be no-op and result in the same text in the stream.
+    std::ostringstream s;
+    s << "Some text";
+    EXPECT_NO_THROW(s << tuple);
+    EXPECT_EQ("Some text", s.str());
+    // Now, let's assign some text to the tuple and call operator again.
+    // The new text should be added to the stream.
+    EXPECT_NO_THROW(tuple = " and some other text");
+    EXPECT_NO_THROW(s << tuple);
+    EXPECT_EQ(s.str(), "Some text and some other text");
+
+}
+
+// This test verifies that the value of the tuple can be initialized from the
+// input stream.
+TEST(OpaqueDataTuple, operatorInputStream) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    // The tuple should be empty initially.
+    ASSERT_EQ(0, tuple.getLength());
+    // The input stream has some text. This text should be appended to the
+    // tuple.
+    std::istringstream s;
+    s.str("Some text");
+    EXPECT_NO_THROW(s >> tuple);
+    EXPECT_EQ("Some text", tuple.getText());
+    // Now, let's assign some other text to the stream. This new text should be
+    // assigned to the tuple.
+    s.str("And some other");
+    EXPECT_NO_THROW(s >> tuple);
+    EXPECT_EQ("And some other", tuple.getText());
+}
+
+// This test checks that the tuple is correctly encoded in the wire format when
+// the size of the length field is 1 byte.
+TEST(OpaqueDataTuple, pack1Byte) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_1_BYTE);
+    // Initially, the tuple should be empty.
+    ASSERT_EQ(0, tuple.getLength());
+    // The empty data doesn't make much sense, so the pack() should not
+    // allow it.
+    OutputBuffer out_buf(10);
+    EXPECT_THROW(tuple.pack(out_buf), OpaqueDataTupleError);
+    // Set the data for tuple.
+    std::vector<uint8_t> data;
+    for (int i = 0; i < 100; ++i) {
+        data.push_back(i);
+    }
+    tuple.assign(data.begin(), data.size());
+    // The pack should now succeed.
+    ASSERT_NO_THROW(tuple.pack(out_buf));
+    // The rendered buffer should be 101 bytes long - 1 byte for length,
+    // 100 bytes for the actual data.
+    ASSERT_EQ(101, out_buf.getLength());
+    // Get the rendered data into the vector for convenience.
+    std::vector<uint8_t>
+        render_data(static_cast<const uint8_t*>(out_buf.getData()),
+                    static_cast<const uint8_t*>(out_buf.getData()) + 101);
+    // The first byte is a length byte. It should hold the length of 100.
+    EXPECT_EQ(100, render_data[0]);
+    // Verify that the rendered data is correct.
+    EXPECT_TRUE(std::equal(render_data.begin() + 1, render_data.end(),
+                           data.begin()));
+    // Reset the output buffer for another test.
+    out_buf.clear();
+    // Fill in the tuple buffer so as it reaches maximum allowed length. The
+    // maximum length is 255 when the size of the length field is one byte.
+    for (int i = 100; i < 255; ++i) {
+        data.push_back(i);
+    }
+    ASSERT_EQ(255, data.size());
+    tuple.assign(data.begin(), data.size());
+    // The pack() should be successful again.
+    ASSERT_NO_THROW(tuple.pack(out_buf));
+    // The rendered buffer should be 256 bytes long. The first byte holds the
+    // opaque data length, the remaining bytes hold the actual data.
+    ASSERT_EQ(256, out_buf.getLength());
+    // Check that the data is correct.
+    render_data.assign(static_cast<const uint8_t*>(out_buf.getData()),
+                       static_cast<const uint8_t*>(out_buf.getData()) + 256);
+    EXPECT_EQ(255, render_data[0]);
+    EXPECT_TRUE(std::equal(render_data.begin() + 1, render_data.end(),
+                           data.begin()));
+    // Clear output buffer for another test.
+    out_buf.clear();
+    // Add one more value to the tuple. Now, the resulting buffer should exceed
+    // the maximum length. An attempt to pack() should fail.
+    data.push_back(255);
+    tuple.assign(data.begin(), data.size());
+    EXPECT_THROW(tuple.pack(out_buf), OpaqueDataTupleError);
+}
+
+// This test checks that the tuple is correctly encoded in the wire format when
+// the size of the length field is 2 bytes.
+TEST(OpaqueDataTuple, pack2Bytes) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    // Initially, the tuple should be empty.
+    ASSERT_EQ(0, tuple.getLength());
+    // The empty data doesn't make much sense, so the pack() should not
+    // allow it.
+    OutputBuffer out_buf(10);
+    EXPECT_THROW(tuple.pack(out_buf), OpaqueDataTupleError);
+    // Set the data for tuple.
+    std::vector<uint8_t> data;
+    for (int i = 0; i < 512; ++i) {
+        data.push_back(i);
+    }
+    tuple.assign(data.begin(), data.size());
+    // The pack should now succeed.
+    ASSERT_NO_THROW(tuple.pack(out_buf));
+    // The rendered buffer should be 514 bytes long - 2 bytes for length,
+    // 512 bytes for the actual data.
+    ASSERT_EQ(514, out_buf.getLength());
+    // Get the rendered data into the vector for convenience.
+    std::vector<uint8_t>
+        render_data(static_cast<const uint8_t*>(out_buf.getData()),
+                    static_cast<const uint8_t*>(out_buf.getData()) + 514);
+    // The first two bytes hold the length of 512.
+    uint16_t len = (render_data[0] << 8) + render_data[1];
+    EXPECT_EQ(512, len);
+    // Verify that the rendered data is correct.
+    EXPECT_TRUE(std::equal(render_data.begin() + 2, render_data.end(),
+                           data.begin()));
+
+    // Without clearing the output buffer, try to do it again. The pack should
+    // append the data to the current buffer.
+    ASSERT_NO_THROW(tuple.pack(out_buf));
+    EXPECT_EQ(1028, out_buf.getLength());
+
+    // Check that we can render the buffer of the maximal allowed size.
+    data.assign(65535, 1);
+    ASSERT_NO_THROW(tuple.assign(data.begin(), data.size()));
+    ASSERT_NO_THROW(tuple.pack(out_buf));
+
+    out_buf.clear();
+
+    // Append one additional byte. The total length of the tuple now exceeds
+    // the maximal value. An attempt to render it should throw an exception.
+    data.assign(1, 1);
+    ASSERT_NO_THROW(tuple.append(data.begin(), data.size()));
+    EXPECT_THROW(tuple.pack(out_buf), OpaqueDataTupleError);
+}
+
+// This test verifies that the tuple is decoded from the wire format.
+TEST(OpaqueDataTuple, unpack1Byte) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_1_BYTE);
+    const char wire_data[] = {
+        0x0B,                               // Length is 11
+        0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, // Hello<space>
+        0x77, 0x6F, 0x72, 0x6C, 0x64        // world
+    };
+
+    ASSERT_NO_THROW(tuple.unpack(wire_data, wire_data + sizeof(wire_data)));
+    EXPECT_EQ(11, tuple.getLength());
+    EXPECT_EQ("Hello world", tuple.getText());
+}
+
+// This test verifies that the tuple having a length of 0, is decoded from
+// the wire format.
+TEST(OpaqueDataTuple, unpack1ByteZeroLength) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_1_BYTE);
+    EXPECT_NO_THROW(tuple = "Hello world");
+    ASSERT_NE(tuple.getLength(), 0);
+
+    const char wire_data[] = {
+        0
+    };
+    ASSERT_NO_THROW(tuple.unpack(wire_data, wire_data + sizeof(wire_data)));
+
+    EXPECT_EQ(0, tuple.getLength());
+}
+
+// This test verfifies that exception is thrown if the empty buffer is being
+// parsed.
+TEST(OpaqueDataTuple, unpack1ByteEmptyBuffer) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_1_BYTE);
+    const char wire_data[] = {
+        1, 2, 3
+    };
+    EXPECT_THROW(tuple.unpack(wire_data, wire_data), OpaqueDataTupleError);
+}
+
+// This test verifies that exception is thrown when parsing truncated buffer.
+TEST(OpaqueDataTuple, unpack1ByteTruncatedBuffer) {
+   OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_1_BYTE);
+    const char wire_data[] = {
+        10, 2, 3
+    };
+    EXPECT_THROW(tuple.unpack(wire_data, wire_data + sizeof(wire_data)),
+                 OpaqueDataTupleError);
+}
+
+// This test verifies that the tuple is decoded from the wire format.
+TEST(OpaqueDataTuple, unpack2Byte) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    std::vector<uint8_t> wire_data;
+    // Set tuple length to 400 (0x190).
+    wire_data.push_back(1);
+    wire_data.push_back(0x90);
+    // Fill in the buffer with some data.
+    for (int i = 0; i < 400; ++i) {
+        wire_data.push_back(i);
+    }
+    // The unpack shoud succeed.
+    ASSERT_NO_THROW(tuple.unpack(wire_data.begin(), wire_data.end()));
+    // The decoded length should be 400.
+    ASSERT_EQ(400, tuple.getLength());
+    // And the data should match.
+    EXPECT_TRUE(std::equal(wire_data.begin() + 2, wire_data.end(),
+                           tuple.getData().begin()));
+}
+
+// This test verifies that the tuple having a length of 0, is decoded from
+// the wire format.
+TEST(OpaqueDataTuple, unpack2ByteZeroLength) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    // Set some data for the tuple.
+    EXPECT_NO_THROW(tuple = "Hello world");
+    ASSERT_NE(tuple.getLength(), 0);
+    // The buffer holds just a length field with the value of 0.
+    const char wire_data[] = {
+        0, 0
+    };
+    // The empty tuple should be successfully decoded.
+    ASSERT_NO_THROW(tuple.unpack(wire_data, wire_data + sizeof(wire_data)));
+    // The data should be replaced with an empty buffer.
+    EXPECT_EQ(0, tuple.getLength());
+}
+
+// This test verifies that exception is thrown if the empty buffer is being
+// parsed.
+TEST(OpaqueDataTuple, unpack2ByteEmptyBuffer) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    //  Initialize the input buffer with some data, just to avoid initializing
+    // empty array.
+    const char wire_data[] = {
+        1, 2, 3
+    };
+    // Pass empty buffer (first iterator equal to second iterator).
+    // This should not be accepted.
+    EXPECT_THROW(tuple.unpack(wire_data, wire_data), OpaqueDataTupleError);
+}
+
+// This test verifies that exception if thrown when parsing truncated buffer.
+TEST(OpaqueDataTuple, unpack2ByteTruncatedBuffer) {
+   OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+   // Specify the data with the length of 10, but limit the buffer size to
+   // 2 bytes.
+   const char wire_data[] = {
+       0, 10, 2, 3
+   };
+   // This should fail because the buffer is truncated.
+   EXPECT_THROW(tuple.unpack(wire_data, wire_data + sizeof(wire_data)),
+                OpaqueDataTupleError);
+}
+
+
+} // anonymous namespace

+ 456 - 0
src/lib/dhcp/tests/option_vendor_class_unittest.cc

@@ -0,0 +1,456 @@
+// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+
+#include <exceptions/exceptions.h>
+#include <dhcp/option_vendor_class.h>
+#include <util/buffer.h>
+#include <gtest/gtest.h>
+
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::util;
+
+namespace {
+
+// This test checks that the DHCPv4 option constructor sets the default
+// properties to the expected values. This constructor should add an
+// empty opaque data tuple (it is essentially the same as adding a 1-byte
+// long field which carries a value of 0).
+TEST(OptionVendorClass, constructor4) {
+    OptionVendorClass vendor_class(Option::V4, 1234);
+    EXPECT_EQ(1234, vendor_class.getVendorId());
+    // Option length is 1 byte for header + 1 byte for option size +
+    // 4 bytes of enterprise id + 1 byte for opaque data.
+    EXPECT_EQ(7, vendor_class.len());
+    // There should be one empty tuple.
+    ASSERT_EQ(1, vendor_class.getTuplesNum());
+    EXPECT_EQ(0, vendor_class.getTuple(0).getLength());
+}
+
+// This test checks that the DHCPv6 option constructor sets the default
+// properties to the expected values.
+TEST(OptionVendorClass, constructor6) {
+    OptionVendorClass vendor_class(Option::V6, 2345);
+    EXPECT_EQ(2345, vendor_class.getVendorId());
+    // Option length is 2 bytes for option code + 2 bytes for option size +
+    // 4 bytes of enterprise id.
+    EXPECT_EQ(8, vendor_class.len());
+    // There should be no tuples.
+    EXPECT_EQ(0, vendor_class.getTuplesNum());
+}
+
+// This test verifies that it is possible to append the opaque data tuple
+// to the option and then retrieve it.
+TEST(OptionVendorClass, addTuple) {
+    OptionVendorClass vendor_class(Option::V6, 2345);
+    // Initially there should be no tuples (for DHCPv6).
+    ASSERT_EQ(0, vendor_class.getTuplesNum());
+    // Create a new tuple and add it to the option.
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    tuple = "xyz";
+    vendor_class.addTuple(tuple);
+    // The option should now hold one tuple.
+    ASSERT_EQ(1, vendor_class.getTuplesNum());
+    EXPECT_EQ("xyz", vendor_class.getTuple(0).getText());
+    // Add another tuple.
+    tuple = "abc";
+    vendor_class.addTuple(tuple);
+    // The option should now hold exactly two tuples in the order in which
+    // they were added.
+    ASSERT_EQ(2, vendor_class.getTuplesNum());
+    EXPECT_EQ("xyz", vendor_class.getTuple(0).getText());
+    EXPECT_EQ("abc", vendor_class.getTuple(1).getText());
+
+    // Check that hasTuple correctly identifies existing tuples.
+    EXPECT_TRUE(vendor_class.hasTuple("xyz"));
+    EXPECT_TRUE(vendor_class.hasTuple("abc"));
+    EXPECT_FALSE(vendor_class.hasTuple("other"));
+
+    // Attempt to add the tuple with 1 byte long length field should fail
+    // for DHCPv6 option.
+    OpaqueDataTuple tuple2(OpaqueDataTuple::LENGTH_1_BYTE);
+    EXPECT_THROW(vendor_class.addTuple(tuple2), isc::BadValue);
+}
+
+// This test checks that it is possible to replace existing tuple.
+TEST(OptionVendorClass, setTuple) {
+    OptionVendorClass vendor_class(Option::V4, 1234);
+    // The DHCPv4 option should carry one empty tuple.
+    ASSERT_EQ(1, vendor_class.getTuplesNum());
+    ASSERT_TRUE(vendor_class.getTuple(0).getText().empty());
+    // Replace the empty tuple with non-empty one.
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_1_BYTE);
+    tuple = "xyz";
+    ASSERT_NO_THROW(vendor_class.setTuple(0, tuple));
+    // There should be one tuple with updated data.
+    ASSERT_EQ(1, vendor_class.getTuplesNum());
+    EXPECT_EQ("xyz", vendor_class.getTuple(0).getText());
+
+    // Add another one.
+    tuple = "abc";
+    vendor_class.addTuple(tuple);
+    ASSERT_EQ(2, vendor_class.getTuplesNum());
+    ASSERT_EQ("abc", vendor_class.getTuple(1).getText());
+
+    // Try to replace them with new tuples.
+    tuple = "new_xyz";
+    ASSERT_NO_THROW(vendor_class.setTuple(0, tuple));
+    ASSERT_EQ(2, vendor_class.getTuplesNum());
+    EXPECT_EQ("new_xyz", vendor_class.getTuple(0).getText());
+
+    tuple = "new_abc";
+    ASSERT_NO_THROW(vendor_class.setTuple(1, tuple));
+    ASSERT_EQ(2, vendor_class.getTuplesNum());
+    EXPECT_EQ("new_abc", vendor_class.getTuple(1).getText());
+
+    // For out of range position, exception should be thrown.
+    tuple = "foo";
+    EXPECT_THROW(vendor_class.setTuple(2, tuple), isc::OutOfRange);
+
+    // Attempt to add the tuple with 2 byte long length field should fail
+    // for DHCPv4 option.
+    OpaqueDataTuple tuple2(OpaqueDataTuple::LENGTH_2_BYTES);
+    EXPECT_THROW(vendor_class.addTuple(tuple2), isc::BadValue);
+}
+
+// Check that the returned length of the DHCPv4 option is correct.
+TEST(OptionVendorClass, len4) {
+    OptionVendorClass vendor_class(Option::V4, 1234);
+    ASSERT_EQ(7, vendor_class.len());
+    // Replace the default empty tuple.
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_1_BYTE);
+    tuple = "xyz";
+    ASSERT_NO_THROW(vendor_class.setTuple(0, tuple));
+    // The total length should get increased by the size of 'xyz'.
+    EXPECT_EQ(10, vendor_class.len());
+    // Add another tuple.
+    tuple = "abc";
+    vendor_class.addTuple(tuple);
+    // The total size now grows by the additional enterprise id and the
+    // 1 byte of the tuple length field and 3 bytes of 'abc'.
+    EXPECT_EQ(18, vendor_class.len());
+}
+
+// Check that the returned length of the DHCPv6 option is correct.
+TEST(OptionVendorClass, len6) {
+    OptionVendorClass vendor_class(Option::V6, 1234);
+    ASSERT_EQ(8, vendor_class.len());
+    // Add first tuple.
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    tuple = "xyz";
+    ASSERT_NO_THROW(vendor_class.addTuple(tuple));
+    // The total length grows by 2 bytes of the length field and 3 bytes
+    // consumed by 'xyz'.
+    EXPECT_EQ(13, vendor_class.len());
+    // Add another tuple and check that the total size gets increased.
+    tuple = "abc";
+    vendor_class.addTuple(tuple);
+    EXPECT_EQ(18, vendor_class.len());
+}
+
+// Check that the option is rendered to the buffer in wire format.
+TEST(OptionVendorClass, pack4) {
+    OptionVendorClass vendor_class(Option::V4, 1234);
+    ASSERT_EQ(1, vendor_class.getTuplesNum());
+    // By default, there is an empty tuple in the option. Let's replace
+    // it with the tuple with some data.
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_1_BYTE);
+    tuple = "Hello world";
+    vendor_class.setTuple(0, tuple);
+    // And add another tuple so as resulting option is a bit more complex.
+    tuple = "foo";
+    vendor_class.addTuple(tuple);
+
+    // Render the data to the buffer.
+    OutputBuffer buf(10);
+    ASSERT_NO_THROW(vendor_class.pack(buf));
+    ASSERT_EQ(26, buf.getLength());
+
+    // Prepare reference data.
+    const uint8_t ref[] = {
+        0x7C, 0x18,                         // option 124, length 24
+        0, 0, 0x4, 0xD2,                    // enterprise id 1234
+        0x0B,                               // tuple length is 11
+        0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, // Hello<space>
+        0x77, 0x6F, 0x72, 0x6C, 0x64,       // world
+        0, 0, 0x4, 0xD2,                    // enterprise id 1234
+        3,                                  // tuple length is 3
+        0x66, 0x6F, 0x6F                    // foo
+    };
+    // Compare the buffer with reference data.
+    EXPECT_EQ(0, memcmp(static_cast<const void*>(ref),
+                        static_cast<const void*>(buf.getData()), 26));
+}
+
+// Check that the DHCPv6 option is rendered to the buffer in wire format.
+TEST(OptionVendorClass, pack6) {
+    OptionVendorClass vendor_class(Option::V6, 1234);
+    ASSERT_EQ(0, vendor_class.getTuplesNum());
+    // Add tuple.
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    tuple = "Hello world";
+    vendor_class.addTuple(tuple);
+    // And add another tuple so as resulting option is a bit more complex.
+    tuple = "foo";
+    vendor_class.addTuple(tuple);
+
+    // Render the data to the buffer.
+    OutputBuffer buf(10);
+    ASSERT_NO_THROW(vendor_class.pack(buf));
+    ASSERT_EQ(26, buf.getLength());
+
+    // Prepare reference data.
+    const uint8_t ref[] = {
+        0x00, 0x10, 0x00, 0x16,             // option 16, length 22
+        0x00, 0x00, 0x04, 0xD2,             // enterprise id 1234
+        0x00, 0x0B,                         // tuple length is 11
+        0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, // Hello<space>
+        0x77, 0x6F, 0x72, 0x6C, 0x64,       // world
+        0x00, 0x03,                         // tuple length is 3
+        0x66, 0x6F, 0x6F                    // foo
+    };
+    // Compare the buffer with reference data.
+    EXPECT_EQ(0, memcmp(static_cast<const void*>(ref),
+                        static_cast<const void*>(buf.getData()),
+                        buf.getLength()));
+}
+
+// This function checks that the DHCPv4 option with two opaque data tuples
+// is parsed correctly.
+TEST(OptionVendorClass, unpack4) {
+    // Prepare data to decode.
+    const uint8_t buf_data[] = {
+        0, 0, 0x4, 0xD2,                    // enterprise id 1234
+        0x0B,                               // tuple length is 11
+        0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, // Hello<space>
+        0x77, 0x6F, 0x72, 0x6C, 0x64,       // world
+        0, 0, 0x4, 0xD2,                    // enterprise id 1234
+        3,                                  // tuple length is 3
+        0x66, 0x6F, 0x6F                    // foo
+    };
+    OptionBuffer buf(buf_data, buf_data + sizeof(buf_data));
+
+    OptionVendorClassPtr vendor_class;
+    ASSERT_NO_THROW(
+        vendor_class = OptionVendorClassPtr(new OptionVendorClass(Option::V4,
+                                                                  buf.begin(),
+                                                                  buf.end()));
+    );
+    EXPECT_EQ(DHO_VIVCO_SUBOPTIONS, vendor_class->getType());
+    EXPECT_EQ(1234, vendor_class->getVendorId());
+    ASSERT_EQ(2, vendor_class->getTuplesNum());
+    EXPECT_EQ("Hello world", vendor_class->getTuple(0).getText());
+    EXPECT_EQ("foo", vendor_class->getTuple(1).getText());
+}
+
+// This function checks that the DHCPv4 option with two opaque data tuples
+// is parsed correctly.
+TEST(OptionVendorClass, unpack6) {
+    // Prepare data to decode.
+    const uint8_t buf_data[] = {
+        0, 0, 0x4, 0xD2,                    // enterprise id 1234
+        0x00, 0x0B,                         // tuple length is 11
+        0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, // Hello<space>
+        0x77, 0x6F, 0x72, 0x6C, 0x64,       // world
+        0x00, 0x03,                         // tuple length is 3
+        0x66, 0x6F, 0x6F                    // foo
+    };
+    OptionBuffer buf(buf_data, buf_data + sizeof(buf_data));
+
+    OptionVendorClassPtr vendor_class;
+    ASSERT_NO_THROW(
+        vendor_class = OptionVendorClassPtr(new OptionVendorClass(Option::V6,
+                                                                  buf.begin(),
+                                                                  buf.end()));
+    );
+    EXPECT_EQ(D6O_VENDOR_CLASS, vendor_class->getType());
+    EXPECT_EQ(1234, vendor_class->getVendorId());
+    ASSERT_EQ(2, vendor_class->getTuplesNum());
+    EXPECT_EQ("Hello world", vendor_class->getTuple(0).getText());
+    EXPECT_EQ("foo", vendor_class->getTuple(1).getText());
+}
+
+
+// This test checks that the DHCPv4 option with opaque data of size 0
+// is correctly parsed.
+TEST(OptionVendorClass, unpack4EmptyTuple) {
+    // Prepare data to decode.
+    const uint8_t buf_data[] = {
+        0, 0, 0x4, 0xD2,                    // enterprise id 1234
+        0x00,                               // tuple length is 0
+    };
+    OptionBuffer buf(buf_data, buf_data + sizeof(buf_data));
+
+    OptionVendorClassPtr vendor_class;
+    ASSERT_NO_THROW(
+        vendor_class = OptionVendorClassPtr(new OptionVendorClass(Option::V4,
+                                                                  buf.begin(),
+                                                                  buf.end()));
+    );
+    EXPECT_EQ(DHO_VIVCO_SUBOPTIONS, vendor_class->getType());
+    EXPECT_EQ(1234, vendor_class->getVendorId());
+    ASSERT_EQ(1, vendor_class->getTuplesNum());
+    EXPECT_TRUE(vendor_class->getTuple(0).getText().empty());
+}
+
+// This test checks that the DHCPv6 option with opaque data of size 0
+// is correctly parsed.
+TEST(OptionVendorClass, unpack6EmptyTuple) {
+    // Prepare data to decode.
+    const uint8_t buf_data[] = {
+        0, 0, 0x4, 0xD2,  // enterprise id 1234
+        0x00, 0x00        // tuple length is 0
+    };
+    OptionBuffer buf(buf_data, buf_data + sizeof(buf_data));
+
+    OptionVendorClassPtr vendor_class;
+    ASSERT_NO_THROW(
+        vendor_class = OptionVendorClassPtr(new OptionVendorClass(Option::V6,
+                                                                  buf.begin(),
+                                                                  buf.end()));
+    );
+    EXPECT_EQ(D6O_VENDOR_CLASS, vendor_class->getType());
+    EXPECT_EQ(1234, vendor_class->getVendorId());
+    ASSERT_EQ(1, vendor_class->getTuplesNum());
+    EXPECT_TRUE(vendor_class->getTuple(0).getText().empty());
+}
+
+// This test checks that exception is thrown when parsing truncated DHCPv4
+// V-I Vendor Class option.
+TEST(OptionVendorClass, unpack4Truncated) {
+    // Prepare data to decode.
+    const uint8_t buf_data[] = {
+        0, 0, 0x4, 0xD2,                    // enterprise id 1234
+        0x0B,                               // tuple length is 11
+        0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, // Hello<space>
+        0x77, 0x6F, 0x72, 0x6C, 0x64,       // world
+        0, 0, 0x4, 0xD2,                    // enterprise id 1234
+    };
+    OptionBuffer buf(buf_data, buf_data + sizeof(buf_data));
+
+    EXPECT_THROW(OptionVendorClass (Option::V4, buf.begin(), buf.end()),
+                 isc::OutOfRange);
+}
+
+// This test checks that exception is thrown when parsing truncated DHCPv6
+// Vendor Class option.
+TEST(OptionVendorClass, unpack6Truncated) {
+    // Prepare data to decode.
+    const uint8_t buf_data[] = {
+        0, 0, 0x4, 0xD2,                    // enterprise id 1234
+        0x00, 0x0B,                         // tuple length is 11
+        0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, // Hello<space>
+        0x77, 0x6F, 0x72, 0x6C              // worl (truncated d!)
+    };
+    OptionBuffer buf(buf_data, buf_data + sizeof(buf_data));
+
+    EXPECT_THROW(OptionVendorClass (Option::V6, buf.begin(), buf.end()),
+                 isc::dhcp::OpaqueDataTupleError);
+}
+
+// This test checks that exception is thrown when parsing DHCPv4 V-I Vendor
+// Class option which doesn't have opaque data length. This test is different
+// from the corresponding test for v6 in that, the v4 test expects that
+// exception is thrown when parsing DHCPv4 option without data-len field
+// (has no tuples), whereas for DHCPv6 option it is perfectly ok that
+// option has no tuples (see class constructor).
+TEST(OptionVendorClass, unpack4NoTuple) {
+    // Prepare data to decode.
+    const uint8_t buf_data[] = {
+        0, 0, 0x4, 0xD2  // enterprise id 1234
+    };
+    OptionBuffer buf(buf_data, buf_data + sizeof(buf_data));
+
+    ASSERT_THROW(OptionVendorClass (Option::V4, buf.begin(), buf.end()),
+                 isc::OutOfRange);
+}
+
+// This test checks that the DHCPv6 Vendor Class option containing no opaque
+// data is parsed correctly. This test is different from the corresponding
+// test for v4 in that, the v6 test checks that the option parsing succeeds
+// when option has no opaque data tuples, whereas the v4 test expects that
+// parsing fails for DHCPv4 option which doesn't have opaque-data (see
+// class constructor).
+TEST(OptionVendorClass, unpack6NoTuple) {
+    // Prepare data to decode.
+    const uint8_t buf_data[] = {
+        0, 0, 0x4, 0xD2  // enterprise id 1234
+    };
+    OptionBuffer buf(buf_data, buf_data + sizeof(buf_data));
+
+    OptionVendorClassPtr vendor_class;
+    ASSERT_NO_THROW(
+        vendor_class = OptionVendorClassPtr(new OptionVendorClass(Option::V6,
+                                                                  buf.begin(),
+                                                                  buf.end()));
+    );
+    EXPECT_EQ(D6O_VENDOR_CLASS, vendor_class->getType());
+    EXPECT_EQ(1234, vendor_class->getVendorId());
+    EXPECT_EQ(0, vendor_class->getTuplesNum());
+}
+
+// Verifies correctness of the text representation of the DHCPv4 option.
+TEST(OptionVendorClass, toText4) {
+    OptionVendorClass vendor_class(Option::V4, 1234);
+    ASSERT_EQ(1, vendor_class.getTuplesNum());
+    // By default, there is an empty tuple in the option. Let's replace
+    // it with the tuple with some data.
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_1_BYTE);
+    tuple = "Hello world";
+    vendor_class.setTuple(0, tuple);
+    // And add another tuple so as resulting option is a bit more complex.
+    tuple = "foo";
+    vendor_class.addTuple(tuple);
+    // Check that the text representation of the option is as expected.
+    EXPECT_EQ("type=124, len=24,  enterprise id=0x4d2,"
+              " data-len0=11, vendor-class-data0='Hello world',"
+              " enterprise id=0x4d2, data-len1=3, vendor-class-data1='foo'",
+              vendor_class.toText());
+
+    // Check that indentation works.
+    EXPECT_EQ("   type=124, len=24,  enterprise id=0x4d2,"
+              " data-len0=11, vendor-class-data0='Hello world',"
+              " enterprise id=0x4d2, data-len1=3, vendor-class-data1='foo'",
+              vendor_class.toText(3));
+}
+
+// Verifies correctness of the text representation of the DHCPv4 option.
+TEST(OptionVendorClass, toText6) {
+    OptionVendorClass vendor_class(Option::V6, 1234);
+    ASSERT_EQ(0, vendor_class.getTuplesNum());
+    // By default, there is an empty tuple in the option. Let's replace
+    // it with the tuple with some data.
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    tuple = "Hello world";
+    vendor_class.addTuple(tuple);
+    // And add another tuple so as resulting option is a bit more complex.
+    tuple = "foo";
+    vendor_class.addTuple(tuple);
+    // Check that the text representation of the option is as expected.
+    EXPECT_EQ("type=16, len=22,  enterprise id=0x4d2,"
+              " data-len0=11, vendor-class-data0='Hello world',"
+              " data-len1=3, vendor-class-data1='foo'",
+              vendor_class.toText());
+
+    // Check that indentation works.
+    EXPECT_EQ("  type=16, len=22,  enterprise id=0x4d2,"
+              " data-len0=11, vendor-class-data0='Hello world',"
+              " data-len1=3, vendor-class-data1='foo'",
+              vendor_class.toText(2));
+}
+
+} // end of anonymous namespace
+

+ 49 - 0
src/lib/dhcp/tests/pkt_filter6_test_stub.cc

@@ -0,0 +1,49 @@
+// Copyright (C) 2014 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/tests/pkt_filter6_test_stub.h>
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+PktFilter6TestStub::PktFilter6TestStub() {
+}
+
+SocketInfo
+PktFilter6TestStub::openSocket(const Iface&,
+           const isc::asiolink::IOAddress& addr,
+           const uint16_t port, const bool) {
+    return (SocketInfo(addr, port, 0));
+}
+
+Pkt6Ptr
+PktFilter6TestStub::receive(const SocketInfo&) {
+    return Pkt6Ptr();
+}
+
+bool
+PktFilter6TestStub::joinMulticast(int, const std::string&,
+                                  const std::string &) {
+    return (true);
+}
+
+int
+PktFilter6TestStub::send(const Iface&, uint16_t, const Pkt6Ptr&) {
+    return (0);
+}
+
+}
+}
+}

+ 99 - 0
src/lib/dhcp/tests/pkt_filter6_test_stub.h

@@ -0,0 +1,99 @@
+// Copyright (C) 2014 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 PKT_FILTER6_TEST_STUB_H
+#define PKT_FILTER6_TEST_STUB_H
+
+#include <asiolink/io_address.h>
+#include <dhcp/iface_mgr.h>
+#include <dhcp/pkt_filter6.h>
+#include <dhcp/pkt6.h>
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @brief A stub implementation of the PktFilter6 class.
+///
+/// This class implements abstract methods of the @c isc::dhcp::PktFilter6
+/// class. It is used by unit tests, which test protected methods of the
+/// @c isc::dhcp::test::PktFilter6 class. The implemented abstract methods are
+/// no-op.
+class PktFilter6TestStub : public PktFilter6 {
+public:
+
+    /// @brief Constructor.
+    PktFilter6TestStub();
+
+    /// @brief Simulate opening of the socket.
+    ///
+    /// This function simulates opening a primary socket. In reality, it doesn't
+    /// open a socket but the socket descriptor returned in the SocketInfo
+    /// structure is always set to 0.
+    ///
+    /// @param iface An interface descriptor.
+    /// @param addr Address on the interface to be used to send packets.
+    /// @param port Port number to bind socket to.
+    /// @param join_multicast A flag which indicates if the socket should be
+    /// configured to join multicast (if true).
+    ///
+    /// @note All parameters are ignored.
+    ///
+    /// @return A SocketInfo structure with the socket descriptor set to 0. The
+    /// fallback socket descriptor is set to a negative value.
+    virtual SocketInfo openSocket(const Iface& iface,
+                                  const isc::asiolink::IOAddress& addr,
+                                  const uint16_t port,
+                                  const bool join_multicast);
+
+    /// @brief Simulate reception of the DHCPv6 message.
+    ///
+    /// @param socket_info A structure holding socket information.
+    ///
+    /// @note All parameters are ignored.
+    ///
+    /// @return always a NULL object.
+    virtual Pkt6Ptr receive(const SocketInfo& socket_info);
+
+    /// @brief Simulates sending a DHCPv6 message.
+    ///
+    /// This function does nothing.
+    ///
+    /// @param iface An interface to be used to send DHCPv6 message.
+    /// @param port A port used to send a message.
+    /// @param pkt A DHCPv6 to be sent.
+    ///
+    /// @note All parameters are ignored.
+    ///
+    /// @return 0.
+    virtual int send(const Iface& iface, uint16_t port, const Pkt6Ptr& pkt);
+
+    /// @brief Simulate joining IPv6 multicast group on a socket.
+    ///
+    /// @note All parameters are ignored.
+    ///
+    /// @param sock A socket descriptor (socket must be bound).
+    /// @param ifname An interface name (for link-scoped multicast groups).
+    /// @param mcast A multicast address to join (e.g. "ff02::1:2").
+    ///
+    /// @return true if multicast join was successful
+    static bool joinMulticast(int sock, const std::string& ifname,
+                              const std::string & mcast);
+};
+
+} // namespace isc::dhcp::test
+} // namespace isc::dhcp
+} // namespace isc
+
+#endif // PKT_FILTER6_TEST_STUB_H

+ 6 - 0
src/lib/dhcp_ddns/dhcp_ddns_messages.mes

@@ -19,6 +19,12 @@ This is an error message that indicates that an invalid request to update
 a DNS entry was received by the application.  Either the format or the content
 of the request is incorrect. The request will be ignored.
 
+% DHCP_DDNS_NCR_FLUSH_IO_ERROR DHCP-DDNS Last send before stopping did not complete successfully: %1
+This is an error message that indicates the DHCP-DDNS client was unable to
+complete the last send prior to exiting send mode.  This is a programmatic
+error, highly unlikely to occur, and should not impair the application's ability
+to process requests.
+
 % DHCP_DDNS_NCR_LISTEN_CLOSE_ERROR application encountered an error while closing the listener used to receive NameChangeRequests : %1
 This is an error message that indicates the application was unable to close the
 listener connection used to receive NameChangeRequests.  Closure may occur

+ 43 - 5
src/lib/dhcp_ddns/ncr_io.cc

@@ -15,6 +15,7 @@
 #include <dhcp_ddns/dhcp_ddns_log.h>
 #include <dhcp_ddns/ncr_io.h>
 
+#include <asio.hpp>
 #include <boost/algorithm/string/predicate.hpp>
 
 namespace isc {
@@ -159,7 +160,7 @@ NameChangeListener::invokeRecvHandler(const Result result,
 NameChangeSender::NameChangeSender(RequestSendHandler& send_handler,
                                    size_t send_queue_max)
     : sending_(false), send_handler_(send_handler),
-      send_queue_max_(send_queue_max) {
+      send_queue_max_(send_queue_max), io_service_(NULL) {
 
     // Queue size must be big enough to hold at least 1 entry.
     setQueueMaxSize(send_queue_max);
@@ -177,6 +178,8 @@ NameChangeSender::startSending(isc::asiolink::IOService& io_service) {
 
     // Call implementation dependent open.
     try {
+        // Remember io service we're given.
+        io_service_ = &io_service;
         open(io_service);
     } catch (const isc::Exception& ex) {
         stopSending();
@@ -185,10 +188,30 @@ NameChangeSender::startSending(isc::asiolink::IOService& io_service) {
 
     // Set our status to sending.
     setSending(true);
+
+    // If there's any queued already.. we'll start sending.
+    sendNext();
 }
 
 void
 NameChangeSender::stopSending() {
+    // Set it send indicator to false, no matter what. This allows us to at 
+    // least try to re-open via startSending(). Also, setting it false now, 
+    // allows us to break sendNext() chain in invokeSendHandler.
+    setSending(false);
+
+    // If there is an outstanding IO to complete, attempt to process it.
+    if (ioReady() && io_service_ != NULL) {
+        try {
+            runReadyIO();
+        } catch (const std::exception& ex) {
+            // Swallow exceptions. If we have some sort of error we'll log
+            // it but we won't propagate the throw.
+            LOG_ERROR(dhcp_ddns_logger,
+                  DHCP_DDNS_NCR_FLUSH_IO_ERROR).arg(ex.what());
+        }
+    }
+
     try {
         // Call implementation dependent close.
         close();
@@ -199,9 +222,7 @@ NameChangeSender::stopSending() {
                   DHCP_DDNS_NCR_SEND_CLOSE_ERROR).arg(ex.what());
     }
 
-    // Set it false, no matter what.  This allows us to at least try to
-    // re-open via startSending().
-    setSending(false);
+    io_service_ = NULL;
 }
 
 void
@@ -274,7 +295,9 @@ NameChangeSender::invokeSendHandler(const NameChangeSender::Result result) {
 
     // Set up the next send
     try {
-        sendNext();
+        if (amSending()) {
+            sendNext();
+        }
     } catch (const isc::Exception& ex) {
         // It is possible though unlikely, for sendNext to fail without
         // scheduling the send. While, unlikely, it does mean the callback
@@ -367,5 +390,20 @@ NameChangeSender::getSelectFd() {
     isc_throw(NotImplemented, "NameChangeSender::getSelectFd is not supported");
 }
 
+void
+NameChangeSender::runReadyIO() {
+    if (!io_service_) {
+        isc_throw(NcrSenderError, "NameChangeSender::runReadyIO"
+                  " sender io service is null");
+    }
+
+    // We shouldn't be here if IO isn't ready to execute.
+    // By running poll we're gauranteed not to hang.
+    /// @todo Trac# 3325 requests that asiolink::IOService provide a
+    /// wrapper for poll().
+    io_service_->get_io_service().poll_one();
+}
+
+
 } // namespace isc::dhcp_ddns
 } // namespace isc

+ 35 - 2
src/lib/dhcp_ddns/ncr_io.h

@@ -161,7 +161,7 @@ public:
 /// Assuming the open is successful, startListener will call receiveNext, to
 /// initiate an asynchronous receive.  This method calls the virtual method,
 /// doReceive().  The listener derivation uses doReceive to instigate an IO
-/// layer asynchronous receieve passing in its IO layer callback to
+/// layer asynchronous receive passing in its IO layer callback to
 /// handle receive events from the IO source.
 ///
 /// As stated earlier, the derivation's NameChangeRequest completion handler
@@ -576,6 +576,11 @@ public:
     /// @throw NcrSenderError if the sender is not in send mode,
     virtual int getSelectFd() = 0;
 
+    /// @brief Returns whether or not the sender has IO ready to process.
+    ///
+    /// @return true if the sender has at IO ready, false otherwise.
+    virtual bool ioReady() = 0;
+
 protected:
     /// @brief Dequeues and sends the next request on the send queue.
     ///
@@ -688,7 +693,7 @@ public:
         return (send_queue_max_);
     }
 
-    /// @brief Sets the maxium queue size to the given value.
+    /// @brief Sets the maximum queue size to the given value.
     ///
     /// Sets the maximum number of entries allowed in the queue to the
     /// the given value.
@@ -715,6 +720,28 @@ public:
     /// end of the queue.
     const NameChangeRequestPtr& peekAt(const size_t index) const;
 
+    /// @brief Processes sender IO events
+    ///
+    /// Executes at most one ready handler on the sender's IO service. If
+    /// no handlers are ready it returns immediately.
+    ///
+    /// @warning - Running all ready handlers, in theory, could process all
+    /// messages currently queued.
+    ///
+    /// NameChangeSender daisy chains requests together in its completion
+    /// by one message completion's handler initiating the next message's send.
+    /// When using UDP, a send immediately marks its event handler as ready
+    /// to run.  If this occurs inside a call to ioservice::poll() or run(),
+    /// that event will also be run.  If that handler calls UDP send then
+    /// that send's handler will be marked ready and executed and so on.  If
+    /// there were 1000 messages in the queue then all them would be sent from
+    /// within the context of one call to runReadyIO().
+    /// By running only one handler at time, we ensure that NCR IO activity
+    /// doesn't starve other processing.  It is unclear how much of a real
+    /// threat this poses but for now it is best to err on the side of caution.
+    ///
+    virtual void runReadyIO();
+
 protected:
     /// @brief Returns a reference to the send queue.
     SendQueue& getSendQueue() {
@@ -746,6 +773,12 @@ private:
 
     /// @brief Pointer to the request which is in the process of being sent.
     NameChangeRequestPtr ncr_to_send_;
+
+    /// @brief Pointer to the IOService currently being used by the sender.
+    /// @note We need to remember the io_service but we receive it by
+    /// reference.  Use a raw pointer to store it.  This value should never be
+    /// exposed and is only valid while in send mode.
+    asiolink::IOService* io_service_;
 };
 
 /// @brief Defines a smart pointer to an instance of a sender.

+ 10 - 0
src/lib/dhcp_ddns/ncr_udp.cc

@@ -359,6 +359,16 @@ NameChangeUDPSender::getSelectFd() {
     return(watch_socket_->getSelectFd());
 }
 
+bool
+NameChangeUDPSender::ioReady() {
+    if (watch_socket_) {
+        return (watch_socket_->isReady());
+    }
+
+    return (false);
+}
+
+
 
 }; // end of isc::dhcp_ddns namespace
 }; // end of isc namespace

+ 5 - 0
src/lib/dhcp_ddns/ncr_udp.h

@@ -542,6 +542,11 @@ public:
     /// @throw NcrSenderError if the sender is not in send mode,
     virtual int getSelectFd();
 
+    /// @brief Returns whether or not the sender has IO ready to process.
+    ///
+    /// @return true if the sender has at IO ready, false otherwise.
+    virtual bool ioReady();
+
 private:
     /// @brief IP address from which to send.
     isc::asiolink::IOAddress ip_address_;

+ 82 - 18
src/lib/dhcp_ddns/tests/ncr_udp_unittests.cc

@@ -361,7 +361,11 @@ TEST(NameChangeUDPSenderBasicTest, basicSendTests) {
 
     // Verify select_fd is valid and currently shows no ready to read.
     ASSERT_NE(dhcp_ddns::WatchSocket::INVALID_SOCKET, select_fd);
+
+    // Make sure select_fd does evaluates to not ready via select and
+    // that ioReady() method agrees.
     ASSERT_EQ(0, selectCheck(select_fd));
+    ASSERT_FALSE(sender.ioReady());
 
     // Iterate over a series of messages, sending each one. Since we
     // do not invoke IOService::run, then the messages should accumulate
@@ -392,18 +396,22 @@ TEST(NameChangeUDPSenderBasicTest, basicSendTests) {
     // IOService::run_one. This should complete the send of exactly one
     // message and the queue count should decrement accordingly.
     for (int i = num_msgs; i > 0; i--) {
-        // Verify that sender shows IO ready.
+        // Make sure select_fd does evaluates to ready via select and
+        // that ioReady() method agrees.
         ASSERT_TRUE(selectCheck(select_fd) > 0);
+        ASSERT_TRUE(sender.ioReady());
 
         // Execute at one ready handler.
-        io_service.run_one();
+        ASSERT_NO_THROW(sender.runReadyIO());
 
         // Verify that the queue count decrements in step with each run.
         EXPECT_EQ(i-1, sender.getQueueSize());
     }
 
-    // Verify that sender shows no IO ready.
-    EXPECT_EQ(0, selectCheck(select_fd));
+    // Make sure select_fd does evaluates to not ready via select and
+    // that ioReady() method agrees.
+    ASSERT_EQ(0, selectCheck(select_fd));
+    ASSERT_FALSE(sender.ioReady());
 
     // Verify that the queue is empty.
     EXPECT_EQ(0, sender.getQueueSize());
@@ -419,22 +427,79 @@ TEST(NameChangeUDPSenderBasicTest, basicSendTests) {
     // Verify that flushing the queue is not allowed in sending state.
     EXPECT_THROW(sender.clearSendQueue(), NcrSenderError);
 
-    // Put a message on the queue.
-    EXPECT_NO_THROW(sender.sendRequest(ncr));
-    EXPECT_EQ(1, sender.getQueueSize());
+    // Put num_msgs messages on the queue.
+    for (int i = 0; i < num_msgs; i++) {
+        ASSERT_NO_THROW(ncr = NameChangeRequest::fromJSON(valid_msgs[i]));
+        EXPECT_NO_THROW(sender.sendRequest(ncr));
+    }
+
+    // Make sure we have number of messages expected.
+    EXPECT_EQ(num_msgs, sender.getQueueSize());
 
     // Verify that we can gracefully stop sending.
     EXPECT_NO_THROW(sender.stopSending());
     EXPECT_FALSE(sender.amSending());
 
     // Verify that the queue is preserved after leaving sending state.
-    EXPECT_EQ(1, sender.getQueueSize());
+    EXPECT_EQ(num_msgs - 1, sender.getQueueSize());
 
     // Verify that flushing the queue works when not sending.
     EXPECT_NO_THROW(sender.clearSendQueue());
     EXPECT_EQ(0, sender.getQueueSize());
 }
 
+/// @brief Tests that sending gets kick-started if the queue isn't empty
+/// when startSending is called.
+TEST(NameChangeUDPSenderBasicTest, autoStart) {
+    isc::asiolink::IOAddress ip_address(TEST_ADDRESS);
+    isc::asiolink::IOService io_service;
+    SimpleSendHandler ncr_handler;
+
+    // Tests are based on a list of messages, get the count now.
+    int num_msgs = sizeof(valid_msgs)/sizeof(char*);
+
+    // Create the sender, setting the queue max equal to the number of
+    // messages we will have in the list.
+    NameChangeUDPSender sender(ip_address, SENDER_PORT, ip_address,
+                               LISTENER_PORT, FMT_JSON, ncr_handler,
+                               num_msgs, true);
+
+    // Verify that we can start sending.
+    EXPECT_NO_THROW(sender.startSending(io_service));
+    EXPECT_TRUE(sender.amSending());
+
+    // Queue up messages.
+    NameChangeRequestPtr ncr;
+    for (int i = 0; i < num_msgs; i++) {
+        ASSERT_NO_THROW(ncr = NameChangeRequest::fromJSON(valid_msgs[i]));
+        EXPECT_NO_THROW(sender.sendRequest(ncr));
+    }
+    // Make sure queue count is what we expect.
+    EXPECT_EQ(num_msgs, sender.getQueueSize());
+
+    // Stop sending.
+    ASSERT_NO_THROW(sender.stopSending());
+    ASSERT_FALSE(sender.amSending());
+
+    // We should have completed the first message only.
+    EXPECT_EQ(--num_msgs, sender.getQueueSize());
+
+    // Restart sending.
+    EXPECT_NO_THROW(sender.startSending(io_service));
+
+    // We should be able to loop through remaining messages and send them.
+    for (int i = num_msgs; i > 0; i--) {
+        // ioReady() should evaluate to true.
+        ASSERT_TRUE(sender.ioReady());
+
+        // Execute at one ready handler.
+        ASSERT_NO_THROW(sender.runReadyIO());
+    }
+
+    // Verify that the queue is empty.
+    EXPECT_EQ(0, sender.getQueueSize());
+}
+
 /// @brief Tests NameChangeUDPSender basic send  with INADDR_ANY and port 0.
 TEST(NameChangeUDPSenderBasicTest, anyAddressSend) {
     isc::asiolink::IOAddress ip_address(TEST_ADDRESS);
@@ -454,23 +519,19 @@ TEST(NameChangeUDPSenderBasicTest, anyAddressSend) {
     ASSERT_NO_THROW(sender.startSending(io_service));
     EXPECT_TRUE(sender.amSending());
 
-    // Fetch the sender's select-fd.
-    int select_fd = sender.getSelectFd();
-
     // Create and queue up a message.
     NameChangeRequestPtr ncr;
     ASSERT_NO_THROW(ncr = NameChangeRequest::fromJSON(valid_msgs[0]));
     EXPECT_NO_THROW(sender.sendRequest(ncr));
     EXPECT_EQ(1, sender.getQueueSize());
 
-    // message and the queue count should decrement accordingly.
-    // Execute at one ready handler.
-    ASSERT_TRUE(selectCheck(select_fd) > 0);
-    ASSERT_NO_THROW(io_service.run_one());
+    // Verify we have a ready IO, then execute at one ready handler.
+    ASSERT_TRUE(sender.ioReady());
+    ASSERT_NO_THROW(sender.runReadyIO());
 
     // Verify that sender shows no IO ready.
     // and that the queue is empty.
-    EXPECT_EQ(0, selectCheck(select_fd));
+    ASSERT_FALSE(sender.ioReady());
     EXPECT_EQ(0, sender.getQueueSize());
 }
 
@@ -514,6 +575,9 @@ TEST(NameChangeSender, assumeQueue) {
     // Take sender1 out of send mode.
     ASSERT_NO_THROW(sender1.stopSending());
     ASSERT_FALSE(sender1.amSending());
+    // Stopping should have completed the first message.
+    --num_msgs;
+    EXPECT_EQ(num_msgs, sender1.getQueueSize());
 
     // Transfer should succeed. Verify sender1 has none,
     // and sender2 has num_msgs queued.
@@ -719,7 +783,7 @@ TEST(NameChangeUDPSenderBasicTest, watchClosedAfterSendRequest) {
     // Run one handler. This should execute the send completion handler
     // after sending the first message.  Duing completion handling, we will
     // attempt to queue the second message which should fail.
-    ASSERT_NO_THROW(io_service.run_one());
+    ASSERT_NO_THROW(sender.runReadyIO());
 
     // Verify handler got called twice. First request should have be sent
     // without error, second call should have failed to send due to watch
@@ -767,7 +831,7 @@ TEST(NameChangeUDPSenderBasicTest, watchSocketBadRead) {
     // after sending the message.  Duing completion handling clearing the
     // watch socket should fail, which will close the socket, but not
     // result in a throw.
-    ASSERT_NO_THROW(io_service.run_one());
+    ASSERT_NO_THROW(sender.runReadyIO());
 
     // Verify handler got called twice. First request should have be sent
     // without error, second call should have failed to send due to watch

+ 6 - 1
src/lib/dhcp_ddns/tests/watch_socket_unittests.cc

@@ -11,7 +11,7 @@
 // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
 // OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 // PERFORMANCE OF THIS SOFTWARE.
-
+#include <config.h>
 #include <dhcp_ddns/watch_socket.h>
 #include <test_utils.h>
 
@@ -20,6 +20,11 @@
 #include <sys/select.h>
 #include <sys/ioctl.h>
 
+#ifdef HAVE_SYS_FILIO_H
+// FIONREAD is here on Solaris
+#include <sys/filio.h>
+#endif
+
 using namespace std;
 using namespace isc;
 using namespace isc::dhcp_ddns;

+ 2 - 1
src/lib/dhcpsrv/Makefile.am

@@ -39,7 +39,8 @@ libb10_dhcpsrv_la_SOURCES  =
 libb10_dhcpsrv_la_SOURCES += addr_utilities.cc addr_utilities.h
 libb10_dhcpsrv_la_SOURCES += alloc_engine.cc alloc_engine.h
 libb10_dhcpsrv_la_SOURCES += callout_handle_store.h
-libb10_dhcpsrv_la_SOURCES += d2_client.cc d2_client.h
+libb10_dhcpsrv_la_SOURCES += d2_client_cfg.cc d2_client_cfg.h
+libb10_dhcpsrv_la_SOURCES += d2_client_mgr.cc d2_client_mgr.h
 libb10_dhcpsrv_la_SOURCES += dbaccess_parser.cc dbaccess_parser.h
 libb10_dhcpsrv_la_SOURCES += dhcpsrv_log.cc dhcpsrv_log.h
 libb10_dhcpsrv_la_SOURCES += cfgmgr.cc cfgmgr.h

+ 23 - 19
src/lib/dhcpsrv/cfgmgr.cc

@@ -154,22 +154,8 @@ CfgMgr::getSubnet6(const std::string& iface,
 
 Subnet6Ptr
 CfgMgr::getSubnet6(const isc::asiolink::IOAddress& hint,
-                   const isc::dhcp::ClientClasses& classes) {
-
-    // If there's only one subnet configured, let's just use it
-    // The idea is to keep small deployments easy. In a small network - one
-    // router that also runs DHCPv6 server. User specifies a single pool and
-    // expects it to just work. Without this, the server would complain that it
-    // doesn't have IP address on its interfaces that matches that
-    // configuration. Such requirement makes sense in IPv4, but not in IPv6.
-    // The server does not need to have a global address (using just link-local
-    // is ok for DHCPv6 server) from the pool it serves.
-    if ((subnets6_.size() == 1) && hint.isV6LinkLocal()) {
-        LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE,
-                  DHCPSRV_CFGMGR_ONLY_SUBNET6)
-                  .arg(subnets6_[0]->toText()).arg(hint.toText());
-        return (subnets6_[0]);
-    }
+                   const isc::dhcp::ClientClasses& classes,
+                   const bool relay) {
 
     // If there is more than one, we need to choose the proper one
     for (Subnet6Collection::iterator subnet = subnets6_.begin();
@@ -180,9 +166,17 @@ CfgMgr::getSubnet6(const isc::asiolink::IOAddress& hint,
             continue;
         }
 
-        if ((*subnet)->inRange(hint)) {
+        // If the hint is a relay address, and there is relay info specified
+        // for this subnet and those two match, then use this subnet.
+        if (relay && ((*subnet)->getRelayInfo().addr_ == hint) ) {
             LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE,
-                      DHCPSRV_CFGMGR_SUBNET6)
+                      DHCPSRV_CFGMGR_SUBNET6_RELAY)
+                .arg((*subnet)->toText()).arg(hint.toText());
+            return (*subnet);
+        }
+
+        if ((*subnet)->inRange(hint)) {
+            LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE, DHCPSRV_CFGMGR_SUBNET6)
                       .arg((*subnet)->toText()).arg(hint.toText());
             return (*subnet);
         }
@@ -232,7 +226,8 @@ void CfgMgr::addSubnet6(const Subnet6Ptr& subnet) {
 
 Subnet4Ptr
 CfgMgr::getSubnet4(const isc::asiolink::IOAddress& hint,
-                   const isc::dhcp::ClientClasses& classes) const {
+                   const isc::dhcp::ClientClasses& classes,
+                   bool relay) const {
     // Iterate over existing subnets to find a suitable one for the
     // given address.
     for (Subnet4Collection::const_iterator subnet = subnets4_.begin();
@@ -243,6 +238,15 @@ CfgMgr::getSubnet4(const isc::asiolink::IOAddress& hint,
             continue;
         }
 
+        // If the hint is a relay address, and there is relay info specified
+        // for this subnet and those two match, then use this subnet.
+        if (relay && ((*subnet)->getRelayInfo().addr_ == hint) ) {
+            LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE,
+                      DHCPSRV_CFGMGR_SUBNET4_RELAY)
+                .arg((*subnet)->toText()).arg(hint.toText());
+            return (*subnet);
+        }
+
         // Let's check if the client belongs to the given subnet
         if ((*subnet)->inRange(hint)) {
             LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE,

+ 21 - 3
src/lib/dhcpsrv/cfgmgr.h

@@ -20,7 +20,7 @@
 #include <dhcp/option_definition.h>
 #include <dhcp/option_space.h>
 #include <dhcp/classify.h>
-#include <dhcpsrv/d2_client.h>
+#include <dhcpsrv/d2_client_mgr.h>
 #include <dhcpsrv/option_space_container.h>
 #include <dhcpsrv/pool.h>
 #include <dhcpsrv/subnet.h>
@@ -169,12 +169,23 @@ public:
     /// If there are any classes specified in a subnet, that subnet
     /// will be selected only if the client belongs to appropriate class.
     ///
+    /// @note The client classification is checked before any relay
+    /// information checks are conducted.
+    ///
+    /// If relay is true then relay info overrides (i.e. value the sysadmin
+    /// can configure in Dhcp6/subnet6[X]/relay/ip-address) can be used.
+    /// That is applicable only for relays. Those overrides must not be used
+    /// for client address or for client hints. They are for link-addr field
+    /// in the RELAY_FORW message only.
+    ///
     /// @param hint an address that belongs to a searched subnet
     /// @param classes classes the client belongs to
+    /// @param relay true if address specified in hint is a relay
     ///
     /// @return a subnet object (or NULL if no suitable match was fount)
     Subnet6Ptr getSubnet6(const isc::asiolink::IOAddress& hint,
-                          const isc::dhcp::ClientClasses& classes);
+                          const isc::dhcp::ClientClasses& classes,
+                          const bool relay = false);
 
     /// @brief get IPv6 subnet by interface name
     ///
@@ -262,12 +273,19 @@ public:
     /// If there are any classes specified in a subnet, that subnet
     /// will be selected only if the client belongs to appropriate class.
     ///
+    /// If relay is true then relay info overrides (i.e. value the sysadmin
+    /// can configure in Dhcp4/subnet4[X]/relay/ip-address) can be used.
+    /// That is true only for relays. Those overrides must not be used
+    /// for client address or for client hints. They are for giaddr only.
+    ///
     /// @param hint an address that belongs to a searched subnet
     /// @param classes classes the client belongs to
+    /// @param relay true if address specified in hint is a relay
     ///
     /// @return a subnet object
     Subnet4Ptr getSubnet4(const isc::asiolink::IOAddress& hint,
-                          const isc::dhcp::ClientClasses& classes) const;
+                          const isc::dhcp::ClientClasses& classes,
+                          bool relay = false) const;
 
     /// @brief Returns a subnet for the specified local interface.
     ///

+ 146 - 0
src/lib/dhcpsrv/d2_client_cfg.cc

@@ -0,0 +1,146 @@
+// Copyright (C) 2013-2014 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_ddns/ncr_udp.h>
+#include <dhcpsrv/d2_client_cfg.h>
+#include <dhcpsrv/dhcpsrv_log.h>
+
+#include <string>
+
+using namespace std;
+
+namespace isc {
+namespace dhcp {
+
+D2ClientConfig::D2ClientConfig(const  bool enable_updates,
+                               const isc::asiolink::IOAddress& server_ip,
+                               const size_t server_port,
+                               const dhcp_ddns::
+                                     NameChangeProtocol& ncr_protocol,
+                               const dhcp_ddns::
+                                     NameChangeFormat& ncr_format,
+                               const bool always_include_fqdn,
+                               const bool override_no_update,
+                               const bool override_client_update,
+                               const bool replace_client_name,
+                               const std::string& generated_prefix,
+                               const std::string& qualifying_suffix)
+    : enable_updates_(enable_updates),
+    server_ip_(server_ip),
+    server_port_(server_port),
+    ncr_protocol_(ncr_protocol),
+    ncr_format_(ncr_format),
+    always_include_fqdn_(always_include_fqdn),
+    override_no_update_(override_no_update),
+    override_client_update_(override_client_update),
+    replace_client_name_(replace_client_name),
+    generated_prefix_(generated_prefix),
+    qualifying_suffix_(qualifying_suffix) {
+    validateContents();
+}
+
+D2ClientConfig::D2ClientConfig()
+    : enable_updates_(false),
+      server_ip_(isc::asiolink::IOAddress("0.0.0.0")),
+      server_port_(0),
+      ncr_protocol_(dhcp_ddns::NCR_UDP),
+      ncr_format_(dhcp_ddns::FMT_JSON),
+      always_include_fqdn_(false),
+      override_no_update_(false),
+      override_client_update_(false),
+      replace_client_name_(false),
+      generated_prefix_("myhost"),
+      qualifying_suffix_("example.com") {
+    validateContents();
+}
+
+D2ClientConfig::~D2ClientConfig(){};
+
+void
+D2ClientConfig::enableUpdates(bool enable) {
+    enable_updates_ = enable;
+}
+
+void
+D2ClientConfig::validateContents() {
+    if (ncr_format_ != dhcp_ddns::FMT_JSON) {
+        isc_throw(D2ClientError, "D2ClientConfig: NCR Format:"
+                    << dhcp_ddns::ncrFormatToString(ncr_format_)
+                    << " is not yet supported");
+    }
+
+    if (ncr_protocol_ != dhcp_ddns::NCR_UDP) {
+        isc_throw(D2ClientError, "D2ClientConfig: NCR Protocol:"
+                    << dhcp_ddns::ncrProtocolToString(ncr_protocol_)
+                    << " is not yet supported");
+    }
+
+    /// @todo perhaps more validation we should do yet?
+    /// Are there any invalid combinations of options we need to test against?
+}
+
+bool
+D2ClientConfig::operator == (const D2ClientConfig& other) const {
+    return ((enable_updates_ == other.enable_updates_) &&
+            (server_ip_ == other.server_ip_) &&
+            (server_port_ == other.server_port_) &&
+            (ncr_protocol_ == other.ncr_protocol_) &&
+            (ncr_format_ == other.ncr_format_) &&
+            (always_include_fqdn_ == other.always_include_fqdn_) &&
+            (override_no_update_ == other.override_no_update_) &&
+            (override_client_update_ == other.override_client_update_) &&
+            (replace_client_name_ == other.replace_client_name_) &&
+            (generated_prefix_ == other.generated_prefix_) &&
+            (qualifying_suffix_ == other.qualifying_suffix_));
+}
+
+bool
+D2ClientConfig::operator != (const D2ClientConfig& other) const {
+    return (!(*this == other));
+}
+
+std::string
+D2ClientConfig::toText() const {
+    std::ostringstream stream;
+
+    stream << "enable_updates: " << (enable_updates_ ? "yes" : "no");
+    if (enable_updates_) {
+        stream << ", server_ip: " << server_ip_.toText()
+               << ", server_port: " << server_port_
+               << ", ncr_protocol: " << ncr_protocol_
+               << ", ncr_format: " << ncr_format_
+               << ", always_include_fqdn: " << (always_include_fqdn_ ?
+                                                "yes" : "no")
+               << ", override_no_update: " << (override_no_update_ ?
+                                               "yes" : "no")
+               << ", override_client_update: " << (override_client_update_ ?
+                                                   "yes" : "no")
+               << ", replace_client_name: " << (replace_client_name_ ?
+                                                "yes" : "no")
+               << ", generated_prefix: [" << generated_prefix_ << "]"
+               << ", qualifying_suffix: [" << qualifying_suffix_ << "]";
+    }
+
+    return (stream.str());
+}
+
+std::ostream&
+operator<<(std::ostream& os, const D2ClientConfig& config) {
+    os << config.toText();
+    return (os);
+}
+
+};  // namespace dhcp
+
+};  // namespace isc

+ 226 - 0
src/lib/dhcpsrv/d2_client_cfg.h

@@ -0,0 +1,226 @@
+// Copyright (C) 2013-2014 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 D2_CLIENT_CFG_H
+#define D2_CLIENT_CFG_H
+
+/// @file d2_client_cfg.h Defines the D2ClientConfig class.
+/// This file defines the classes Kea uses to manage configuration needed to
+/// act as a client of the b10-dhcp-ddns module (aka D2).
+///
+#include <asiolink/io_address.h>
+#include <dhcp_ddns/ncr_io.h>
+#include <exceptions/exceptions.h>
+
+#include <boost/shared_ptr.hpp>
+
+#include <stdint.h>
+#include <string>
+#include <vector>
+
+namespace isc {
+namespace dhcp {
+
+
+/// An exception that is thrown if an error occurs while configuring
+/// the D2 DHCP DDNS client.
+class D2ClientError : public isc::Exception {
+public:
+
+    /// @brief constructor
+    ///
+    /// @param file name of the file, where exception occurred
+    /// @param line line of the file, where exception occurred
+    /// @param what text description of the issue that caused exception
+    D2ClientError(const char* file, size_t line, const char* what)
+        : isc::Exception(file, line, what) {}
+};
+
+/// @brief Acts as a storage vault for D2 client configuration
+///
+/// A simple container class for storing and retrieving the configuration
+/// parameters associated with DHCP-DDNS and acting as a client of D2.
+/// Instances of this class may be constructed through configuration parsing.
+///
+class D2ClientConfig {
+public:
+    /// @brief Constructor
+    ///
+    /// @param enable_updates Enables DHCP-DDNS updates
+    /// @param server_ip IP address of the b10-dhcp-ddns server (IPv4 or IPv6)
+    /// @param server_port IP port of the b10-dhcp-ddns server
+    /// @param ncr_protocol Socket protocol to use with b10-dhcp-ddns
+    /// Currently only UDP is supported.
+    /// @param ncr_format Format of the b10-dhcp-ddns requests.
+    /// Currently only JSON format is supported.
+    /// @param always_include_fqdn Enables always including the FQDN option in
+    /// DHCP responses.
+    /// @param override_no_update Enables updates, even if clients request no
+    /// updates.
+    /// @param override_client_update Perform updates, even if client requested
+    /// delegation.
+    /// @param replace_client_name enables replacement of the domain-name
+    /// supplied by the client with a generated name.
+    /// @param generated_prefix Prefix to use when generating domain-names.
+    /// @param  qualifying_suffix Suffix to use to qualify partial domain-names.
+    ///
+    /// @throw D2ClientError if given an invalid protocol or format.
+    D2ClientConfig(const bool enable_updates,
+                   const isc::asiolink::IOAddress& server_ip,
+                   const size_t server_port,
+                   const dhcp_ddns::NameChangeProtocol& ncr_protocol,
+                   const dhcp_ddns::NameChangeFormat& ncr_format,
+                   const bool always_include_fqdn,
+                   const bool override_no_update,
+                   const bool override_client_update,
+                   const bool replace_client_name,
+                   const std::string& generated_prefix,
+                   const std::string& qualifying_suffix);
+
+    /// @brief Default constructor
+    /// The default constructor creates an instance that has updates disabled.
+    D2ClientConfig();
+
+    /// @brief Destructor
+    virtual ~D2ClientConfig();
+
+    /// @brief Return whether or not DHCP-DDNS updating is enabled.
+    bool getEnableUpdates() const {
+        return(enable_updates_);
+    }
+
+    /// @brief Return the IP address of b10-dhcp-ddns (IPv4 or IPv6).
+    const isc::asiolink::IOAddress& getServerIp() const {
+        return(server_ip_);
+    }
+
+    /// @brief Return the IP port of b10-dhcp-ddns.
+    size_t getServerPort() const {
+        return(server_port_);
+    }
+
+    /// @brief Return the socket protocol to use with b10-dhcp-ddns.
+    const dhcp_ddns::NameChangeProtocol& getNcrProtocol() const {
+         return(ncr_protocol_);
+    }
+
+    /// @brief Return the b10-dhcp-ddns request format.
+    const dhcp_ddns::NameChangeFormat& getNcrFormat() const {
+        return(ncr_format_);
+    }
+
+    /// @brief Return whether or not FQDN is always included in DHCP responses.
+    bool getAlwaysIncludeFqdn() const {
+        return(always_include_fqdn_);
+    }
+
+    /// @brief Return if updates are done even if clients request no updates.
+    bool getOverrideNoUpdate() const {
+        return(override_no_update_);
+    }
+
+    /// @brief Return if updates are done even when clients request delegation.
+    bool getOverrideClientUpdate() const {
+        return(override_client_update_);
+    }
+
+    /// @brief Return whether or not client's domain-name is always replaced.
+    bool getReplaceClientName() const {
+        return(replace_client_name_);
+    }
+
+    /// @brief Return the prefix to use when generating domain-names.
+    const std::string& getGeneratedPrefix() const {
+        return(generated_prefix_);
+    }
+
+    /// @brief Return the suffix to use to qualify partial domain-names.
+    const std::string& getQualifyingSuffix() const {
+        return(qualifying_suffix_);
+    }
+
+    /// @brief Compares two D2ClientConfigs for equality
+    bool operator == (const D2ClientConfig& other) const;
+
+    /// @brief Compares two D2ClientConfigs for inequality
+    bool operator != (const D2ClientConfig& other) const;
+
+    /// @brief Generates a string representation of the class contents.
+    std::string toText() const;
+
+    /// @brief Sets enable-updates flag to the given value.
+    ///
+    /// This is the only value that may be altered outside the constructor
+    /// as it may be desirable to toggle it off and on when dealing with
+    /// D2 IO errors.
+    ///
+    /// @param enable boolean value to assign to the enable-updates flag
+    void enableUpdates(bool enable);
+
+protected:
+    /// @brief Validates member values.
+    ///
+    /// Method is used by the constructor to validate member contents.
+    ///
+    /// @throw D2ClientError if given an invalid protocol or format.
+    virtual void validateContents();
+
+private:
+    /// @brief Indicates whether or not DHCP DDNS updating is enabled.
+    bool enable_updates_;
+
+    /// @brief IP address of the b10-dhcp-ddns server (IPv4 or IPv6).
+    isc::asiolink::IOAddress server_ip_;
+
+    /// @brief IP port of the b10-dhcp-ddns server.
+    size_t server_port_;
+
+    /// @brief The socket protocol to use with b10-dhcp-ddns.
+    /// Currently only UDP is supported.
+    dhcp_ddns::NameChangeProtocol ncr_protocol_;
+
+    /// @brief Format of the b10-dhcp-ddns requests.
+    /// Currently only JSON format is supported.
+    dhcp_ddns::NameChangeFormat ncr_format_;
+
+    /// @brief Should Kea always include the FQDN option in its response.
+    bool always_include_fqdn_;
+
+    /// @brief Should Kea perform updates, even if client requested no updates.
+    /// Overrides the client request for no updates via the N flag.
+    bool override_no_update_;
+
+    /// @brief Should Kea perform updates, even if client requested delegation.
+    bool override_client_update_;
+
+    /// @brief Should Kea replace the domain-name supplied by the client.
+    bool replace_client_name_;
+
+    /// @brief Prefix Kea should use when generating domain-names.
+    std::string generated_prefix_;
+
+    /// @brief Suffix Kea should use when to qualify partial domain-names.
+    std::string qualifying_suffix_;
+};
+
+std::ostream&
+operator<<(std::ostream& os, const D2ClientConfig& config);
+
+/// @brief Defines a pointer for D2ClientConfig instances.
+typedef boost::shared_ptr<D2ClientConfig> D2ClientConfigPtr;
+
+} // namespace isc
+} // namespace dhcp
+
+#endif

+ 95 - 165
src/lib/dhcpsrv/d2_client.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2013-2014 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014 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
@@ -12,10 +12,13 @@
 // OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 // PERFORMANCE OF THIS SOFTWARE.
 
+#include <dhcp/iface_mgr.h>
 #include <dhcp_ddns/ncr_udp.h>
-#include <dhcpsrv/d2_client.h>
+#include <dhcpsrv/d2_client_mgr.h>
 #include <dhcpsrv/dhcpsrv_log.h>
 
+#include <boost/bind.hpp>
+
 #include <string>
 
 using namespace std;
@@ -23,133 +26,26 @@ using namespace std;
 namespace isc {
 namespace dhcp {
 
-//***************************** D2ClientConfig ********************************
-
-D2ClientConfig::D2ClientConfig(const  bool enable_updates,
-                               const isc::asiolink::IOAddress& server_ip,
-                               const size_t server_port,
-                               const dhcp_ddns::
-                                     NameChangeProtocol& ncr_protocol,
-                               const dhcp_ddns::
-                                     NameChangeFormat& ncr_format,
-                               const bool always_include_fqdn,
-                               const bool override_no_update,
-                               const bool override_client_update,
-                               const bool replace_client_name,
-                               const std::string& generated_prefix,
-                               const std::string& qualifying_suffix)
-    : enable_updates_(enable_updates),
-    server_ip_(server_ip),
-    server_port_(server_port),
-    ncr_protocol_(ncr_protocol),
-    ncr_format_(ncr_format),
-    always_include_fqdn_(always_include_fqdn),
-    override_no_update_(override_no_update),
-    override_client_update_(override_client_update),
-    replace_client_name_(replace_client_name),
-    generated_prefix_(generated_prefix),
-    qualifying_suffix_(qualifying_suffix) {
-    validateContents();
-}
-
-D2ClientConfig::D2ClientConfig()
-    : enable_updates_(false),
-      server_ip_(isc::asiolink::IOAddress("0.0.0.0")),
-      server_port_(0),
-      ncr_protocol_(dhcp_ddns::NCR_UDP),
-      ncr_format_(dhcp_ddns::FMT_JSON),
-      always_include_fqdn_(false),
-      override_no_update_(false),
-      override_client_update_(false),
-      replace_client_name_(false),
-      generated_prefix_("myhost"),
-      qualifying_suffix_("example.com") {
-    validateContents();
-}
-
-D2ClientConfig::~D2ClientConfig(){};
-
-void
-D2ClientConfig::validateContents() {
-    if (ncr_format_ != dhcp_ddns::FMT_JSON) {
-        isc_throw(D2ClientError, "D2ClientConfig: NCR Format:"
-                    << dhcp_ddns::ncrFormatToString(ncr_format_)
-                    << " is not yet supported");
-    }
-
-    if (ncr_protocol_ != dhcp_ddns::NCR_UDP) {
-        isc_throw(D2ClientError, "D2ClientConfig: NCR Protocol:"
-                    << dhcp_ddns::ncrProtocolToString(ncr_protocol_)
-                    << " is not yet supported");
-    }
-
-    /// @todo perhaps more validation we should do yet?
-    /// Are there any invalid combinations of options we need to test against?
-}
-
-bool
-D2ClientConfig::operator == (const D2ClientConfig& other) const {
-    return ((enable_updates_ == other.enable_updates_) &&
-            (server_ip_ == other.server_ip_) &&
-            (server_port_ == other.server_port_) &&
-            (ncr_protocol_ == other.ncr_protocol_) &&
-            (ncr_format_ == other.ncr_format_) &&
-            (always_include_fqdn_ == other.always_include_fqdn_) &&
-            (override_no_update_ == other.override_no_update_) &&
-            (override_client_update_ == other.override_client_update_) &&
-            (replace_client_name_ == other.replace_client_name_) &&
-            (generated_prefix_ == other.generated_prefix_) &&
-            (qualifying_suffix_ == other.qualifying_suffix_));
-}
-
-bool
-D2ClientConfig::operator != (const D2ClientConfig& other) const {
-    return (!(*this == other));
-}
-
-std::string
-D2ClientConfig::toText() const {
-    std::ostringstream stream;
-
-    stream << "enable_updates: " << (enable_updates_ ? "yes" : "no");
-    if (enable_updates_) {
-        stream << ", server_ip: " << server_ip_.toText()
-               << ", server_port: " << server_port_
-               << ", ncr_protocol: " << ncr_protocol_
-               << ", ncr_format: " << ncr_format_
-               << ", always_include_fqdn: " << (always_include_fqdn_ ?
-                                                "yes" : "no")
-               << ", override_no_update: " << (override_no_update_ ?
-                                               "yes" : "no")
-               << ", override_client_update: " << (override_client_update_ ?
-                                                   "yes" : "no")
-               << ", replace_client_name: " << (replace_client_name_ ?
-                                                "yes" : "no")
-               << ", generated_prefix: [" << generated_prefix_ << "]"
-               << ", qualifying_suffix: [" << qualifying_suffix_ << "]";
-    }
-
-    return (stream.str());
-}
-
-std::ostream&
-operator<<(std::ostream& os, const D2ClientConfig& config) {
-    os << config.toText();
-    return (os);
-}
-
-
-//******************************** D2ClientMgr ********************************
-
-
 D2ClientMgr::D2ClientMgr() : d2_client_config_(new D2ClientConfig()),
-    name_change_sender_(), private_io_service_(), sender_io_service_(NULL) {
+    name_change_sender_(), private_io_service_(),
+    registered_select_fd_(dhcp_ddns::WatchSocket::INVALID_SOCKET) {
     // Default constructor initializes with a disabled configuration.
 }
 
 D2ClientMgr::~D2ClientMgr(){
-    if (name_change_sender_) {
-        stopSender();
+    stopSender();
+}
+
+void
+D2ClientMgr::suspendUpdates() {
+    if (ddnsEnabled()) {
+        /// @todo For now we will disable updates and stop sending.
+        /// This at least provides a means to shut it off if there are errors.
+        LOG_WARN(dhcpsrv_logger, DHCPSRV_DHCP_DDNS_SUSPEND_UPDATES);
+        d2_client_config_->enableUpdates(false);
+        if (name_change_sender_) {
+            stopSender();
+        }
     }
 }
 
@@ -162,9 +58,11 @@ D2ClientMgr::setD2ClientConfig(D2ClientConfigPtr& new_config) {
 
     // Don't do anything unless configuration values are actually different.
     if (*d2_client_config_ != *new_config) {
+        // Make sure we stop sending first.
+        stopSender();
         if (!new_config->getEnableUpdates()) {
-            // Updating has been turned off, destroy current sender.
-            // Any queued requests are tossed.
+            // Updating has been turned off.
+            // Destroy current sender (any queued requests are tossed).
             name_change_sender_.reset();
         } else {
             dhcp_ddns::NameChangeSenderPtr new_sender;
@@ -201,7 +99,6 @@ D2ClientMgr::setD2ClientConfig(D2ClientConfigPtr& new_config) {
             /// then the queued contents might now be invalid.  There is
             /// no way to regenerate them if they are wrong.
             if (name_change_sender_) {
-                name_change_sender_->stopSending();
                 new_sender->assumeQueue(*name_change_sender_);
             }
 
@@ -311,15 +208,25 @@ D2ClientMgr::qualifyName(const std::string& partial_name) const {
 
 void
 D2ClientMgr::startSender(D2ClientErrorHandler error_handler) {
+    if (amSending()) {
+        return;
+    }
+
     // Create a our own service instance when we are not being multiplexed
     // into an external service..
     private_io_service_.reset(new asiolink::IOService());
     startSender(error_handler, *private_io_service_);
+    LOG_INFO(dhcpsrv_logger, DHCPSRV_DHCP_DDNS_SENDER_STARTED)
+             .arg(d2_client_config_->toText());
 }
 
 void
 D2ClientMgr::startSender(D2ClientErrorHandler error_handler,
                          isc::asiolink::IOService& io_service) {
+    if (amSending()) {
+        return;
+    }
+
     if (!name_change_sender_)  {
         isc_throw(D2ClientError, "D2ClientMgr::startSender sender is null");
     }
@@ -331,14 +238,16 @@ D2ClientMgr::startSender(D2ClientErrorHandler error_handler,
     // Set the error handler.
     client_error_handler_ = error_handler;
 
-    // Remember the io service being used.
-    sender_io_service_ = &io_service;
-
     // Start the sender on the given service.
-    name_change_sender_->startSending(*sender_io_service_);
-
-    /// @todo need to register sender's select-fd with IfaceMgr once 3315 is
-    /// done.
+    name_change_sender_->startSending(io_service);
+
+    // Register sender's select-fd with IfaceMgr.
+    // We need to remember the fd that is registered so we can unregister later.
+    // IO error handling in the sender may alter its select-fd.
+    registered_select_fd_ = name_change_sender_->getSelectFd();
+    IfaceMgr::instance().addExternalSocket(registered_select_fd_,
+                                           boost::bind(&D2ClientMgr::runReadyIO,
+                                                       this));
 }
 
 bool
@@ -348,23 +257,51 @@ D2ClientMgr::amSending() const {
 
 void
 D2ClientMgr::stopSender() {
-    if (!name_change_sender_)  {
-        isc_throw(D2ClientError, "D2ClientMgr::stopSender sender is null");
+    /// Unregister sender's select-fd.
+    if (registered_select_fd_ != dhcp_ddns::WatchSocket::INVALID_SOCKET) {
+        IfaceMgr::instance().deleteExternalSocket(registered_select_fd_);
+        registered_select_fd_ = dhcp_ddns::WatchSocket::INVALID_SOCKET;
     }
 
-    /// @todo need to unregister sender's select-fd with IfaceMgr once 3315 is
-    /// done.
-
-    name_change_sender_->stopSending();
+    // If its not null, call stop.
+    if (amSending()) {
+        name_change_sender_->stopSending();
+        LOG_INFO(dhcpsrv_logger, DHCPSRV_DHCP_DDNS_SENDER_STOPPED);
+    }
 }
 
 void
 D2ClientMgr::sendRequest(dhcp_ddns::NameChangeRequestPtr& ncr) {
-    if (!name_change_sender_) {
-        isc_throw(D2ClientError, "D2ClientMgr::sendRequest sender is null");
+    if (!amSending()) {
+        // This is programmatic error so bust them for it.
+        isc_throw(D2ClientError, "D2ClientMgr::sendRequest not in send mode");
+    }
+
+    try {
+        name_change_sender_->sendRequest(ncr);
+    } catch (const std::exception& ex) {
+        LOG_ERROR(dhcpsrv_logger, DHCPSRV_DHCP_DDNS_NCR_REJECTED)
+                  .arg(ex.what()).arg((ncr ? ncr->toText() : " NULL "));
+        invokeClientErrorHandler(dhcp_ddns::NameChangeSender::ERROR, ncr);
     }
+}
 
-    name_change_sender_->sendRequest(ncr);
+void
+D2ClientMgr::invokeClientErrorHandler(const dhcp_ddns::NameChangeSender::
+                                      Result result,
+                                      dhcp_ddns::NameChangeRequestPtr& ncr) {
+    // Handler is mandatory to enter send mode but test it just to be safe.
+    if (!client_error_handler_) {
+        LOG_ERROR(dhcpsrv_logger, DHCPSRV_DHCP_DDNS_HANDLER_NULL);
+    } else {
+        // Handler is not supposed to throw, but catch just in case.
+        try {
+            (client_error_handler_)(result, ncr);
+        } catch (const std::exception& ex) {
+            LOG_ERROR(dhcpsrv_logger, DHCPSRV_DHCP_DDNS_ERROR_EXCEPTION)
+                      .arg(ex.what());
+        }
+    }
 }
 
 size_t
@@ -376,6 +313,16 @@ D2ClientMgr::getQueueSize() const {
     return(name_change_sender_->getQueueSize());
 }
 
+size_t
+D2ClientMgr::getQueueMaxSize() const {
+    if (!name_change_sender_) {
+        isc_throw(D2ClientError, "D2ClientMgr::getQueueMaxSize sender is null");
+    }
+
+    return(name_change_sender_->getQueueMaxSize());
+}
+
+
 
 const dhcp_ddns::NameChangeRequestPtr&
 D2ClientMgr::peekAt(const size_t index) const {
@@ -402,21 +349,8 @@ D2ClientMgr::operator()(const dhcp_ddns::NameChangeSender::Result result,
         LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
                   DHCPSRV_DHCP_DDNS_NCR_SENT).arg(ncr->toText());
     } else {
-        // Handler is mandatory but test it just to be safe.
-        /// @todo Until we have a better feel for how errors need to be
-        /// handled we farm it out to the application layer.
-        if (client_error_handler_) {
-            // Handler is not supposed to throw, but catch just in case.
-            try {
-                (client_error_handler_)(result, ncr);
-            } catch (const std::exception& ex) {
-                LOG_ERROR(dhcpsrv_logger, DHCPSRV_DHCP_DDNS_ERROR_EXCEPTION)
-                          .arg(ex.what());
-            }
-        } else {
-            LOG_ERROR(dhcpsrv_logger, DHCPSRV_DHCP_DDNS_HANDLER_NULL);
-        }
-   }
+        invokeClientErrorHandler(result, ncr);
+    }
 }
 
 int
@@ -431,17 +365,13 @@ D2ClientMgr::getSelectFd() {
 
 void
 D2ClientMgr::runReadyIO() {
-    if (!sender_io_service_) {
+    if (!name_change_sender_) {
         // This should never happen.
         isc_throw(D2ClientError, "D2ClientMgr::runReadyIO"
-                  " sender io service is null");
+                  " name_change_sender is null");
     }
 
-    // We shouldn't be here if IO isn't ready to execute.
-    // By running poll we're gauranteed not to hang.
-    /// @todo Trac# 3325 requests that asiolink::IOService provide a
-    /// wrapper for poll().
-    sender_io_service_->get_io_service().poll();
+    name_change_sender_->runReadyIO();
 }
 
 };  // namespace dhcp

+ 67 - 206
src/lib/dhcpsrv/d2_client.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2013-2014 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014 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
@@ -12,18 +12,20 @@
 // OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 // PERFORMANCE OF THIS SOFTWARE.
 
-#ifndef D2_CLIENT_H
-#define D2_CLIENT_H
+#ifndef D2_CLIENT_MGR_H
+#define D2_CLIENT_MGR_H
 
-/// @file d2_client.h Defines the D2ClientConfig and D2ClientMgr classes.
-/// This file defines the classes Kea uses to act as a client of the b10-
-/// dhcp-ddns module (aka D2).
+/// @file d2_client_mgr.h Defines the D2ClientMgr class.
+/// This file defines the class Kea uses to act as a client of the
+/// b10-dhcp-ddns module (aka D2).
 ///
 #include <asiolink/io_address.h>
 #include <dhcp_ddns/ncr_io.h>
+#include <dhcpsrv/d2_client_cfg.h>
 #include <exceptions/exceptions.h>
 
 #include <boost/shared_ptr.hpp>
+#include <boost/noncopyable.hpp>
 
 #include <stdint.h>
 #include <string>
@@ -32,185 +34,6 @@
 namespace isc {
 namespace dhcp {
 
-
-/// An exception that is thrown if an error occurs while configuring
-/// the D2 DHCP DDNS client.
-class D2ClientError : public isc::Exception {
-public:
-
-    /// @brief constructor
-    ///
-    /// @param file name of the file, where exception occurred
-    /// @param line line of the file, where exception occurred
-    /// @param what text description of the issue that caused exception
-    D2ClientError(const char* file, size_t line, const char* what)
-        : isc::Exception(file, line, what) {}
-};
-
-/// @brief Acts as a storage vault for D2 client configuration
-///
-/// A simple container class for storing and retrieving the configuration
-/// parameters associated with DHCP-DDNS and acting as a client of D2.
-/// Instances of this class may be constructed through configuration parsing.
-///
-class D2ClientConfig {
-public:
-    /// @brief Constructor
-    ///
-    /// @param enable_updates Enables DHCP-DDNS updates
-    /// @param server_ip IP address of the b10-dhcp-ddns server (IPv4 or IPv6)
-    /// @param server_port IP port of the b10-dhcp-ddns server
-    /// @param ncr_protocol Socket protocol to use with b10-dhcp-ddns
-    /// Currently only UDP is supported.
-    /// @param ncr_format Format of the b10-dhcp-ddns requests.
-    /// Currently only JSON format is supported.
-    /// @param always_include_fqdn Enables always including the FQDN option in
-    /// DHCP responses.
-    /// @param override_no_update Enables updates, even if clients request no
-    /// updates.
-    /// @param override_client_update Perform updates, even if client requested
-    /// delegation.
-    /// @param replace_client_name enables replacement of the domain-name
-    /// supplied by the client with a generated name.
-    /// @param generated_prefix Prefix to use when generating domain-names.
-    /// @param  qualifying_suffix Suffix to use to qualify partial domain-names.
-    ///
-    /// @throw D2ClientError if given an invalid protocol or format.
-    D2ClientConfig(const bool enable_updates,
-                   const isc::asiolink::IOAddress& server_ip,
-                   const size_t server_port,
-                   const dhcp_ddns::NameChangeProtocol& ncr_protocol,
-                   const dhcp_ddns::NameChangeFormat& ncr_format,
-                   const bool always_include_fqdn,
-                   const bool override_no_update,
-                   const bool override_client_update,
-                   const bool replace_client_name,
-                   const std::string& generated_prefix,
-                   const std::string& qualifying_suffix);
-
-    /// @brief Default constructor
-    /// The default constructor creates an instance that has updates disabled.
-    D2ClientConfig();
-
-    /// @brief Destructor
-    virtual ~D2ClientConfig();
-
-    /// @brief Return whether or not DHCP-DDNS updating is enabled.
-    bool getEnableUpdates() const {
-        return(enable_updates_);
-    }
-
-    /// @brief Return the IP address of b10-dhcp-ddns (IPv4 or IPv6).
-    const isc::asiolink::IOAddress& getServerIp() const {
-        return(server_ip_);
-    }
-
-    /// @brief Return the IP port of b10-dhcp-ddns.
-    size_t getServerPort() const {
-        return(server_port_);
-    }
-
-    /// @brief Return the socket protocol to use with b10-dhcp-ddns.
-    const dhcp_ddns::NameChangeProtocol& getNcrProtocol() const {
-         return(ncr_protocol_);
-    }
-
-    /// @brief Return the b10-dhcp-ddns request format.
-    const dhcp_ddns::NameChangeFormat& getNcrFormat() const {
-        return(ncr_format_);
-    }
-
-    /// @brief Return whether or not FQDN is always included in DHCP responses.
-    bool getAlwaysIncludeFqdn() const {
-        return(always_include_fqdn_);
-    }
-
-    /// @brief Return if updates are done even if clients request no updates.
-    bool getOverrideNoUpdate() const {
-        return(override_no_update_);
-    }
-
-    /// @brief Return if updates are done even when clients request delegation.
-    bool getOverrideClientUpdate() const {
-        return(override_client_update_);
-    }
-
-    /// @brief Return whether or not client's domain-name is always replaced.
-    bool getReplaceClientName() const {
-        return(replace_client_name_);
-    }
-
-    /// @brief Return the prefix to use when generating domain-names.
-    const std::string& getGeneratedPrefix() const {
-        return(generated_prefix_);
-    }
-
-    /// @brief Return the suffix to use to qualify partial domain-names.
-    const std::string& getQualifyingSuffix() const {
-        return(qualifying_suffix_);
-    }
-
-    /// @brief Compares two D2ClientConfigs for equality
-    bool operator == (const D2ClientConfig& other) const;
-
-    /// @brief Compares two D2ClientConfigs for inequality
-    bool operator != (const D2ClientConfig& other) const;
-
-    /// @brief Generates a string representation of the class contents.
-    std::string toText() const;
-
-protected:
-    /// @brief Validates member values.
-    ///
-    /// Method is used by the constructor to validate member contents.
-    ///
-    /// @throw D2ClientError if given an invalid protocol or format.
-    virtual void validateContents();
-
-private:
-    /// @brief Indicates whether or not DHCP DDNS updating is enabled.
-    bool enable_updates_;
-
-    /// @brief IP address of the b10-dhcp-ddns server (IPv4 or IPv6).
-    isc::asiolink::IOAddress server_ip_;
-
-    /// @brief IP port of the b10-dhcp-ddns server.
-    size_t server_port_;
-
-    /// @brief The socket protocol to use with b10-dhcp-ddns.
-    /// Currently only UDP is supported.
-    dhcp_ddns::NameChangeProtocol ncr_protocol_;
-
-    /// @brief Format of the b10-dhcp-ddns requests.
-    /// Currently only JSON format is supported.
-    dhcp_ddns::NameChangeFormat ncr_format_;
-
-    /// @brief Should Kea always include the FQDN option in its response.
-    bool always_include_fqdn_;
-
-    /// @brief Should Kea perform updates, even if client requested no updates.
-    /// Overrides the client request for no updates via the N flag.
-    bool override_no_update_;
-
-    /// @brief Should Kea perform updates, even if client requested delegation.
-    bool override_client_update_;
-
-    /// @brief Should Kea replace the domain-name supplied by the client.
-    bool replace_client_name_;
-
-    /// @brief Prefix Kea should use when generating domain-names.
-    std::string generated_prefix_;
-
-    /// @brief Suffix Kea should use when to qualify partial domain-names.
-    std::string qualifying_suffix_;
-};
-
-std::ostream&
-operator<<(std::ostream& os, const D2ClientConfig& config);
-
-/// @brief Defines a pointer for D2ClientConfig instances.
-typedef boost::shared_ptr<D2ClientConfig> D2ClientConfigPtr;
-
 /// @brief Defines the type for D2 IO error handler.
 /// This callback is invoked when a send to b10-dhcp-ddns completes with a
 /// failed status.  This provides the application layer (Kea) with a means to
@@ -219,7 +42,7 @@ typedef boost::shared_ptr<D2ClientConfig> D2ClientConfigPtr;
 /// @param result Result code of the send operation.
 /// @param ncr NameChangeRequest which failed to send.
 ///
-/// @note Handlers are expected not to throw. In the event a hanlder does
+/// @note Handlers are expected not to throw. In the event a handler does
 /// throw invoking code logs the exception and then swallows it.
 typedef
 boost::function<void(const dhcp_ddns::NameChangeSender::Result result,
@@ -240,13 +63,13 @@ boost::function<void(const dhcp_ddns::NameChangeSender::Result result,
 /// used by the NCR IPC with the select-driven IO used by Kea.  Senders expose
 /// a file descriptor, the "select-fd" that can monitored for read-readiness
 /// with the select() function (or variants).  D2ClientMgr provides a method,
-/// runReadyIO(), that will process all ready events on a sender's
-/// IOservice.  Track# 3315 is extending Kea's IfaceMgr to support the
-/// registration of multiple external sockets with callbacks that are then
-/// monitored with IO readiness via select().
-/// @todo D2ClientMgr will be modified to register the sender's select-fd and
-/// runReadyIO() with IfaceMgr when entering the send mode and will
-/// unregister when exiting send mode.
+/// runReadyIO(), that will instructs the sender to process the next ready
+/// ready IO handler on the sender's IOservice.  Track# 3315 extended
+/// Kea's IfaceMgr to support the registration of multiple external sockets
+/// with callbacks that are then monitored with IO readiness via select().
+/// D2ClientMgr registers the sender's select-fd and runReadyIO() with
+/// IfaceMgr when entering the send mode and unregisters it when exiting send
+/// mode.
 ///
 /// To place the manager in send mode, the calling layer must supply an error
 /// handler and optionally an IOService instance.  The error handler is invoked
@@ -258,7 +81,8 @@ boost::function<void(const dhcp_ddns::NameChangeSender::Result result,
 /// into the sender.  Using a private service isolates the sender's IO from
 /// any other services.
 ///
-class D2ClientMgr : public dhcp_ddns::NameChangeSender::RequestSendHandler {
+class D2ClientMgr : public dhcp_ddns::NameChangeSender::RequestSendHandler,
+                    boost::noncopyable {
 public:
     /// @brief Constructor
     ///
@@ -339,7 +163,7 @@ public:
     /// Templated wrapper around the analyzeFqdn() allowing that method to
     /// be used for either IPv4 or IPv6 processing.  This methods resets all
     /// of the flags in the response to zero and then sets the S,N, and O
-    /// flags.  Any other flags are the responsiblity of the invoking layer.
+    /// flags.  Any other flags are the responsibility of the invoking layer.
     ///
     /// @param fqdn FQDN option from which to read client (inbound) flags
     /// @param fqdn_resp FQDN option to update with the server (outbound) flags
@@ -430,17 +254,36 @@ public:
     /// @brief Send the given NameChangeRequests to b10-dhcp-ddns
     ///
     /// Passes NameChangeRequests to the NCR sender for transmission to
-    /// b10-dhcp-ddns.
+    /// b10-dhcp-ddns. If the sender rejects the message, the client's error
+    /// handler will be invoked.  The most likely cause for rejection is
+    /// the senders' queue has reached maximum capacity.
     ///
     /// @param ncr NameChangeRequest to send
     ///
-    /// @throw D2ClientError if sender instance is null. Underlying layer
-    /// may throw NCRSenderExceptions exceptions.
+    /// @throw D2ClientError if sender instance is null or not in send
+    /// mode.  Either of these represents a programmatic error.
     void sendRequest(dhcp_ddns::NameChangeRequestPtr& ncr);
 
+    /// @brief Calls the client's error handler.
+    ///
+    /// Calls the error handler method set by startSender() when an
+    /// error occurs attempting to send a method.  If the error handler
+    /// throws an exception it will be caught and logged.
+    ///
+    /// @param result contains that send outcome status.
+    /// @param ncr is a pointer to the NameChangeRequest that was attempted.
+    ///
+    /// This method is exception safe.
+    void invokeClientErrorHandler(const dhcp_ddns::NameChangeSender::
+                                  Result result,
+                                  dhcp_ddns::NameChangeRequestPtr& ncr);
+
     /// @brief Returns the number of NCRs queued for transmission.
     size_t getQueueSize() const;
 
+    /// @brief Returns the maximum number of NCRs allowed in the queue.
+    size_t getQueueMaxSize() const;
+
     /// @brief Returns the nth NCR queued for transmission.
     ///
     /// Note that the entry is not removed from the queue.
@@ -462,14 +305,28 @@ public:
 
     /// @brief Processes sender IO events
     ///
-    /// Runs all handlers ready for execution on the sender's IO service.
+    /// Serves as callback registered for the sender's select-fd with IfaceMgr.
+    /// It instructs the sender to execute the next ready IO handler.
+    /// It provides an instance method that can be bound via boost::bind, as
+    /// NameChangeSender is abstract.
     void runReadyIO();
 
+    /// @brief Suspends sending requests.
+    ///
+    /// This method is intended to be used when IO errors occur.  It toggles
+    /// the enable-updates configuration flag to off, and takes the sender
+    /// out of send mode.  Messages in the sender's queue will remain in the
+    /// queue.
+    /// @todo This logic may change in NameChangeSender is altered allow
+    /// queuing while stopped.  Currently when a sender is not in send mode
+    /// it will not accept additional messages.
+    void suspendUpdates();
+
 protected:
     /// @brief Function operator implementing the NCR sender callback.
     ///
     /// This method is invoked each time the NameChangeSender completes
-    /// an asychronous send.
+    /// an asynchronous send.
     ///
     /// @param result contains that send outcome status.
     /// @param ncr is a pointer to the NameChangeRequest that was
@@ -491,6 +348,14 @@ protected:
     /// mode.
     int getSelectFd();
 
+    /// @brief Fetches the select-fd that is currently registered.
+    ///
+    /// @return The currently registered select-fd or
+    /// dhcp_ddns::WatchSocket::INVALID_SOCKET.
+    ///
+    /// @note This is only exposed for testing purposes.
+    int getRegisteredSelectFd();
+
 private:
     /// @brief Container class for DHCP-DDNS configuration parameters.
     D2ClientConfigPtr d2_client_config_;
@@ -506,12 +371,8 @@ private:
     /// completes with a failed status.
     D2ClientErrorHandler client_error_handler_;
 
-    /// @brief Pointer to the IOService currently being used by the sender.
-    /// @note We need to remember the io_service given to the sender however
-    /// we may have received only a referenece to it from the calling layer.
-    /// Use a raw pointer to store it.  This value should never be exposed
-    /// and is only valid while in send mode.
-    asiolink::IOService* sender_io_service_;
+    /// @brief Remembers the select-fd registered with IfaceMgr.
+    int registered_select_fd_;
 };
 
 template <class T>

+ 1 - 1
src/lib/dhcpsrv/dhcp_parsers.h

@@ -18,7 +18,7 @@
 #include <asiolink/io_address.h>
 #include <cc/data.h>
 #include <dhcp/option_definition.h>
-#include <dhcpsrv/d2_client.h>
+#include <dhcpsrv/d2_client_cfg.h>
 #include <dhcpsrv/dhcp_config_parser.h>
 #include <dhcpsrv/option_space_container.h>
 #include <dhcpsrv/subnet.h>

+ 57 - 25
src/lib/dhcpsrv/dhcpsrv_messages.mes

@@ -112,6 +112,11 @@ This is a debug message reporting that the DHCP configuration manager has
 returned the specified IPv4 subnet when given the address hint specified
 as the address is within the subnet.
 
+% DHCPSRV_CFGMGR_SUBNET4_RELAY selected subnet %1, because of matching relay addr %2
+This is a debug message reporting that the DHCP configuration manager has
+returned the specified IPv4 subnet, because detected relay agent address
+matches value specified for this subnet.
+
 % DHCPSRV_CFGMGR_SUBNET6 retrieved subnet %1 for address hint %2
 This is a debug message reporting that the DHCP configuration manager has
 returned the specified IPv6 subnet when given the address hint specified
@@ -131,6 +136,11 @@ subnet was selected, because value of interface-id option matched what was
 configured in server's interface-id option for that selected subnet6.
 (see 'interface-id' parameter in the subnet6 definition).
 
+% DHCPSRV_CFGMGR_SUBNET6_RELAY selected subnet %1, because of matching relay addr %2
+This is a debug message reporting that the DHCP configuration manager has
+returned the specified IPv6 subnet, because detected relay agent address
+matches value specified for this subnet.
+
 % DHCPSRV_CLOSE_DB closing currently open %1 database
 This is a debug message, issued when the DHCP server closes the currently
 open lease database.  It is issued at program shutdown and whenever
@@ -138,11 +148,42 @@ the database access parameters are changed: in the latter case, the
 server closes the currently open database, and opens a database using
 the new parameters.
 
-% DHCPSRV_HOOK_LEASE4_SELECT_SKIP Lease4 creation was skipped, because of callout skip flag.
-This debug message is printed when a callout installed on lease4_select
-hook point sets the skip flag. It means that the server was told that
-no lease4 should be assigned. The server will not put that lease in its
-database and the client will get a NAK packet.
+% DHCPSRV_DHCP_DDNS_ERROR_EXCEPTION error handler for DHCP_DDNS IO generated an expected exception: %1
+This is an error message that occurs when an attempt to send a request to
+b10-dhcp-ddns fails there registered error handler threw an uncaught exception.
+This is a programmatic error which should not occur. By convention, the error
+handler should not propagate exceptions. Please report this error.
+
+% DHCPSRV_DHCP_DDNS_HANDLER_NULL error handler for DHCP_DDNS IO is not set.
+This is an error message that occurs when an attempt to send a request to
+b10-dhcp-ddns fails and there is no registered error handler.  This is a
+programmatic error which should never occur and should be reported.
+
+% DHCPSRV_DHCP_DDNS_NCR_REJECTED NameChangeRequest rejected by the sender: %1, ncr: %2
+This is an error message indicating that NameChangeSender used to deliver DDNS
+update requests to b10-dhcp-ddns rejected the request.  This most likely cause
+is the sender's queue has reached maximum capacity.  This would imply that
+requests are being generated faster than they can be delivered.
+
+% DHCPSRV_DHCP_DDNS_NCR_SENT NameChangeRequest sent to b10-dhcp-ddns: %1
+A debug message issued when a NameChangeRequest has been successfully sent to
+b10-dhcp-ddns.
+
+% DHCPSRV_DHCP_DDNS_SENDER_STARTED NameChangeRequest sender has been started: %1
+A informational message issued when a communications with b10-dhcp-ddns has
+been successfully started.
+
+% DHCPSRV_DHCP_DDNS_SENDER_STOPPED NameChangeRequest sender has been stopped.
+A informational message issued when a communications with b10-dhcp-ddns has
+been stopped. This normally occurs during reconfiguration and as part of normal
+shutdown. It may occur if b10-dhcp-ddns communications breakdown.
+
+% DHCPSRV_DHCP_DDNS_SUSPEND_UPDATES DHCP_DDNS updates are being suspended.
+This is a warning message indicating the DHCP_DDNS updates have been turned
+off.  This should only occur if IO errors communicating with b10-dhcp-ddns
+have been experienced.  Any such errors should have preceding entries in the
+log with details.  No further attempts to communicate with b10-dhcp-ddns will
+be made without intervention.
 
 % DHCPSRV_HOOK_LEASE4_RENEW_SKIP DHCPv4 lease was not renewed because a callout set the skip flag.
 This debug message is printed when a callout installed on lease4_renew
@@ -150,6 +191,12 @@ hook point set the skip flag. For this particular hook point, the setting
 of the flag by a callout instructs the server to not renew a lease. The
 server will use existing lease as it is, without extending its lifetime.
 
+% DHCPSRV_HOOK_LEASE4_SELECT_SKIP Lease4 creation was skipped, because of callout skip flag.
+This debug message is printed when a callout installed on lease4_select
+hook point sets the skip flag. It means that the server was told that
+no lease4 should be assigned. The server will not put that lease in its
+database and the client will get a NAK packet.
+
 % DHCPSRV_HOOK_LEASE6_SELECT_SKIP Lease6 (non-temporary) creation was skipped, because of callout skip flag.
 This debug message is printed when a callout installed on lease6_select
 hook point sets the skip flag. It means that the server was told that
@@ -198,6 +245,11 @@ A debug message issued when the server is attempting to obtain a set of
 IPv4 leases from the memory file database for a client with the specified
 client identification.
 
+% DHCPSRV_MEMFILE_GET_CLIENTID_HWADDR_SUBID obtaining IPv4 lease for client ID %1, hardware address %2 and subnet ID %3
+A debug message issued when the server is attempting to obtain an IPv4
+lease from the memory file database for a client with the specified
+client ID, hardware address and subnet ID.
+
 % DHCPSRV_MEMFILE_GET_HWADDR obtaining IPv4 leases for hardware address %1
 A debug message issued when the server is attempting to obtain a set of
 IPv4 leases from the memory file database for a client with the specified
@@ -213,11 +265,6 @@ A debug message issued when the server is attempting to obtain an IPv6
 lease from the memory file database for a client with the specified IAID
 (Identity Association ID), Subnet ID and DUID (DHCP Unique Identifier).
 
-% DHCPSRV_MEMFILE_GET_CLIENTID_HWADDR_SUBID obtaining IPv4 lease for client ID %1, hardware address %2 and subnet ID %3
-A debug message issued when the server is attempting to obtain an IPv4
-lease from the memory file database for a client with the specified
-client ID, hardware address and subnet ID.
-
 % DHCPSRV_MEMFILE_GET_SUBID_CLIENTID obtaining IPv4 lease for subnet ID %1 and client ID %2
 A debug message issued when the server is attempting to obtain an IPv4
 lease from the memory file database for a client with the specified
@@ -344,18 +391,3 @@ indicate an error in the source code, please submit a bug report.
 % DHCPSRV_UNKNOWN_DB unknown database type: %1
 The database access string specified a database type (given in the
 message) that is unknown to the software.  This is a configuration error.
-
-% DHCPSRV_DHCP_DDNS_HANDLER_NULL error handler for DHCP_DDNS IO is not set.
-This is an error message that occurs when an attempt to send a request to
-b10-dhcp-ddns fails and there is no registered error handler.  This is a
-programmatic error which should never occur and should be reported.
-
-% DHCPSRV_DHCP_DDNS_ERROR_EXCEPTION error handler for DHCP_DDNS IO generated an expected exception: %1
-This is an error message that occurs when an attempt to send a request to
-b10-dhcp-ddns fails there registered error handler threw an uncaught exception.
-This is a programmatic error which should not occur. By convention, the error
-handler should not propagate exceptions. Please report this error.
-
-% DHCPSRV_DHCP_DDNS_NCR_SENT NameChangeRequest sent to b10-dhcp-ddns: %1
-A debug message issued when a NameChangeRequest has been successfully sent to
-b10-dhcp-ddns.

+ 7 - 4
src/lib/dhcpsrv/tests/Makefile.am

@@ -33,18 +33,20 @@ if HAVE_GTEST
 # to unexpected errors. For this reason, the --enable-static-link option is
 # ignored for unit tests built here.
 
-nodistdir=$(abs_top_builddir)/src/lib/dhcpsrv/tests
-nodist_LTLIBRARIES = libco1.la libco2.la
+noinst_LTLIBRARIES = libco1.la libco2.la
+
+# -rpath /nowhere is a hack to trigger libtool to not create a
+# convenience archive, resulting in shared modules
 
 libco1_la_SOURCES  = callout_library.cc
 libco1_la_CXXFLAGS = $(AM_CXXFLAGS)
 libco1_la_CPPFLAGS = $(AM_CPPFLAGS) $(LOG4CPLUS_INCLUDES)
-libco1_la_LDFLAGS = -avoid-version -export-dynamic -module
+libco1_la_LDFLAGS = -avoid-version -export-dynamic -module -rpath /nowhere
 
 libco2_la_SOURCES  = callout_library.cc
 libco2_la_CXXFLAGS = $(AM_CXXFLAGS)
 libco2_la_CPPFLAGS = $(AM_CPPFLAGS) $(LOG4CPLUS_INCLUDES)
-libco2_la_LDFLAGS = -avoid-version -export-dynamic -module
+libco2_la_LDFLAGS = -avoid-version -export-dynamic -module -rpath /nowhere
 
 TESTS += libdhcpsrv_unittests
 
@@ -59,6 +61,7 @@ libdhcpsrv_unittests_SOURCES += dbaccess_parser_unittest.cc
 libdhcpsrv_unittests_SOURCES += lease_unittest.cc
 libdhcpsrv_unittests_SOURCES += lease_mgr_factory_unittest.cc
 libdhcpsrv_unittests_SOURCES += lease_mgr_unittest.cc
+libdhcpsrv_unittests_SOURCES += generic_lease_mgr_unittest.cc
 libdhcpsrv_unittests_SOURCES += memfile_lease_mgr_unittest.cc
 libdhcpsrv_unittests_SOURCES += dhcp_parsers_unittest.cc
 if HAVE_MYSQL

+ 82 - 9
src/lib/dhcpsrv/tests/cfgmgr_unittest.cc

@@ -472,6 +472,79 @@ TEST_F(CfgMgrTest, classifySubnet4) {
     EXPECT_FALSE(cfg_mgr.getSubnet4(IOAddress("192.0.2.130"), classify_));
 }
 
+// This test verifies if the configuration manager is able to hold v4 subnets
+// with their relay address information and return proper subnets, based on
+// those addresses.
+TEST_F(CfgMgrTest, subnet4RelayOverride) {
+    CfgMgr& cfg_mgr = CfgMgr::instance();
+
+    // Let's configure 3 subnets
+    Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3));
+    Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3));
+    Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3));
+
+    cfg_mgr.addSubnet4(subnet1);
+    cfg_mgr.addSubnet4(subnet2);
+    cfg_mgr.addSubnet4(subnet3);
+
+    // Check that without relay-info specified, subnets are not selected
+    EXPECT_FALSE(cfg_mgr.getSubnet4(IOAddress("10.0.0.1"), classify_, true));
+    EXPECT_FALSE(cfg_mgr.getSubnet4(IOAddress("10.0.0.2"), classify_, true));
+    EXPECT_FALSE(cfg_mgr.getSubnet4(IOAddress("10.0.0.3"), classify_, true));
+
+    // Now specify relay info
+    subnet1->setRelayInfo(IOAddress("10.0.0.1"));
+    subnet2->setRelayInfo(IOAddress("10.0.0.2"));
+    subnet3->setRelayInfo(IOAddress("10.0.0.3"));
+
+    // And try again. This time relay-info is there and should match.
+    EXPECT_EQ(subnet1, cfg_mgr.getSubnet4(IOAddress("10.0.0.1"), classify_, true));
+    EXPECT_EQ(subnet2, cfg_mgr.getSubnet4(IOAddress("10.0.0.2"), classify_, true));
+    EXPECT_EQ(subnet3, cfg_mgr.getSubnet4(IOAddress("10.0.0.3"), classify_, true));
+
+    // Finally, check that the relay works only if hint provided is relay address
+    EXPECT_FALSE(cfg_mgr.getSubnet4(IOAddress("10.0.0.1"), classify_, false));
+    EXPECT_FALSE(cfg_mgr.getSubnet4(IOAddress("10.0.0.2"), classify_, false));
+    EXPECT_FALSE(cfg_mgr.getSubnet4(IOAddress("10.0.0.3"), classify_, false));
+}
+
+// This test verifies if the configuration manager is able to hold v6 subnets
+// with their relay address information and return proper subnets, based on
+// those addresses.
+TEST_F(CfgMgrTest, subnet6RelayOverride) {
+    CfgMgr& cfg_mgr = CfgMgr::instance();
+
+    // Let's configure 3 subnets
+    Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 48, 1, 2, 3, 4));
+    Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"), 48, 1, 2, 3, 4));
+    Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:3::"), 48, 1, 2, 3, 4));
+
+    cfg_mgr.addSubnet6(subnet1);
+    cfg_mgr.addSubnet6(subnet2);
+    cfg_mgr.addSubnet6(subnet3);
+
+    // Check that without relay-info specified, subnets are not selected
+    EXPECT_FALSE(cfg_mgr.getSubnet6(IOAddress("2001:db8:ff::1"), classify_, true));
+    EXPECT_FALSE(cfg_mgr.getSubnet6(IOAddress("2001:db8:ff::2"), classify_, true));
+    EXPECT_FALSE(cfg_mgr.getSubnet6(IOAddress("2001:db8:ff::3"), classify_, true));
+
+    // Now specify relay info
+    subnet1->setRelayInfo(IOAddress("2001:db8:ff::1"));
+    subnet2->setRelayInfo(IOAddress("2001:db8:ff::2"));
+    subnet3->setRelayInfo(IOAddress("2001:db8:ff::3"));
+
+    // And try again. This time relay-info is there and should match.
+    EXPECT_EQ(subnet1, cfg_mgr.getSubnet6(IOAddress("2001:db8:ff::1"), classify_, true));
+    EXPECT_EQ(subnet2, cfg_mgr.getSubnet6(IOAddress("2001:db8:ff::2"), classify_, true));
+    EXPECT_EQ(subnet3, cfg_mgr.getSubnet6(IOAddress("2001:db8:ff::3"), classify_, true));
+
+    // Finally, check that the relay works only if hint provided is relay address
+    EXPECT_FALSE(cfg_mgr.getSubnet6(IOAddress("2001:db8:ff::1"), classify_, false));
+    EXPECT_FALSE(cfg_mgr.getSubnet6(IOAddress("2001:db8:ff::2"), classify_, false));
+    EXPECT_FALSE(cfg_mgr.getSubnet6(IOAddress("2001:db8:ff::3"), classify_, false));
+}
+
+
 // This test verifies if the configuration manager is able to hold and return
 // valid leases
 TEST_F(CfgMgrTest, classifySubnet6) {
@@ -626,10 +699,10 @@ TEST_F(CfgMgrTest, subnet6) {
     // Now we have only one subnet, any request will be served from it
     EXPECT_EQ(subnet1, cfg_mgr.getSubnet6(IOAddress("2000::1"), classify_));
 
-    // If we have only a single subnet and the request came from a local
-    // address, let's use that subnet
-    EXPECT_EQ(subnet1, cfg_mgr.getSubnet6(IOAddress("fe80::dead:beef"),
-                  classify_));
+    // We used to allow getting a sole subnet if there was only one subnet
+    // configured. That is no longer true. The code should not return
+    // a subnet.
+    EXPECT_FALSE(cfg_mgr.getSubnet6(IOAddress("fe80::dead:beef"), classify_));
 
     cfg_mgr.addSubnet6(subnet2);
     cfg_mgr.addSubnet6(subnet3);
@@ -670,10 +743,10 @@ TEST_F(CfgMgrTest, subnet6Interface) {
     // only one subnet defined.
     EXPECT_FALSE(cfg_mgr.getSubnet6("bar", classify_));
 
-    // If we have only a single subnet and the request came from a local
-    // address, let's use that subnet
-    EXPECT_EQ(subnet1, cfg_mgr.getSubnet6(IOAddress("fe80::dead:beef"),
-                                          classify_));
+    // We used to allow getting a sole subnet if there was only one subnet
+    // configured. That is no longer true. The code should not return
+    // a subnet.
+    EXPECT_FALSE(cfg_mgr.getSubnet6(IOAddress("fe80::dead:beef"), classify_));
 
     cfg_mgr.addSubnet6(subnet2);
     cfg_mgr.addSubnet6(subnet3);
@@ -903,7 +976,7 @@ TEST_F(CfgMgrTest, d2ClientConfig) {
     // After CfgMgr construction, D2ClientMgr member should be initialized
     // with a D2 configuration that is disabled.
     // Verify we can Fetch the mgr.
-    D2ClientMgr d2_mgr = CfgMgr::instance().getD2ClientMgr();
+    D2ClientMgr& d2_mgr = CfgMgr::instance().getD2ClientMgr();
     EXPECT_FALSE(d2_mgr.ddnsEnabled());
 
     // Make sure the convenience method fetches the config correctly.

+ 7 - 1
src/lib/dhcpsrv/tests/d2_client_unittest.cc

@@ -15,7 +15,7 @@
 #include <config.h>
 #include <dhcp/option4_client_fqdn.h>
 #include <dhcp/option6_client_fqdn.h>
-#include <dhcpsrv/d2_client.h>
+#include <dhcpsrv/d2_client_mgr.h>
 #include <exceptions/exceptions.h>
 
 #include <gtest/gtest.h>
@@ -36,6 +36,12 @@ TEST(D2ClientConfigTest, constructorsAndAccessors) {
     ASSERT_NO_THROW(d2_client_config.reset(new D2ClientConfig()));
     EXPECT_FALSE(d2_client_config->getEnableUpdates());
 
+    // Verify the enable-updates can be toggled.
+    d2_client_config->enableUpdates(true);
+    EXPECT_TRUE(d2_client_config->getEnableUpdates());
+    d2_client_config->enableUpdates(false);
+    EXPECT_FALSE(d2_client_config->getEnableUpdates());
+
     d2_client_config.reset();
 
     bool enable_updates = true;

+ 119 - 25
src/lib/dhcpsrv/tests/d2_udp_unittest.cc

@@ -19,7 +19,8 @@
 #include <asio.hpp>
 #include <asiolink/io_service.h>
 #include <config.h>
-#include <dhcpsrv/d2_client.h>
+#include <dhcp/iface_mgr.h>
+#include <dhcpsrv/d2_client_mgr.h>
 #include <exceptions/exceptions.h>
 
 #include <boost/function.hpp>
@@ -105,6 +106,7 @@ public:
         FD_ZERO(&read_fds);
 
         int select_fd = -1;
+        // cppcheck-suppress redundantAssignment
         ASSERT_NO_THROW(select_fd = getSelectFd());
 
         FD_SET(select_fd,  &read_fds);
@@ -231,7 +233,7 @@ TEST_F(D2ClientMgrTest, udpSenderQueing) {
 
     // Trying to send a NCR when not in send mode should fail.
     dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr();
-    EXPECT_THROW(sendRequest(ncr), dhcp_ddns::NcrSenderError);
+    EXPECT_THROW(sendRequest(ncr), D2ClientError);
 
     // Place sender in send mode.
     ASSERT_NO_THROW(startSender(getErrorHandler()));
@@ -330,55 +332,147 @@ TEST_F(D2ClientMgrTest, udpSendExternalIOService) {
 /// when send errors occur.
 TEST_F(D2ClientMgrTest, udpSendErrorHandler) {
     // Enable DDNS with server at 127.0.0.1/prot 53001 via UDP.
-    enableDdns("127.0.0.1", 530001, dhcp_ddns::NCR_UDP);
-
-    // Trying to fetch the select-fd when not sending should fail.
-    ASSERT_THROW(getSelectFd(), D2ClientError);
-
     // Place sender in send mode.
+    enableDdns("127.0.0.1", 530001, dhcp_ddns::NCR_UDP);
     ASSERT_NO_THROW(startSender(getErrorHandler()));
 
-    // select_fd should evaluate to NOT ready to read.
-    selectCheck(false);
-
     // Simulate a failed response in the send call back. This should
     // cause the error handler to get invoked.
     simulate_send_failure_ = true;
 
+    // Verify error count is zero.
     ASSERT_EQ(0, error_handler_count_);
 
     // Send a test request.
     dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr();
     ASSERT_NO_THROW(sendRequest(ncr));
 
-    // select_fd should evaluate to ready to read.
-    selectCheck(true);
+    // Call the ready handler. This should complete the message with an error.
+    ASSERT_NO_THROW(runReadyIO());
 
-    // Call service handler.
-    runReadyIO();
+    // If we executed error handler properly, the error count should one.
+    ASSERT_EQ(1, error_handler_count_);
+}
 
-    // select_fd should evaluate to not ready to read.
-    selectCheck(false);
 
-    ASSERT_EQ(1, error_handler_count_);
+/// @brief Checks that client error handler exceptions are handled gracefully.
+TEST_F(D2ClientMgrTest, udpSendErrorHandlerThrow) {
+    // Enable DDNS with server at 127.0.0.1/prot 53001 via UDP.
+    // Place sender in send mode.
+    enableDdns("127.0.0.1", 530001, dhcp_ddns::NCR_UDP);
+    ASSERT_NO_THROW(startSender(getErrorHandler()));
 
-    // Simulate a failed response in the send call back. This should
-    // cause the error handler to get invoked.
+    // Simulate a failed response in the send call back and
+    // force a throw in the error handler.
     simulate_send_failure_ = true;
     error_handler_throw_ = true;
 
+    // Verify error count is zero.
+    ASSERT_EQ(0, error_handler_count_);
+
     // Send a test request.
-    ncr = buildTestNcr();
+    dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr();
     ASSERT_NO_THROW(sendRequest(ncr));
 
-    // Call the io service handler.
-    runReadyIO();
+    // Call the ready handler. This should complete the message with an error.
+    // The handler should throw but the exception should not escape.
+    ASSERT_NO_THROW(runReadyIO());
 
-    // Simulation flag should be false.
+    // If throw flag is false, then we were in the error handler should
+    // have thrown.
     ASSERT_FALSE(error_handler_throw_);
 
-    // Count should still be 1.
-    ASSERT_EQ(1, error_handler_count_);
+    // If error count is still zero, then we did throw.
+    ASSERT_EQ(0, error_handler_count_);
+}
+
+/// @brief Tests that D2ClientMgr registers and unregisters with IfaceMgr.
+TEST_F(D2ClientMgrTest, ifaceRegister) {
+    // Enable DDNS with server at 127.0.0.1/prot 53001 via UDP.
+    enableDdns("127.0.0.1", 530001, dhcp_ddns::NCR_UDP);
+
+    // Place sender in send mode.
+    ASSERT_NO_THROW(startSender(getErrorHandler()));
+
+    // Queue three messages.
+    for (int i = 0; i < 3; ++i) {
+        dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr();
+        ASSERT_NO_THROW(sendRequest(ncr));
+    }
+
+    // Make sure queue count is correct.
+    EXPECT_EQ(3, getQueueSize());
+
+    // select_fd should evaluate to ready to read.
+    selectCheck(true);
+
+    // Calling receive should complete the first message and start the second.
+    IfaceMgr::instance().receive4(0, 0);
+
+    // Verify the callback hander was invoked, no errors counted.
+    EXPECT_EQ(2, getQueueSize());
+    ASSERT_EQ(1, callback_count_);
+    ASSERT_EQ(0, error_handler_count_);
+
+    // Stop the sender.  This should complete the second message but leave
+    // the third in the queue.
+    ASSERT_NO_THROW(stopSender());
+    EXPECT_EQ(1, getQueueSize());
+    ASSERT_EQ(2, callback_count_);
+    ASSERT_EQ(0, error_handler_count_);
+
+    // Calling receive again should have no affect.
+    IfaceMgr::instance().receive4(0, 0);
+    EXPECT_EQ(1, getQueueSize());
+    ASSERT_EQ(2, callback_count_);
+    ASSERT_EQ(0, error_handler_count_);
+}
+
+/// @brief Checks that D2ClientMgr suspendUpdates works properly.
+TEST_F(D2ClientMgrTest, udpSuspendUpdates) {
+    // Enable DDNS with server at 127.0.0.1/prot 53001 via UDP.
+    // Place sender in send mode.
+    enableDdns("127.0.0.1", 530001, dhcp_ddns::NCR_UDP);
+    ASSERT_NO_THROW(startSender(getErrorHandler()));
+
+    // Send a test request.
+    for (int i = 0; i < 3; ++i) {
+        dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr();
+        ASSERT_NO_THROW(sendRequest(ncr));
+    }
+    ASSERT_EQ(3, getQueueSize());
+
+    // Call the ready handler. This should complete the first message
+    // and initiate sending the second message.
+    ASSERT_NO_THROW(runReadyIO());
+
+    // Queue count should have gone down by 1.
+    ASSERT_EQ(2, getQueueSize());
+
+    // Suspend updates. This should disable updates and stop the sender.
+    ASSERT_NO_THROW(suspendUpdates());
+
+    EXPECT_FALSE(ddnsEnabled());
+    EXPECT_FALSE(amSending());
+
+    // Stopping the sender should have completed the second message's
+    // in-progess send, so queue size should be 1.
+    ASSERT_EQ(1, getQueueSize());
+}
+
+/// @brief Tests that invokeErrorHandler does not fail if there is no handler.
+TEST_F(D2ClientMgrTest, missingErrorHandler) {
+    // Ensure we aren't in send mode.
+    ASSERT_FALSE(ddnsEnabled());
+    ASSERT_FALSE(amSending());
+
+    // There is no error handler at this point, so invoking should not throw.
+    dhcp_ddns::NameChangeRequestPtr ncr;
+    ASSERT_NO_THROW(invokeClientErrorHandler(dhcp_ddns::NameChangeSender::ERROR,
+                                             ncr));
+
+    // Verify we didn't invoke the error handler, error count is zero.
+    ASSERT_EQ(0, error_handler_count_);
 }
 
 } // end of anonymous namespace

+ 0 - 0
src/lib/dhcpsrv/tests/generic_lease_mgr_unittest.cc


Some files were not shown because too many files changed in this diff