summaryrefslogtreecommitdiffstats
path: root/src/bin/dhcp4/tests
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin/dhcp4/tests')
-rw-r--r--src/bin/dhcp4/tests/Makefile.am181
-rw-r--r--src/bin/dhcp4/tests/Makefile.in1758
-rw-r--r--src/bin/dhcp4/tests/callout_library_1.cc34
-rw-r--r--src/bin/dhcp4/tests/callout_library_2.cc24
-rw-r--r--src/bin/dhcp4/tests/callout_library_3.cc95
-rw-r--r--src/bin/dhcp4/tests/callout_library_common.h110
-rw-r--r--src/bin/dhcp4/tests/classify_unittest.cc1443
-rw-r--r--src/bin/dhcp4/tests/client_handler_unittest.cc893
-rw-r--r--src/bin/dhcp4/tests/config_backend_unittest.cc566
-rw-r--r--src/bin/dhcp4/tests/config_parser_unittest.cc7835
-rw-r--r--src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc2234
-rw-r--r--src/bin/dhcp4/tests/d2_unittest.cc529
-rw-r--r--src/bin/dhcp4/tests/d2_unittest.h115
-rw-r--r--src/bin/dhcp4/tests/decline_unittest.cc367
-rw-r--r--src/bin/dhcp4/tests/dhcp4_client.cc600
-rw-r--r--src/bin/dhcp4/tests/dhcp4_client.h533
-rw-r--r--src/bin/dhcp4/tests/dhcp4_process_tests.sh.in591
-rw-r--r--src/bin/dhcp4/tests/dhcp4_srv_unittest.cc5222
-rw-r--r--src/bin/dhcp4/tests/dhcp4_test_utils.cc1011
-rw-r--r--src/bin/dhcp4/tests/dhcp4_test_utils.h799
-rw-r--r--src/bin/dhcp4/tests/dhcp4_unittests.cc26
-rw-r--r--src/bin/dhcp4/tests/dhcp4to6_ipc_unittest.cc411
-rw-r--r--src/bin/dhcp4/tests/direct_client_unittest.cc438
-rw-r--r--src/bin/dhcp4/tests/dora_unittest.cc3192
-rw-r--r--src/bin/dhcp4/tests/fqdn_unittest.cc2885
-rw-r--r--src/bin/dhcp4/tests/get_config_unittest.cc12481
-rw-r--r--src/bin/dhcp4/tests/get_config_unittest.cc.skel374
-rw-r--r--src/bin/dhcp4/tests/get_config_unittest.h27
-rw-r--r--src/bin/dhcp4/tests/hooks_unittest.cc3442
-rw-r--r--src/bin/dhcp4/tests/host_options_unittest.cc554
-rw-r--r--src/bin/dhcp4/tests/host_unittest.cc843
-rw-r--r--src/bin/dhcp4/tests/inform_unittest.cc657
-rw-r--r--src/bin/dhcp4/tests/kea_controller_unittest.cc1102
-rw-r--r--src/bin/dhcp4/tests/marker_file.cc60
-rw-r--r--src/bin/dhcp4/tests/marker_file.h.in62
-rw-r--r--src/bin/dhcp4/tests/out_of_range_unittest.cc712
-rw-r--r--src/bin/dhcp4/tests/parser_unittest.cc990
-rw-r--r--src/bin/dhcp4/tests/release_unittest.cc418
-rw-r--r--src/bin/dhcp4/tests/shared_network_unittest.cc3013
-rw-r--r--src/bin/dhcp4/tests/simple_parser4_unittest.cc215
-rw-r--r--src/bin/dhcp4/tests/test_data_files_config.h.in9
-rw-r--r--src/bin/dhcp4/tests/test_libraries.h.in32
-rw-r--r--src/bin/dhcp4/tests/vendor_opts_unittest.cc2798
43 files changed, 59681 insertions, 0 deletions
diff --git a/src/bin/dhcp4/tests/Makefile.am b/src/bin/dhcp4/tests/Makefile.am
new file mode 100644
index 0000000..7394350
--- /dev/null
+++ b/src/bin/dhcp4/tests/Makefile.am
@@ -0,0 +1,181 @@
+SUBDIRS = .
+
+# Add to the tarball:
+EXTRA_DIST = get_config_unittest.cc.skel
+
+TESTS_ENVIRONMENT = $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND)
+
+# Shell tests
+SHTESTS = dhcp4_process_tests.sh
+
+# As with every file generated by ./configure, clean them up when running
+# "make distclean", but not on "make clean".
+DISTCLEANFILES = $(SHTESTS)
+DISTCLEANFILES += marker_file.h
+DISTCLEANFILES += test_data_files_config.h
+DISTCLEANFILES += test_libraries.h
+
+AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
+AM_CPPFLAGS += -I$(top_srcdir)/src -I$(top_builddir)/src
+AM_CPPFLAGS += -I$(top_srcdir)/src/bin -I$(top_builddir)/src/bin
+AM_CPPFLAGS += $(BOOST_INCLUDES)
+AM_CPPFLAGS += -DTEST_DATA_BUILDDIR=\"$(abs_top_builddir)/src/bin/dhcp4/tests\"
+AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\"
+AM_CPPFLAGS += -DCFG_EXAMPLES=\"$(abs_top_srcdir)/doc/examples/kea4\"
+AM_CPPFLAGS += -DSYNTAX_FILE=\"$(abs_srcdir)/../dhcp4_parser.yy\"
+AM_CPPFLAGS += -DKEA_LFC_EXECUTABLE=\"$(abs_top_builddir)/src/bin/lfc/kea-lfc\"
+
+AM_CXXFLAGS = $(KEA_CXXFLAGS)
+
+if USE_STATIC_LINK
+AM_LDFLAGS = -static
+endif
+
+# Build shared libraries for testing. The libtool way to create a shared library
+# is to specify "-avoid-version -export-dynamic -module" in the library LDFLAGS
+# (see http://www.gnu.org/software/libtool/manual/html_node/Link-mode.html).
+# Use of these switches will guarantee that the .so files are created in the
+# .libs folder and they can be dlopened.
+# Note that the shared libraries with callouts should not be used together with
+# the --enable-static-link option. With this option, the bind10 libraries are
+# statically linked with the program and if the callout invokes the methods
+# which belong to these libraries, the library with the callout will get its
+# own copy of the static objects (e.g. logger, ServerHooks) and that will lead
+# to unexpected errors. For this reason, the --enable-static-link option is
+# ignored for unit tests built here.
+
+# -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 -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 -rpath /nowhere
+
+libco3_la_SOURCES = callout_library_3.cc callout_library_common.h
+libco3_la_CXXFLAGS = $(AM_CXXFLAGS)
+libco3_la_CPPFLAGS = $(AM_CPPFLAGS)
+libco3_la_LDFLAGS = -avoid-version -export-dynamic -module -rpath /nowhere
+
+# Don't install test libraries.
+noinst_LTLIBRARIES = libco1.la libco2.la libco3.la
+
+if HAVE_GTEST
+
+# C++ tests
+PROGRAM_TESTS = dhcp4_unittests
+
+dhcp4_unittests_SOURCES = d2_unittest.h d2_unittest.cc
+dhcp4_unittests_SOURCES += dhcp4_unittests.cc
+dhcp4_unittests_SOURCES += dhcp4_srv_unittest.cc
+dhcp4_unittests_SOURCES += dhcp4_test_utils.cc dhcp4_test_utils.h
+dhcp4_unittests_SOURCES += direct_client_unittest.cc
+dhcp4_unittests_SOURCES += ctrl_dhcp4_srv_unittest.cc
+dhcp4_unittests_SOURCES += classify_unittest.cc
+dhcp4_unittests_SOURCES += config_backend_unittest.cc
+dhcp4_unittests_SOURCES += config_parser_unittest.cc
+dhcp4_unittests_SOURCES += fqdn_unittest.cc
+dhcp4_unittests_SOURCES += marker_file.cc
+dhcp4_unittests_SOURCES += dhcp4_client.cc dhcp4_client.h
+dhcp4_unittests_SOURCES += hooks_unittest.cc
+dhcp4_unittests_SOURCES += inform_unittest.cc
+dhcp4_unittests_SOURCES += dora_unittest.cc
+dhcp4_unittests_SOURCES += host_options_unittest.cc
+dhcp4_unittests_SOURCES += release_unittest.cc
+dhcp4_unittests_SOURCES += parser_unittest.cc
+dhcp4_unittests_SOURCES += out_of_range_unittest.cc
+dhcp4_unittests_SOURCES += decline_unittest.cc
+dhcp4_unittests_SOURCES += kea_controller_unittest.cc
+dhcp4_unittests_SOURCES += dhcp4to6_ipc_unittest.cc
+dhcp4_unittests_SOURCES += simple_parser4_unittest.cc
+dhcp4_unittests_SOURCES += get_config_unittest.cc get_config_unittest.h
+dhcp4_unittests_SOURCES += shared_network_unittest.cc
+dhcp4_unittests_SOURCES += host_unittest.cc
+dhcp4_unittests_SOURCES += vendor_opts_unittest.cc
+dhcp4_unittests_SOURCES += client_handler_unittest.cc
+
+nodist_dhcp4_unittests_SOURCES = marker_file.h test_libraries.h
+
+dhcp4_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
+dhcp4_unittests_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS)
+if HAVE_MYSQL
+dhcp4_unittests_LDFLAGS += $(MYSQL_LIBS)
+endif
+if HAVE_PGSQL
+dhcp4_unittests_LDFLAGS += $(PGSQL_LIBS)
+endif
+dhcp4_unittests_LDFLAGS += $(GTEST_LDFLAGS)
+
+dhcp4_unittests_LDADD = $(top_builddir)/src/bin/dhcp4/libdhcp4.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/testutils/libdhcpsrvtest.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/process/libkea-process.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/eval/libkea-eval.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/stats/libkea-stats.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/config/libkea-cfgclient.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/http/libkea-http.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/dhcp/tests/libdhcptest.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la
+
+if HAVE_PGSQL
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/pgsql/testutils/libpgsqltest.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/pgsql/libkea-pgsql.la
+endif
+
+if HAVE_MYSQL
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/mysql/testutils/libmysqltest.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/mysql/libkea-mysql.la
+endif
+
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/database/testutils/libdatabasetest.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/database/libkea-database.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/testutils/libkea-testutils.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/cc/libkea-cc.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/dns/libkea-dns++.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/log/libkea-log.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/util/libkea-util.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
+dhcp4_unittests_LDADD += $(LOG4CPLUS_LIBS) $(CRYPTO_LIBS)
+dhcp4_unittests_LDADD += $(BOOST_LIBS) $(GTEST_LDADD)
+
+# Run C++ tests on "make check".
+TESTS = $(PROGRAM_TESTS)
+
+# Run shell tests on "make check".
+check_SCRIPTS = $(SHTESTS)
+TESTS += $(SHTESTS)
+
+# Don't install C++ tests.
+noinst_PROGRAMS = $(PROGRAM_TESTS)
+
+# Use this target if you want to rebuild the get-config unit-tests.
+#
+# TODO: We could also automate the replacement step with some variation
+# of this: https://stackoverflow.com/questions/6790631
+rebuild-tests:
+ rm -f x u get_config_unittest.cc
+ cp -f get_config_unittest.cc.skel get_config_unittest.cc
+ $(MAKE) CXXFLAGS=-DEXTRACT_CONFIG V=1
+ ./dhcp4_unittests --gtest_filter="Dhcp4Parser*" > /dev/null 2> x
+ echo "Please copy content of x file into EXTRACTED_CONFIGS in get_config_unittest.cc"
+ read -p "Press ENTER when ready"
+ $(MAKE) CXXFLAGS=-DGENERATE_ACTION V=1
+ ./dhcp4_unittests --gtest_filter="Dhcp4GetConfig*" > /dev/null 2> u
+ echo "Please copy content of u file into UNPARSED_CONFIGS in get_config_unittest.cc"
+ read -p "Press ENTER when ready"
+ touch get_config_unittest.cc
+ $(MAKE)
+
+endif
+
+# Don't install shell tests.
+noinst_SCRIPTS = $(SHTESTS)
diff --git a/src/bin/dhcp4/tests/Makefile.in b/src/bin/dhcp4/tests/Makefile.in
new file mode 100644
index 0000000..918135a
--- /dev/null
+++ b/src/bin/dhcp4/tests/Makefile.in
@@ -0,0 +1,1758 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+@HAVE_GTEST_TRUE@@HAVE_MYSQL_TRUE@am__append_1 = $(MYSQL_LIBS)
+@HAVE_GTEST_TRUE@@HAVE_PGSQL_TRUE@am__append_2 = $(PGSQL_LIBS)
+@HAVE_GTEST_TRUE@@HAVE_PGSQL_TRUE@am__append_3 = $(top_builddir)/src/lib/pgsql/testutils/libpgsqltest.la \
+@HAVE_GTEST_TRUE@@HAVE_PGSQL_TRUE@ $(top_builddir)/src/lib/pgsql/libkea-pgsql.la
+@HAVE_GTEST_TRUE@@HAVE_MYSQL_TRUE@am__append_4 = $(top_builddir)/src/lib/mysql/testutils/libmysqltest.la \
+@HAVE_GTEST_TRUE@@HAVE_MYSQL_TRUE@ $(top_builddir)/src/lib/mysql/libkea-mysql.la
+@HAVE_GTEST_TRUE@TESTS = $(am__EXEEXT_1) $(SHTESTS)
+@HAVE_GTEST_TRUE@noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/bin/dhcp4/tests
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4macros/ax_boost_for_kea.m4 \
+ $(top_srcdir)/m4macros/ax_cpp11.m4 \
+ $(top_srcdir)/m4macros/ax_cpp20.m4 \
+ $(top_srcdir)/m4macros/ax_crypto.m4 \
+ $(top_srcdir)/m4macros/ax_find_library.m4 \
+ $(top_srcdir)/m4macros/ax_gssapi.m4 \
+ $(top_srcdir)/m4macros/ax_gtest.m4 \
+ $(top_srcdir)/m4macros/ax_isc_rpath.m4 \
+ $(top_srcdir)/m4macros/ax_netconf.m4 \
+ $(top_srcdir)/m4macros/libtool.m4 \
+ $(top_srcdir)/m4macros/ltoptions.m4 \
+ $(top_srcdir)/m4macros/ltsugar.m4 \
+ $(top_srcdir)/m4macros/ltversion.m4 \
+ $(top_srcdir)/m4macros/lt~obsolete.m4 \
+ $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES = dhcp4_process_tests.sh marker_file.h \
+ test_data_files_config.h test_libraries.h
+CONFIG_CLEAN_VPATH_FILES =
+@HAVE_GTEST_TRUE@am__EXEEXT_1 = dhcp4_unittests$(EXEEXT)
+PROGRAMS = $(noinst_PROGRAMS)
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libco1_la_LIBADD =
+am_libco1_la_OBJECTS = libco1_la-callout_library_1.lo
+libco1_la_OBJECTS = $(am_libco1_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+libco1_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CXXLD) $(libco1_la_CXXFLAGS) \
+ $(CXXFLAGS) $(libco1_la_LDFLAGS) $(LDFLAGS) -o $@
+libco2_la_LIBADD =
+am_libco2_la_OBJECTS = libco2_la-callout_library_2.lo
+libco2_la_OBJECTS = $(am_libco2_la_OBJECTS)
+libco2_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CXXLD) $(libco2_la_CXXFLAGS) \
+ $(CXXFLAGS) $(libco2_la_LDFLAGS) $(LDFLAGS) -o $@
+libco3_la_LIBADD =
+am_libco3_la_OBJECTS = libco3_la-callout_library_3.lo
+libco3_la_OBJECTS = $(am_libco3_la_OBJECTS)
+libco3_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CXXLD) $(libco3_la_CXXFLAGS) \
+ $(CXXFLAGS) $(libco3_la_LDFLAGS) $(LDFLAGS) -o $@
+am__dhcp4_unittests_SOURCES_DIST = d2_unittest.h d2_unittest.cc \
+ dhcp4_unittests.cc dhcp4_srv_unittest.cc dhcp4_test_utils.cc \
+ dhcp4_test_utils.h direct_client_unittest.cc \
+ ctrl_dhcp4_srv_unittest.cc classify_unittest.cc \
+ config_backend_unittest.cc config_parser_unittest.cc \
+ fqdn_unittest.cc marker_file.cc dhcp4_client.cc dhcp4_client.h \
+ hooks_unittest.cc inform_unittest.cc dora_unittest.cc \
+ host_options_unittest.cc release_unittest.cc \
+ parser_unittest.cc out_of_range_unittest.cc \
+ decline_unittest.cc kea_controller_unittest.cc \
+ dhcp4to6_ipc_unittest.cc simple_parser4_unittest.cc \
+ get_config_unittest.cc get_config_unittest.h \
+ shared_network_unittest.cc host_unittest.cc \
+ vendor_opts_unittest.cc client_handler_unittest.cc
+@HAVE_GTEST_TRUE@am_dhcp4_unittests_OBJECTS = \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-d2_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-dhcp4_unittests.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-dhcp4_srv_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-dhcp4_test_utils.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-direct_client_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-ctrl_dhcp4_srv_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-classify_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-config_backend_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-config_parser_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-fqdn_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-marker_file.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-dhcp4_client.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-hooks_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-inform_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-dora_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-host_options_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-release_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-parser_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-out_of_range_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-decline_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-kea_controller_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-dhcp4to6_ipc_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-simple_parser4_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-get_config_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-shared_network_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-host_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-vendor_opts_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ dhcp4_unittests-client_handler_unittest.$(OBJEXT)
+nodist_dhcp4_unittests_OBJECTS =
+dhcp4_unittests_OBJECTS = $(am_dhcp4_unittests_OBJECTS) \
+ $(nodist_dhcp4_unittests_OBJECTS)
+am__DEPENDENCIES_1 =
+@HAVE_GTEST_TRUE@dhcp4_unittests_DEPENDENCIES = \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/bin/dhcp4/libdhcp4.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/dhcpsrv/testutils/libdhcpsrvtest.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/process/libkea-process.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/eval/libkea-eval.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/stats/libkea-stats.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/config/libkea-cfgclient.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/http/libkea-http.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/dhcp/tests/libdhcptest.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/hooks/libkea-hooks.la \
+@HAVE_GTEST_TRUE@ $(am__append_3) $(am__append_4) \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/database/testutils/libdatabasetest.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/database/libkea-database.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/testutils/libkea-testutils.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/cc/libkea-cc.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/asiolink/libkea-asiolink.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/dns/libkea-dns++.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/log/libkea-log.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/util/libkea-util.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/exceptions/libkea-exceptions.la \
+@HAVE_GTEST_TRUE@ $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_1) \
+@HAVE_GTEST_TRUE@ $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_1)
+dhcp4_unittests_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CXXLD) \
+ $(AM_CXXFLAGS) $(CXXFLAGS) $(dhcp4_unittests_LDFLAGS) \
+ $(LDFLAGS) -o $@
+SCRIPTS = $(noinst_SCRIPTS)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = \
+ ./$(DEPDIR)/dhcp4_unittests-classify_unittest.Po \
+ ./$(DEPDIR)/dhcp4_unittests-client_handler_unittest.Po \
+ ./$(DEPDIR)/dhcp4_unittests-config_backend_unittest.Po \
+ ./$(DEPDIR)/dhcp4_unittests-config_parser_unittest.Po \
+ ./$(DEPDIR)/dhcp4_unittests-ctrl_dhcp4_srv_unittest.Po \
+ ./$(DEPDIR)/dhcp4_unittests-d2_unittest.Po \
+ ./$(DEPDIR)/dhcp4_unittests-decline_unittest.Po \
+ ./$(DEPDIR)/dhcp4_unittests-dhcp4_client.Po \
+ ./$(DEPDIR)/dhcp4_unittests-dhcp4_srv_unittest.Po \
+ ./$(DEPDIR)/dhcp4_unittests-dhcp4_test_utils.Po \
+ ./$(DEPDIR)/dhcp4_unittests-dhcp4_unittests.Po \
+ ./$(DEPDIR)/dhcp4_unittests-dhcp4to6_ipc_unittest.Po \
+ ./$(DEPDIR)/dhcp4_unittests-direct_client_unittest.Po \
+ ./$(DEPDIR)/dhcp4_unittests-dora_unittest.Po \
+ ./$(DEPDIR)/dhcp4_unittests-fqdn_unittest.Po \
+ ./$(DEPDIR)/dhcp4_unittests-get_config_unittest.Po \
+ ./$(DEPDIR)/dhcp4_unittests-hooks_unittest.Po \
+ ./$(DEPDIR)/dhcp4_unittests-host_options_unittest.Po \
+ ./$(DEPDIR)/dhcp4_unittests-host_unittest.Po \
+ ./$(DEPDIR)/dhcp4_unittests-inform_unittest.Po \
+ ./$(DEPDIR)/dhcp4_unittests-kea_controller_unittest.Po \
+ ./$(DEPDIR)/dhcp4_unittests-marker_file.Po \
+ ./$(DEPDIR)/dhcp4_unittests-out_of_range_unittest.Po \
+ ./$(DEPDIR)/dhcp4_unittests-parser_unittest.Po \
+ ./$(DEPDIR)/dhcp4_unittests-release_unittest.Po \
+ ./$(DEPDIR)/dhcp4_unittests-shared_network_unittest.Po \
+ ./$(DEPDIR)/dhcp4_unittests-simple_parser4_unittest.Po \
+ ./$(DEPDIR)/dhcp4_unittests-vendor_opts_unittest.Po \
+ ./$(DEPDIR)/libco1_la-callout_library_1.Plo \
+ ./$(DEPDIR)/libco2_la-callout_library_2.Plo \
+ ./$(DEPDIR)/libco3_la-callout_library_3.Plo
+am__mv = mv -f
+CXXCOMPILE = $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) \
+ $(AM_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS)
+LTCXXCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CXXFLAGS) $(CXXFLAGS)
+AM_V_CXX = $(am__v_CXX_@AM_V@)
+am__v_CXX_ = $(am__v_CXX_@AM_DEFAULT_V@)
+am__v_CXX_0 = @echo " CXX " $@;
+am__v_CXX_1 =
+CXXLD = $(CXX)
+CXXLINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CXXLD) $(AM_CXXFLAGS) \
+ $(CXXFLAGS) $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CXXLD = $(am__v_CXXLD_@AM_V@)
+am__v_CXXLD_ = $(am__v_CXXLD_@AM_DEFAULT_V@)
+am__v_CXXLD_0 = @echo " CXXLD " $@;
+am__v_CXXLD_1 =
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libco1_la_SOURCES) $(libco2_la_SOURCES) \
+ $(libco3_la_SOURCES) $(dhcp4_unittests_SOURCES) \
+ $(nodist_dhcp4_unittests_SOURCES)
+DIST_SOURCES = $(libco1_la_SOURCES) $(libco2_la_SOURCES) \
+ $(libco3_la_SOURCES) $(am__dhcp4_unittests_SOURCES_DIST)
+RECURSIVE_TARGETS = all-recursive check-recursive cscopelist-recursive \
+ ctags-recursive dvi-recursive html-recursive info-recursive \
+ install-data-recursive install-dvi-recursive \
+ install-exec-recursive install-html-recursive \
+ install-info-recursive install-pdf-recursive \
+ install-ps-recursive install-recursive installcheck-recursive \
+ installdirs-recursive pdf-recursive ps-recursive \
+ tags-recursive uninstall-recursive
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+RECURSIVE_CLEAN_TARGETS = mostlyclean-recursive clean-recursive \
+ distclean-recursive maintainer-clean-recursive
+am__recursive_targets = \
+ $(RECURSIVE_TARGETS) \
+ $(RECURSIVE_CLEAN_TARGETS) \
+ $(am__extra_recursive_targets)
+AM_RECURSIVE_TARGETS = $(am__recursive_targets:-recursive=) TAGS CTAGS \
+ distdir distdir-am
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__tty_colors_dummy = \
+ mgn= red= grn= lgn= blu= brg= std=; \
+ am__color_tests=no
+am__tty_colors = { \
+ $(am__tty_colors_dummy); \
+ if test "X$(AM_COLOR_TESTS)" = Xno; then \
+ am__color_tests=no; \
+ elif test "X$(AM_COLOR_TESTS)" = Xalways; then \
+ am__color_tests=yes; \
+ elif test "X$$TERM" != Xdumb && { test -t 1; } 2>/dev/null; then \
+ am__color_tests=yes; \
+ fi; \
+ if test $$am__color_tests = yes; then \
+ red=''; \
+ grn=''; \
+ lgn=''; \
+ blu=''; \
+ mgn=''; \
+ brg=''; \
+ std=''; \
+ fi; \
+}
+DIST_SUBDIRS = $(SUBDIRS)
+am__DIST_COMMON = $(srcdir)/Makefile.in \
+ $(srcdir)/dhcp4_process_tests.sh.in $(srcdir)/marker_file.h.in \
+ $(srcdir)/test_data_files_config.h.in \
+ $(srcdir)/test_libraries.h.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+am__relativize = \
+ dir0=`pwd`; \
+ sed_first='s,^\([^/]*\)/.*$$,\1,'; \
+ sed_rest='s,^[^/]*/*,,'; \
+ sed_last='s,^.*/\([^/]*\)$$,\1,'; \
+ sed_butlast='s,/*[^/]*$$,,'; \
+ while test -n "$$dir1"; do \
+ first=`echo "$$dir1" | sed -e "$$sed_first"`; \
+ if test "$$first" != "."; then \
+ if test "$$first" = ".."; then \
+ dir2=`echo "$$dir0" | sed -e "$$sed_last"`/"$$dir2"; \
+ dir0=`echo "$$dir0" | sed -e "$$sed_butlast"`; \
+ else \
+ first2=`echo "$$dir2" | sed -e "$$sed_first"`; \
+ if test "$$first2" = "$$first"; then \
+ dir2=`echo "$$dir2" | sed -e "$$sed_rest"`; \
+ else \
+ dir2="../$$dir2"; \
+ fi; \
+ dir0="$$dir0"/"$$first"; \
+ fi; \
+ fi; \
+ dir1=`echo "$$dir1" | sed -e "$$sed_rest"`; \
+ done; \
+ reldir="$$dir2"
+ACLOCAL = @ACLOCAL@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+AR = @AR@
+ASCIIDOC = @ASCIIDOC@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BOOST_INCLUDES = @BOOST_INCLUDES@
+BOOST_LIBS = @BOOST_LIBS@
+BOTAN_TOOL = @BOTAN_TOOL@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CFLAGS = @CFLAGS@
+CONTRIB_DIR = @CONTRIB_DIR@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPTO_CFLAGS = @CRYPTO_CFLAGS@
+CRYPTO_INCLUDES = @CRYPTO_INCLUDES@
+CRYPTO_LDFLAGS = @CRYPTO_LDFLAGS@
+CRYPTO_LIBS = @CRYPTO_LIBS@
+CRYPTO_PACKAGE = @CRYPTO_PACKAGE@
+CRYPTO_RPATH = @CRYPTO_RPATH@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DISTCHECK_BOOST_CONFIGURE_FLAG = @DISTCHECK_BOOST_CONFIGURE_FLAG@
+DISTCHECK_CONTRIB_CONFIGURE_FLAG = @DISTCHECK_CONTRIB_CONFIGURE_FLAG@
+DISTCHECK_CRYPTO_CONFIGURE_FLAG = @DISTCHECK_CRYPTO_CONFIGURE_FLAG@
+DISTCHECK_GSSAPI_CONFIGURE_FLAG = @DISTCHECK_GSSAPI_CONFIGURE_FLAG@
+DISTCHECK_GTEST_CONFIGURE_FLAG = @DISTCHECK_GTEST_CONFIGURE_FLAG@
+DISTCHECK_KEA_SHELL_CONFIGURE_FLAG = @DISTCHECK_KEA_SHELL_CONFIGURE_FLAG@
+DISTCHECK_LIBYANGCPP_CONFIGURE_FLAG = @DISTCHECK_LIBYANGCPP_CONFIGURE_FLAG@
+DISTCHECK_LIBYANG_CONFIGURE_FLAG = @DISTCHECK_LIBYANG_CONFIGURE_FLAG@
+DISTCHECK_LOG4CPLUS_CONFIGURE_FLAG = @DISTCHECK_LOG4CPLUS_CONFIGURE_FLAG@
+DISTCHECK_MYSQL_CONFIGURE_FLAG = @DISTCHECK_MYSQL_CONFIGURE_FLAG@
+DISTCHECK_PERFDHCP_CONFIGURE_FLAG = @DISTCHECK_PERFDHCP_CONFIGURE_FLAG@
+DISTCHECK_PGSQL_CONFIGURE_FLAG = @DISTCHECK_PGSQL_CONFIGURE_FLAG@
+DISTCHECK_PREMIUM_CONFIGURE_FLAG = @DISTCHECK_PREMIUM_CONFIGURE_FLAG@
+DISTCHECK_SYSREPOCPP_CONFIGURE_FLAG = @DISTCHECK_SYSREPOCPP_CONFIGURE_FLAG@
+DISTCHECK_SYSREPO_CONFIGURE_FLAG = @DISTCHECK_SYSREPO_CONFIGURE_FLAG@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+GENHTML = @GENHTML@
+GREP = @GREP@
+GSSAPI_CFLAGS = @GSSAPI_CFLAGS@
+GSSAPI_LIBS = @GSSAPI_LIBS@
+GTEST_CONFIG = @GTEST_CONFIG@
+GTEST_INCLUDES = @GTEST_INCLUDES@
+GTEST_LDADD = @GTEST_LDADD@
+GTEST_LDFLAGS = @GTEST_LDFLAGS@
+GTEST_SOURCE = @GTEST_SOURCE@
+HAVE_NETCONF = @HAVE_NETCONF@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KEA_CXXFLAGS = @KEA_CXXFLAGS@
+KEA_SRCID = @KEA_SRCID@
+KRB5_CONFIG = @KRB5_CONFIG@
+LCOV = @LCOV@
+LD = @LD@
+LDFLAGS = @LDFLAGS@
+LEX = @LEX@
+LEXLIB = @LEXLIB@
+LEX_OUTPUT_ROOT = @LEX_OUTPUT_ROOT@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBTOOL = @LIBTOOL@
+LIBYANGCPP_CPPFLAGS = @LIBYANGCPP_CPPFLAGS@
+LIBYANGCPP_INCLUDEDIR = @LIBYANGCPP_INCLUDEDIR@
+LIBYANGCPP_LIBS = @LIBYANGCPP_LIBS@
+LIBYANGCPP_PREFIX = @LIBYANGCPP_PREFIX@
+LIBYANGCPP_VERSION = @LIBYANGCPP_VERSION@
+LIBYANG_CPPFLAGS = @LIBYANG_CPPFLAGS@
+LIBYANG_INCLUDEDIR = @LIBYANG_INCLUDEDIR@
+LIBYANG_LIBS = @LIBYANG_LIBS@
+LIBYANG_PREFIX = @LIBYANG_PREFIX@
+LIBYANG_VERSION = @LIBYANG_VERSION@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LOG4CPLUS_INCLUDES = @LOG4CPLUS_INCLUDES@
+LOG4CPLUS_LIBS = @LOG4CPLUS_LIBS@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MYSQL_CPPFLAGS = @MYSQL_CPPFLAGS@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PACKAGE_VERSION_TYPE = @PACKAGE_VERSION_TYPE@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PDFLATEX = @PDFLATEX@
+PERL = @PERL@
+PGSQL_CPPFLAGS = @PGSQL_CPPFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PKGPYTHONDIR = @PKGPYTHONDIR@
+PKG_CONFIG = @PKG_CONFIG@
+PLANTUML = @PLANTUML@
+PREMIUM_DIR = @PREMIUM_DIR@
+PYTHON = @PYTHON@
+PYTHON_EXEC_PREFIX = @PYTHON_EXEC_PREFIX@
+PYTHON_PLATFORM = @PYTHON_PLATFORM@
+PYTHON_PREFIX = @PYTHON_PREFIX@
+PYTHON_VERSION = @PYTHON_VERSION@
+RANLIB = @RANLIB@
+SED = @SED@
+SEP = @SEP@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SPHINXBUILD = @SPHINXBUILD@
+SRPD_PLUGINS_PATH = @SRPD_PLUGINS_PATH@
+SR_PLUGINS_PATH = @SR_PLUGINS_PATH@
+SR_REPO_PATH = @SR_REPO_PATH@
+STRIP = @STRIP@
+SYSREPOCPP_CPPFLAGS = @SYSREPOCPP_CPPFLAGS@
+SYSREPOCPP_INCLUDEDIR = @SYSREPOCPP_INCLUDEDIR@
+SYSREPOCPP_LIBS = @SYSREPOCPP_LIBS@
+SYSREPOCPP_PREFIX = @SYSREPOCPP_PREFIX@
+SYSREPOCPP_VERSION = @SYSREPOCPP_VERSION@
+SYSREPO_CPPFLAGS = @SYSREPO_CPPFLAGS@
+SYSREPO_INCLUDEDIR = @SYSREPO_INCLUDEDIR@
+SYSREPO_LIBS = @SYSREPO_LIBS@
+SYSREPO_PREFIX = @SYSREPO_PREFIX@
+SYSREPO_VERSION = @SYSREPO_VERSION@
+USE_LCOV = @USE_LCOV@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+WARNING_GCC_44_STRICT_ALIASING_CFLAG = @WARNING_GCC_44_STRICT_ALIASING_CFLAG@
+YACC = @YACC@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+pkgpyexecdir = @pkgpyexecdir@
+pkgpythondir = @pkgpythondir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+pyexecdir = @pyexecdir@
+pythondir = @pythondir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+srcdir = @srcdir@
+sysconfdir = @sysconfdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+SUBDIRS = .
+
+# Add to the tarball:
+EXTRA_DIST = get_config_unittest.cc.skel
+TESTS_ENVIRONMENT = $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND)
+
+# Shell tests
+SHTESTS = dhcp4_process_tests.sh
+
+# As with every file generated by ./configure, clean them up when running
+# "make distclean", but not on "make clean".
+DISTCLEANFILES = $(SHTESTS) marker_file.h test_data_files_config.h \
+ test_libraries.h
+AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib \
+ -I$(top_srcdir)/src -I$(top_builddir)/src \
+ -I$(top_srcdir)/src/bin -I$(top_builddir)/src/bin \
+ $(BOOST_INCLUDES) \
+ -DTEST_DATA_BUILDDIR=\"$(abs_top_builddir)/src/bin/dhcp4/tests\" \
+ -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\" \
+ -DCFG_EXAMPLES=\"$(abs_top_srcdir)/doc/examples/kea4\" \
+ -DSYNTAX_FILE=\"$(abs_srcdir)/../dhcp4_parser.yy\" \
+ -DKEA_LFC_EXECUTABLE=\"$(abs_top_builddir)/src/bin/lfc/kea-lfc\"
+AM_CXXFLAGS = $(KEA_CXXFLAGS)
+@USE_STATIC_LINK_TRUE@AM_LDFLAGS = -static
+
+# Build shared libraries for testing. The libtool way to create a shared library
+# is to specify "-avoid-version -export-dynamic -module" in the library LDFLAGS
+# (see http://www.gnu.org/software/libtool/manual/html_node/Link-mode.html).
+# Use of these switches will guarantee that the .so files are created in the
+# .libs folder and they can be dlopened.
+# Note that the shared libraries with callouts should not be used together with
+# the --enable-static-link option. With this option, the bind10 libraries are
+# statically linked with the program and if the callout invokes the methods
+# which belong to these libraries, the library with the callout will get its
+# own copy of the static objects (e.g. logger, ServerHooks) and that will lead
+# to unexpected errors. For this reason, the --enable-static-link option is
+# ignored for unit tests built here.
+
+# -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 -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 -rpath /nowhere
+libco3_la_SOURCES = callout_library_3.cc callout_library_common.h
+libco3_la_CXXFLAGS = $(AM_CXXFLAGS)
+libco3_la_CPPFLAGS = $(AM_CPPFLAGS)
+libco3_la_LDFLAGS = -avoid-version -export-dynamic -module -rpath /nowhere
+
+# Don't install test libraries.
+noinst_LTLIBRARIES = libco1.la libco2.la libco3.la
+
+# C++ tests
+@HAVE_GTEST_TRUE@PROGRAM_TESTS = dhcp4_unittests
+@HAVE_GTEST_TRUE@dhcp4_unittests_SOURCES = d2_unittest.h \
+@HAVE_GTEST_TRUE@ d2_unittest.cc dhcp4_unittests.cc \
+@HAVE_GTEST_TRUE@ dhcp4_srv_unittest.cc dhcp4_test_utils.cc \
+@HAVE_GTEST_TRUE@ dhcp4_test_utils.h direct_client_unittest.cc \
+@HAVE_GTEST_TRUE@ ctrl_dhcp4_srv_unittest.cc \
+@HAVE_GTEST_TRUE@ classify_unittest.cc \
+@HAVE_GTEST_TRUE@ config_backend_unittest.cc \
+@HAVE_GTEST_TRUE@ config_parser_unittest.cc fqdn_unittest.cc \
+@HAVE_GTEST_TRUE@ marker_file.cc dhcp4_client.cc dhcp4_client.h \
+@HAVE_GTEST_TRUE@ hooks_unittest.cc inform_unittest.cc \
+@HAVE_GTEST_TRUE@ dora_unittest.cc host_options_unittest.cc \
+@HAVE_GTEST_TRUE@ release_unittest.cc parser_unittest.cc \
+@HAVE_GTEST_TRUE@ out_of_range_unittest.cc decline_unittest.cc \
+@HAVE_GTEST_TRUE@ kea_controller_unittest.cc \
+@HAVE_GTEST_TRUE@ dhcp4to6_ipc_unittest.cc \
+@HAVE_GTEST_TRUE@ simple_parser4_unittest.cc \
+@HAVE_GTEST_TRUE@ get_config_unittest.cc get_config_unittest.h \
+@HAVE_GTEST_TRUE@ shared_network_unittest.cc host_unittest.cc \
+@HAVE_GTEST_TRUE@ vendor_opts_unittest.cc \
+@HAVE_GTEST_TRUE@ client_handler_unittest.cc
+@HAVE_GTEST_TRUE@nodist_dhcp4_unittests_SOURCES = marker_file.h test_libraries.h
+@HAVE_GTEST_TRUE@dhcp4_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
+@HAVE_GTEST_TRUE@dhcp4_unittests_LDFLAGS = $(AM_LDFLAGS) \
+@HAVE_GTEST_TRUE@ $(CRYPTO_LDFLAGS) $(am__append_1) \
+@HAVE_GTEST_TRUE@ $(am__append_2) $(GTEST_LDFLAGS)
+@HAVE_GTEST_TRUE@dhcp4_unittests_LDADD = \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/bin/dhcp4/libdhcp4.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/dhcpsrv/testutils/libdhcpsrvtest.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/process/libkea-process.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/eval/libkea-eval.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/stats/libkea-stats.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/config/libkea-cfgclient.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/http/libkea-http.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/dhcp/tests/libdhcptest.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/hooks/libkea-hooks.la \
+@HAVE_GTEST_TRUE@ $(am__append_3) $(am__append_4) \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/database/testutils/libdatabasetest.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/database/libkea-database.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/testutils/libkea-testutils.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/cc/libkea-cc.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/asiolink/libkea-asiolink.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/dns/libkea-dns++.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/log/libkea-log.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/util/libkea-util.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/exceptions/libkea-exceptions.la \
+@HAVE_GTEST_TRUE@ $(LOG4CPLUS_LIBS) $(CRYPTO_LIBS) \
+@HAVE_GTEST_TRUE@ $(BOOST_LIBS) $(GTEST_LDADD)
+
+# Run shell tests on "make check".
+@HAVE_GTEST_TRUE@check_SCRIPTS = $(SHTESTS)
+
+# Don't install shell tests.
+noinst_SCRIPTS = $(SHTESTS)
+all: all-recursive
+
+.SUFFIXES:
+.SUFFIXES: .cc .lo .o .obj
+$(srcdir)/Makefile.in: $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/bin/dhcp4/tests/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/bin/dhcp4/tests/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+dhcp4_process_tests.sh: $(top_builddir)/config.status $(srcdir)/dhcp4_process_tests.sh.in
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@
+marker_file.h: $(top_builddir)/config.status $(srcdir)/marker_file.h.in
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@
+test_data_files_config.h: $(top_builddir)/config.status $(srcdir)/test_data_files_config.h.in
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@
+test_libraries.h: $(top_builddir)/config.status $(srcdir)/test_libraries.h.in
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libco1.la: $(libco1_la_OBJECTS) $(libco1_la_DEPENDENCIES) $(EXTRA_libco1_la_DEPENDENCIES)
+ $(AM_V_CXXLD)$(libco1_la_LINK) $(libco1_la_OBJECTS) $(libco1_la_LIBADD) $(LIBS)
+
+libco2.la: $(libco2_la_OBJECTS) $(libco2_la_DEPENDENCIES) $(EXTRA_libco2_la_DEPENDENCIES)
+ $(AM_V_CXXLD)$(libco2_la_LINK) $(libco2_la_OBJECTS) $(libco2_la_LIBADD) $(LIBS)
+
+libco3.la: $(libco3_la_OBJECTS) $(libco3_la_DEPENDENCIES) $(EXTRA_libco3_la_DEPENDENCIES)
+ $(AM_V_CXXLD)$(libco3_la_LINK) $(libco3_la_OBJECTS) $(libco3_la_LIBADD) $(LIBS)
+
+dhcp4_unittests$(EXEEXT): $(dhcp4_unittests_OBJECTS) $(dhcp4_unittests_DEPENDENCIES) $(EXTRA_dhcp4_unittests_DEPENDENCIES)
+ @rm -f dhcp4_unittests$(EXEEXT)
+ $(AM_V_CXXLD)$(dhcp4_unittests_LINK) $(dhcp4_unittests_OBJECTS) $(dhcp4_unittests_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-classify_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-client_handler_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-config_backend_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-config_parser_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-ctrl_dhcp4_srv_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-d2_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-decline_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-dhcp4_client.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-dhcp4_srv_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-dhcp4_test_utils.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-dhcp4_unittests.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-dhcp4to6_ipc_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-direct_client_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-dora_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-fqdn_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-get_config_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-hooks_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-host_options_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-host_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-inform_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-kea_controller_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-marker_file.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-out_of_range_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-parser_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-release_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-shared_network_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-simple_parser4_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dhcp4_unittests-vendor_opts_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libco1_la-callout_library_1.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libco2_la-callout_library_2.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libco3_la-callout_library_3.Plo@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.cc.o:
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXXCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXXCOMPILE) -c -o $@ $<
+
+.cc.obj:
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXXCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXXCOMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.cc.lo:
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LTCXXCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LTCXXCOMPILE) -c -o $@ $<
+
+libco1_la-callout_library_1.lo: callout_library_1.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libco1_la_CPPFLAGS) $(CPPFLAGS) $(libco1_la_CXXFLAGS) $(CXXFLAGS) -MT libco1_la-callout_library_1.lo -MD -MP -MF $(DEPDIR)/libco1_la-callout_library_1.Tpo -c -o libco1_la-callout_library_1.lo `test -f 'callout_library_1.cc' || echo '$(srcdir)/'`callout_library_1.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libco1_la-callout_library_1.Tpo $(DEPDIR)/libco1_la-callout_library_1.Plo
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='callout_library_1.cc' object='libco1_la-callout_library_1.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libco1_la_CPPFLAGS) $(CPPFLAGS) $(libco1_la_CXXFLAGS) $(CXXFLAGS) -c -o libco1_la-callout_library_1.lo `test -f 'callout_library_1.cc' || echo '$(srcdir)/'`callout_library_1.cc
+
+libco2_la-callout_library_2.lo: callout_library_2.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libco2_la_CPPFLAGS) $(CPPFLAGS) $(libco2_la_CXXFLAGS) $(CXXFLAGS) -MT libco2_la-callout_library_2.lo -MD -MP -MF $(DEPDIR)/libco2_la-callout_library_2.Tpo -c -o libco2_la-callout_library_2.lo `test -f 'callout_library_2.cc' || echo '$(srcdir)/'`callout_library_2.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libco2_la-callout_library_2.Tpo $(DEPDIR)/libco2_la-callout_library_2.Plo
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='callout_library_2.cc' object='libco2_la-callout_library_2.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libco2_la_CPPFLAGS) $(CPPFLAGS) $(libco2_la_CXXFLAGS) $(CXXFLAGS) -c -o libco2_la-callout_library_2.lo `test -f 'callout_library_2.cc' || echo '$(srcdir)/'`callout_library_2.cc
+
+libco3_la-callout_library_3.lo: callout_library_3.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libco3_la_CPPFLAGS) $(CPPFLAGS) $(libco3_la_CXXFLAGS) $(CXXFLAGS) -MT libco3_la-callout_library_3.lo -MD -MP -MF $(DEPDIR)/libco3_la-callout_library_3.Tpo -c -o libco3_la-callout_library_3.lo `test -f 'callout_library_3.cc' || echo '$(srcdir)/'`callout_library_3.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libco3_la-callout_library_3.Tpo $(DEPDIR)/libco3_la-callout_library_3.Plo
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='callout_library_3.cc' object='libco3_la-callout_library_3.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libco3_la_CPPFLAGS) $(CPPFLAGS) $(libco3_la_CXXFLAGS) $(CXXFLAGS) -c -o libco3_la-callout_library_3.lo `test -f 'callout_library_3.cc' || echo '$(srcdir)/'`callout_library_3.cc
+
+dhcp4_unittests-d2_unittest.o: d2_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-d2_unittest.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-d2_unittest.Tpo -c -o dhcp4_unittests-d2_unittest.o `test -f 'd2_unittest.cc' || echo '$(srcdir)/'`d2_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-d2_unittest.Tpo $(DEPDIR)/dhcp4_unittests-d2_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='d2_unittest.cc' object='dhcp4_unittests-d2_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-d2_unittest.o `test -f 'd2_unittest.cc' || echo '$(srcdir)/'`d2_unittest.cc
+
+dhcp4_unittests-d2_unittest.obj: d2_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-d2_unittest.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-d2_unittest.Tpo -c -o dhcp4_unittests-d2_unittest.obj `if test -f 'd2_unittest.cc'; then $(CYGPATH_W) 'd2_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/d2_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-d2_unittest.Tpo $(DEPDIR)/dhcp4_unittests-d2_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='d2_unittest.cc' object='dhcp4_unittests-d2_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-d2_unittest.obj `if test -f 'd2_unittest.cc'; then $(CYGPATH_W) 'd2_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/d2_unittest.cc'; fi`
+
+dhcp4_unittests-dhcp4_unittests.o: dhcp4_unittests.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-dhcp4_unittests.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-dhcp4_unittests.Tpo -c -o dhcp4_unittests-dhcp4_unittests.o `test -f 'dhcp4_unittests.cc' || echo '$(srcdir)/'`dhcp4_unittests.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-dhcp4_unittests.Tpo $(DEPDIR)/dhcp4_unittests-dhcp4_unittests.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='dhcp4_unittests.cc' object='dhcp4_unittests-dhcp4_unittests.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-dhcp4_unittests.o `test -f 'dhcp4_unittests.cc' || echo '$(srcdir)/'`dhcp4_unittests.cc
+
+dhcp4_unittests-dhcp4_unittests.obj: dhcp4_unittests.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-dhcp4_unittests.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-dhcp4_unittests.Tpo -c -o dhcp4_unittests-dhcp4_unittests.obj `if test -f 'dhcp4_unittests.cc'; then $(CYGPATH_W) 'dhcp4_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/dhcp4_unittests.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-dhcp4_unittests.Tpo $(DEPDIR)/dhcp4_unittests-dhcp4_unittests.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='dhcp4_unittests.cc' object='dhcp4_unittests-dhcp4_unittests.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-dhcp4_unittests.obj `if test -f 'dhcp4_unittests.cc'; then $(CYGPATH_W) 'dhcp4_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/dhcp4_unittests.cc'; fi`
+
+dhcp4_unittests-dhcp4_srv_unittest.o: dhcp4_srv_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-dhcp4_srv_unittest.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-dhcp4_srv_unittest.Tpo -c -o dhcp4_unittests-dhcp4_srv_unittest.o `test -f 'dhcp4_srv_unittest.cc' || echo '$(srcdir)/'`dhcp4_srv_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-dhcp4_srv_unittest.Tpo $(DEPDIR)/dhcp4_unittests-dhcp4_srv_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='dhcp4_srv_unittest.cc' object='dhcp4_unittests-dhcp4_srv_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-dhcp4_srv_unittest.o `test -f 'dhcp4_srv_unittest.cc' || echo '$(srcdir)/'`dhcp4_srv_unittest.cc
+
+dhcp4_unittests-dhcp4_srv_unittest.obj: dhcp4_srv_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-dhcp4_srv_unittest.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-dhcp4_srv_unittest.Tpo -c -o dhcp4_unittests-dhcp4_srv_unittest.obj `if test -f 'dhcp4_srv_unittest.cc'; then $(CYGPATH_W) 'dhcp4_srv_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/dhcp4_srv_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-dhcp4_srv_unittest.Tpo $(DEPDIR)/dhcp4_unittests-dhcp4_srv_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='dhcp4_srv_unittest.cc' object='dhcp4_unittests-dhcp4_srv_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-dhcp4_srv_unittest.obj `if test -f 'dhcp4_srv_unittest.cc'; then $(CYGPATH_W) 'dhcp4_srv_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/dhcp4_srv_unittest.cc'; fi`
+
+dhcp4_unittests-dhcp4_test_utils.o: dhcp4_test_utils.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-dhcp4_test_utils.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-dhcp4_test_utils.Tpo -c -o dhcp4_unittests-dhcp4_test_utils.o `test -f 'dhcp4_test_utils.cc' || echo '$(srcdir)/'`dhcp4_test_utils.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-dhcp4_test_utils.Tpo $(DEPDIR)/dhcp4_unittests-dhcp4_test_utils.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='dhcp4_test_utils.cc' object='dhcp4_unittests-dhcp4_test_utils.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-dhcp4_test_utils.o `test -f 'dhcp4_test_utils.cc' || echo '$(srcdir)/'`dhcp4_test_utils.cc
+
+dhcp4_unittests-dhcp4_test_utils.obj: dhcp4_test_utils.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-dhcp4_test_utils.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-dhcp4_test_utils.Tpo -c -o dhcp4_unittests-dhcp4_test_utils.obj `if test -f 'dhcp4_test_utils.cc'; then $(CYGPATH_W) 'dhcp4_test_utils.cc'; else $(CYGPATH_W) '$(srcdir)/dhcp4_test_utils.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-dhcp4_test_utils.Tpo $(DEPDIR)/dhcp4_unittests-dhcp4_test_utils.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='dhcp4_test_utils.cc' object='dhcp4_unittests-dhcp4_test_utils.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-dhcp4_test_utils.obj `if test -f 'dhcp4_test_utils.cc'; then $(CYGPATH_W) 'dhcp4_test_utils.cc'; else $(CYGPATH_W) '$(srcdir)/dhcp4_test_utils.cc'; fi`
+
+dhcp4_unittests-direct_client_unittest.o: direct_client_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-direct_client_unittest.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-direct_client_unittest.Tpo -c -o dhcp4_unittests-direct_client_unittest.o `test -f 'direct_client_unittest.cc' || echo '$(srcdir)/'`direct_client_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-direct_client_unittest.Tpo $(DEPDIR)/dhcp4_unittests-direct_client_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='direct_client_unittest.cc' object='dhcp4_unittests-direct_client_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-direct_client_unittest.o `test -f 'direct_client_unittest.cc' || echo '$(srcdir)/'`direct_client_unittest.cc
+
+dhcp4_unittests-direct_client_unittest.obj: direct_client_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-direct_client_unittest.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-direct_client_unittest.Tpo -c -o dhcp4_unittests-direct_client_unittest.obj `if test -f 'direct_client_unittest.cc'; then $(CYGPATH_W) 'direct_client_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/direct_client_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-direct_client_unittest.Tpo $(DEPDIR)/dhcp4_unittests-direct_client_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='direct_client_unittest.cc' object='dhcp4_unittests-direct_client_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-direct_client_unittest.obj `if test -f 'direct_client_unittest.cc'; then $(CYGPATH_W) 'direct_client_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/direct_client_unittest.cc'; fi`
+
+dhcp4_unittests-ctrl_dhcp4_srv_unittest.o: ctrl_dhcp4_srv_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-ctrl_dhcp4_srv_unittest.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-ctrl_dhcp4_srv_unittest.Tpo -c -o dhcp4_unittests-ctrl_dhcp4_srv_unittest.o `test -f 'ctrl_dhcp4_srv_unittest.cc' || echo '$(srcdir)/'`ctrl_dhcp4_srv_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-ctrl_dhcp4_srv_unittest.Tpo $(DEPDIR)/dhcp4_unittests-ctrl_dhcp4_srv_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='ctrl_dhcp4_srv_unittest.cc' object='dhcp4_unittests-ctrl_dhcp4_srv_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-ctrl_dhcp4_srv_unittest.o `test -f 'ctrl_dhcp4_srv_unittest.cc' || echo '$(srcdir)/'`ctrl_dhcp4_srv_unittest.cc
+
+dhcp4_unittests-ctrl_dhcp4_srv_unittest.obj: ctrl_dhcp4_srv_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-ctrl_dhcp4_srv_unittest.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-ctrl_dhcp4_srv_unittest.Tpo -c -o dhcp4_unittests-ctrl_dhcp4_srv_unittest.obj `if test -f 'ctrl_dhcp4_srv_unittest.cc'; then $(CYGPATH_W) 'ctrl_dhcp4_srv_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/ctrl_dhcp4_srv_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-ctrl_dhcp4_srv_unittest.Tpo $(DEPDIR)/dhcp4_unittests-ctrl_dhcp4_srv_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='ctrl_dhcp4_srv_unittest.cc' object='dhcp4_unittests-ctrl_dhcp4_srv_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-ctrl_dhcp4_srv_unittest.obj `if test -f 'ctrl_dhcp4_srv_unittest.cc'; then $(CYGPATH_W) 'ctrl_dhcp4_srv_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/ctrl_dhcp4_srv_unittest.cc'; fi`
+
+dhcp4_unittests-classify_unittest.o: classify_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-classify_unittest.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-classify_unittest.Tpo -c -o dhcp4_unittests-classify_unittest.o `test -f 'classify_unittest.cc' || echo '$(srcdir)/'`classify_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-classify_unittest.Tpo $(DEPDIR)/dhcp4_unittests-classify_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='classify_unittest.cc' object='dhcp4_unittests-classify_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-classify_unittest.o `test -f 'classify_unittest.cc' || echo '$(srcdir)/'`classify_unittest.cc
+
+dhcp4_unittests-classify_unittest.obj: classify_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-classify_unittest.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-classify_unittest.Tpo -c -o dhcp4_unittests-classify_unittest.obj `if test -f 'classify_unittest.cc'; then $(CYGPATH_W) 'classify_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/classify_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-classify_unittest.Tpo $(DEPDIR)/dhcp4_unittests-classify_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='classify_unittest.cc' object='dhcp4_unittests-classify_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-classify_unittest.obj `if test -f 'classify_unittest.cc'; then $(CYGPATH_W) 'classify_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/classify_unittest.cc'; fi`
+
+dhcp4_unittests-config_backend_unittest.o: config_backend_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-config_backend_unittest.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-config_backend_unittest.Tpo -c -o dhcp4_unittests-config_backend_unittest.o `test -f 'config_backend_unittest.cc' || echo '$(srcdir)/'`config_backend_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-config_backend_unittest.Tpo $(DEPDIR)/dhcp4_unittests-config_backend_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='config_backend_unittest.cc' object='dhcp4_unittests-config_backend_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-config_backend_unittest.o `test -f 'config_backend_unittest.cc' || echo '$(srcdir)/'`config_backend_unittest.cc
+
+dhcp4_unittests-config_backend_unittest.obj: config_backend_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-config_backend_unittest.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-config_backend_unittest.Tpo -c -o dhcp4_unittests-config_backend_unittest.obj `if test -f 'config_backend_unittest.cc'; then $(CYGPATH_W) 'config_backend_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/config_backend_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-config_backend_unittest.Tpo $(DEPDIR)/dhcp4_unittests-config_backend_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='config_backend_unittest.cc' object='dhcp4_unittests-config_backend_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-config_backend_unittest.obj `if test -f 'config_backend_unittest.cc'; then $(CYGPATH_W) 'config_backend_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/config_backend_unittest.cc'; fi`
+
+dhcp4_unittests-config_parser_unittest.o: config_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-config_parser_unittest.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-config_parser_unittest.Tpo -c -o dhcp4_unittests-config_parser_unittest.o `test -f 'config_parser_unittest.cc' || echo '$(srcdir)/'`config_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-config_parser_unittest.Tpo $(DEPDIR)/dhcp4_unittests-config_parser_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='config_parser_unittest.cc' object='dhcp4_unittests-config_parser_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-config_parser_unittest.o `test -f 'config_parser_unittest.cc' || echo '$(srcdir)/'`config_parser_unittest.cc
+
+dhcp4_unittests-config_parser_unittest.obj: config_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-config_parser_unittest.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-config_parser_unittest.Tpo -c -o dhcp4_unittests-config_parser_unittest.obj `if test -f 'config_parser_unittest.cc'; then $(CYGPATH_W) 'config_parser_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/config_parser_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-config_parser_unittest.Tpo $(DEPDIR)/dhcp4_unittests-config_parser_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='config_parser_unittest.cc' object='dhcp4_unittests-config_parser_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-config_parser_unittest.obj `if test -f 'config_parser_unittest.cc'; then $(CYGPATH_W) 'config_parser_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/config_parser_unittest.cc'; fi`
+
+dhcp4_unittests-fqdn_unittest.o: fqdn_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-fqdn_unittest.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-fqdn_unittest.Tpo -c -o dhcp4_unittests-fqdn_unittest.o `test -f 'fqdn_unittest.cc' || echo '$(srcdir)/'`fqdn_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-fqdn_unittest.Tpo $(DEPDIR)/dhcp4_unittests-fqdn_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='fqdn_unittest.cc' object='dhcp4_unittests-fqdn_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-fqdn_unittest.o `test -f 'fqdn_unittest.cc' || echo '$(srcdir)/'`fqdn_unittest.cc
+
+dhcp4_unittests-fqdn_unittest.obj: fqdn_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-fqdn_unittest.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-fqdn_unittest.Tpo -c -o dhcp4_unittests-fqdn_unittest.obj `if test -f 'fqdn_unittest.cc'; then $(CYGPATH_W) 'fqdn_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/fqdn_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-fqdn_unittest.Tpo $(DEPDIR)/dhcp4_unittests-fqdn_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='fqdn_unittest.cc' object='dhcp4_unittests-fqdn_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-fqdn_unittest.obj `if test -f 'fqdn_unittest.cc'; then $(CYGPATH_W) 'fqdn_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/fqdn_unittest.cc'; fi`
+
+dhcp4_unittests-marker_file.o: marker_file.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-marker_file.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-marker_file.Tpo -c -o dhcp4_unittests-marker_file.o `test -f 'marker_file.cc' || echo '$(srcdir)/'`marker_file.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-marker_file.Tpo $(DEPDIR)/dhcp4_unittests-marker_file.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='marker_file.cc' object='dhcp4_unittests-marker_file.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-marker_file.o `test -f 'marker_file.cc' || echo '$(srcdir)/'`marker_file.cc
+
+dhcp4_unittests-marker_file.obj: marker_file.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-marker_file.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-marker_file.Tpo -c -o dhcp4_unittests-marker_file.obj `if test -f 'marker_file.cc'; then $(CYGPATH_W) 'marker_file.cc'; else $(CYGPATH_W) '$(srcdir)/marker_file.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-marker_file.Tpo $(DEPDIR)/dhcp4_unittests-marker_file.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='marker_file.cc' object='dhcp4_unittests-marker_file.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-marker_file.obj `if test -f 'marker_file.cc'; then $(CYGPATH_W) 'marker_file.cc'; else $(CYGPATH_W) '$(srcdir)/marker_file.cc'; fi`
+
+dhcp4_unittests-dhcp4_client.o: dhcp4_client.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-dhcp4_client.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-dhcp4_client.Tpo -c -o dhcp4_unittests-dhcp4_client.o `test -f 'dhcp4_client.cc' || echo '$(srcdir)/'`dhcp4_client.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-dhcp4_client.Tpo $(DEPDIR)/dhcp4_unittests-dhcp4_client.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='dhcp4_client.cc' object='dhcp4_unittests-dhcp4_client.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-dhcp4_client.o `test -f 'dhcp4_client.cc' || echo '$(srcdir)/'`dhcp4_client.cc
+
+dhcp4_unittests-dhcp4_client.obj: dhcp4_client.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-dhcp4_client.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-dhcp4_client.Tpo -c -o dhcp4_unittests-dhcp4_client.obj `if test -f 'dhcp4_client.cc'; then $(CYGPATH_W) 'dhcp4_client.cc'; else $(CYGPATH_W) '$(srcdir)/dhcp4_client.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-dhcp4_client.Tpo $(DEPDIR)/dhcp4_unittests-dhcp4_client.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='dhcp4_client.cc' object='dhcp4_unittests-dhcp4_client.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-dhcp4_client.obj `if test -f 'dhcp4_client.cc'; then $(CYGPATH_W) 'dhcp4_client.cc'; else $(CYGPATH_W) '$(srcdir)/dhcp4_client.cc'; fi`
+
+dhcp4_unittests-hooks_unittest.o: hooks_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-hooks_unittest.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-hooks_unittest.Tpo -c -o dhcp4_unittests-hooks_unittest.o `test -f 'hooks_unittest.cc' || echo '$(srcdir)/'`hooks_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-hooks_unittest.Tpo $(DEPDIR)/dhcp4_unittests-hooks_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='hooks_unittest.cc' object='dhcp4_unittests-hooks_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-hooks_unittest.o `test -f 'hooks_unittest.cc' || echo '$(srcdir)/'`hooks_unittest.cc
+
+dhcp4_unittests-hooks_unittest.obj: hooks_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-hooks_unittest.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-hooks_unittest.Tpo -c -o dhcp4_unittests-hooks_unittest.obj `if test -f 'hooks_unittest.cc'; then $(CYGPATH_W) 'hooks_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/hooks_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-hooks_unittest.Tpo $(DEPDIR)/dhcp4_unittests-hooks_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='hooks_unittest.cc' object='dhcp4_unittests-hooks_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-hooks_unittest.obj `if test -f 'hooks_unittest.cc'; then $(CYGPATH_W) 'hooks_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/hooks_unittest.cc'; fi`
+
+dhcp4_unittests-inform_unittest.o: inform_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-inform_unittest.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-inform_unittest.Tpo -c -o dhcp4_unittests-inform_unittest.o `test -f 'inform_unittest.cc' || echo '$(srcdir)/'`inform_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-inform_unittest.Tpo $(DEPDIR)/dhcp4_unittests-inform_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='inform_unittest.cc' object='dhcp4_unittests-inform_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-inform_unittest.o `test -f 'inform_unittest.cc' || echo '$(srcdir)/'`inform_unittest.cc
+
+dhcp4_unittests-inform_unittest.obj: inform_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-inform_unittest.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-inform_unittest.Tpo -c -o dhcp4_unittests-inform_unittest.obj `if test -f 'inform_unittest.cc'; then $(CYGPATH_W) 'inform_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/inform_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-inform_unittest.Tpo $(DEPDIR)/dhcp4_unittests-inform_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='inform_unittest.cc' object='dhcp4_unittests-inform_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-inform_unittest.obj `if test -f 'inform_unittest.cc'; then $(CYGPATH_W) 'inform_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/inform_unittest.cc'; fi`
+
+dhcp4_unittests-dora_unittest.o: dora_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-dora_unittest.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-dora_unittest.Tpo -c -o dhcp4_unittests-dora_unittest.o `test -f 'dora_unittest.cc' || echo '$(srcdir)/'`dora_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-dora_unittest.Tpo $(DEPDIR)/dhcp4_unittests-dora_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='dora_unittest.cc' object='dhcp4_unittests-dora_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-dora_unittest.o `test -f 'dora_unittest.cc' || echo '$(srcdir)/'`dora_unittest.cc
+
+dhcp4_unittests-dora_unittest.obj: dora_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-dora_unittest.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-dora_unittest.Tpo -c -o dhcp4_unittests-dora_unittest.obj `if test -f 'dora_unittest.cc'; then $(CYGPATH_W) 'dora_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/dora_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-dora_unittest.Tpo $(DEPDIR)/dhcp4_unittests-dora_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='dora_unittest.cc' object='dhcp4_unittests-dora_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-dora_unittest.obj `if test -f 'dora_unittest.cc'; then $(CYGPATH_W) 'dora_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/dora_unittest.cc'; fi`
+
+dhcp4_unittests-host_options_unittest.o: host_options_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-host_options_unittest.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-host_options_unittest.Tpo -c -o dhcp4_unittests-host_options_unittest.o `test -f 'host_options_unittest.cc' || echo '$(srcdir)/'`host_options_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-host_options_unittest.Tpo $(DEPDIR)/dhcp4_unittests-host_options_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='host_options_unittest.cc' object='dhcp4_unittests-host_options_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-host_options_unittest.o `test -f 'host_options_unittest.cc' || echo '$(srcdir)/'`host_options_unittest.cc
+
+dhcp4_unittests-host_options_unittest.obj: host_options_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-host_options_unittest.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-host_options_unittest.Tpo -c -o dhcp4_unittests-host_options_unittest.obj `if test -f 'host_options_unittest.cc'; then $(CYGPATH_W) 'host_options_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/host_options_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-host_options_unittest.Tpo $(DEPDIR)/dhcp4_unittests-host_options_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='host_options_unittest.cc' object='dhcp4_unittests-host_options_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-host_options_unittest.obj `if test -f 'host_options_unittest.cc'; then $(CYGPATH_W) 'host_options_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/host_options_unittest.cc'; fi`
+
+dhcp4_unittests-release_unittest.o: release_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-release_unittest.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-release_unittest.Tpo -c -o dhcp4_unittests-release_unittest.o `test -f 'release_unittest.cc' || echo '$(srcdir)/'`release_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-release_unittest.Tpo $(DEPDIR)/dhcp4_unittests-release_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='release_unittest.cc' object='dhcp4_unittests-release_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-release_unittest.o `test -f 'release_unittest.cc' || echo '$(srcdir)/'`release_unittest.cc
+
+dhcp4_unittests-release_unittest.obj: release_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-release_unittest.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-release_unittest.Tpo -c -o dhcp4_unittests-release_unittest.obj `if test -f 'release_unittest.cc'; then $(CYGPATH_W) 'release_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/release_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-release_unittest.Tpo $(DEPDIR)/dhcp4_unittests-release_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='release_unittest.cc' object='dhcp4_unittests-release_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-release_unittest.obj `if test -f 'release_unittest.cc'; then $(CYGPATH_W) 'release_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/release_unittest.cc'; fi`
+
+dhcp4_unittests-parser_unittest.o: parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-parser_unittest.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-parser_unittest.Tpo -c -o dhcp4_unittests-parser_unittest.o `test -f 'parser_unittest.cc' || echo '$(srcdir)/'`parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-parser_unittest.Tpo $(DEPDIR)/dhcp4_unittests-parser_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='parser_unittest.cc' object='dhcp4_unittests-parser_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-parser_unittest.o `test -f 'parser_unittest.cc' || echo '$(srcdir)/'`parser_unittest.cc
+
+dhcp4_unittests-parser_unittest.obj: parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-parser_unittest.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-parser_unittest.Tpo -c -o dhcp4_unittests-parser_unittest.obj `if test -f 'parser_unittest.cc'; then $(CYGPATH_W) 'parser_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/parser_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-parser_unittest.Tpo $(DEPDIR)/dhcp4_unittests-parser_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='parser_unittest.cc' object='dhcp4_unittests-parser_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-parser_unittest.obj `if test -f 'parser_unittest.cc'; then $(CYGPATH_W) 'parser_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/parser_unittest.cc'; fi`
+
+dhcp4_unittests-out_of_range_unittest.o: out_of_range_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-out_of_range_unittest.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-out_of_range_unittest.Tpo -c -o dhcp4_unittests-out_of_range_unittest.o `test -f 'out_of_range_unittest.cc' || echo '$(srcdir)/'`out_of_range_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-out_of_range_unittest.Tpo $(DEPDIR)/dhcp4_unittests-out_of_range_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='out_of_range_unittest.cc' object='dhcp4_unittests-out_of_range_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-out_of_range_unittest.o `test -f 'out_of_range_unittest.cc' || echo '$(srcdir)/'`out_of_range_unittest.cc
+
+dhcp4_unittests-out_of_range_unittest.obj: out_of_range_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-out_of_range_unittest.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-out_of_range_unittest.Tpo -c -o dhcp4_unittests-out_of_range_unittest.obj `if test -f 'out_of_range_unittest.cc'; then $(CYGPATH_W) 'out_of_range_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/out_of_range_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-out_of_range_unittest.Tpo $(DEPDIR)/dhcp4_unittests-out_of_range_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='out_of_range_unittest.cc' object='dhcp4_unittests-out_of_range_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-out_of_range_unittest.obj `if test -f 'out_of_range_unittest.cc'; then $(CYGPATH_W) 'out_of_range_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/out_of_range_unittest.cc'; fi`
+
+dhcp4_unittests-decline_unittest.o: decline_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-decline_unittest.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-decline_unittest.Tpo -c -o dhcp4_unittests-decline_unittest.o `test -f 'decline_unittest.cc' || echo '$(srcdir)/'`decline_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-decline_unittest.Tpo $(DEPDIR)/dhcp4_unittests-decline_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='decline_unittest.cc' object='dhcp4_unittests-decline_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-decline_unittest.o `test -f 'decline_unittest.cc' || echo '$(srcdir)/'`decline_unittest.cc
+
+dhcp4_unittests-decline_unittest.obj: decline_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-decline_unittest.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-decline_unittest.Tpo -c -o dhcp4_unittests-decline_unittest.obj `if test -f 'decline_unittest.cc'; then $(CYGPATH_W) 'decline_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/decline_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-decline_unittest.Tpo $(DEPDIR)/dhcp4_unittests-decline_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='decline_unittest.cc' object='dhcp4_unittests-decline_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-decline_unittest.obj `if test -f 'decline_unittest.cc'; then $(CYGPATH_W) 'decline_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/decline_unittest.cc'; fi`
+
+dhcp4_unittests-kea_controller_unittest.o: kea_controller_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-kea_controller_unittest.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-kea_controller_unittest.Tpo -c -o dhcp4_unittests-kea_controller_unittest.o `test -f 'kea_controller_unittest.cc' || echo '$(srcdir)/'`kea_controller_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-kea_controller_unittest.Tpo $(DEPDIR)/dhcp4_unittests-kea_controller_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='kea_controller_unittest.cc' object='dhcp4_unittests-kea_controller_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-kea_controller_unittest.o `test -f 'kea_controller_unittest.cc' || echo '$(srcdir)/'`kea_controller_unittest.cc
+
+dhcp4_unittests-kea_controller_unittest.obj: kea_controller_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-kea_controller_unittest.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-kea_controller_unittest.Tpo -c -o dhcp4_unittests-kea_controller_unittest.obj `if test -f 'kea_controller_unittest.cc'; then $(CYGPATH_W) 'kea_controller_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/kea_controller_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-kea_controller_unittest.Tpo $(DEPDIR)/dhcp4_unittests-kea_controller_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='kea_controller_unittest.cc' object='dhcp4_unittests-kea_controller_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-kea_controller_unittest.obj `if test -f 'kea_controller_unittest.cc'; then $(CYGPATH_W) 'kea_controller_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/kea_controller_unittest.cc'; fi`
+
+dhcp4_unittests-dhcp4to6_ipc_unittest.o: dhcp4to6_ipc_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-dhcp4to6_ipc_unittest.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-dhcp4to6_ipc_unittest.Tpo -c -o dhcp4_unittests-dhcp4to6_ipc_unittest.o `test -f 'dhcp4to6_ipc_unittest.cc' || echo '$(srcdir)/'`dhcp4to6_ipc_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-dhcp4to6_ipc_unittest.Tpo $(DEPDIR)/dhcp4_unittests-dhcp4to6_ipc_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='dhcp4to6_ipc_unittest.cc' object='dhcp4_unittests-dhcp4to6_ipc_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-dhcp4to6_ipc_unittest.o `test -f 'dhcp4to6_ipc_unittest.cc' || echo '$(srcdir)/'`dhcp4to6_ipc_unittest.cc
+
+dhcp4_unittests-dhcp4to6_ipc_unittest.obj: dhcp4to6_ipc_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-dhcp4to6_ipc_unittest.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-dhcp4to6_ipc_unittest.Tpo -c -o dhcp4_unittests-dhcp4to6_ipc_unittest.obj `if test -f 'dhcp4to6_ipc_unittest.cc'; then $(CYGPATH_W) 'dhcp4to6_ipc_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/dhcp4to6_ipc_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-dhcp4to6_ipc_unittest.Tpo $(DEPDIR)/dhcp4_unittests-dhcp4to6_ipc_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='dhcp4to6_ipc_unittest.cc' object='dhcp4_unittests-dhcp4to6_ipc_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-dhcp4to6_ipc_unittest.obj `if test -f 'dhcp4to6_ipc_unittest.cc'; then $(CYGPATH_W) 'dhcp4to6_ipc_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/dhcp4to6_ipc_unittest.cc'; fi`
+
+dhcp4_unittests-simple_parser4_unittest.o: simple_parser4_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-simple_parser4_unittest.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-simple_parser4_unittest.Tpo -c -o dhcp4_unittests-simple_parser4_unittest.o `test -f 'simple_parser4_unittest.cc' || echo '$(srcdir)/'`simple_parser4_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-simple_parser4_unittest.Tpo $(DEPDIR)/dhcp4_unittests-simple_parser4_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='simple_parser4_unittest.cc' object='dhcp4_unittests-simple_parser4_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-simple_parser4_unittest.o `test -f 'simple_parser4_unittest.cc' || echo '$(srcdir)/'`simple_parser4_unittest.cc
+
+dhcp4_unittests-simple_parser4_unittest.obj: simple_parser4_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-simple_parser4_unittest.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-simple_parser4_unittest.Tpo -c -o dhcp4_unittests-simple_parser4_unittest.obj `if test -f 'simple_parser4_unittest.cc'; then $(CYGPATH_W) 'simple_parser4_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/simple_parser4_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-simple_parser4_unittest.Tpo $(DEPDIR)/dhcp4_unittests-simple_parser4_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='simple_parser4_unittest.cc' object='dhcp4_unittests-simple_parser4_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-simple_parser4_unittest.obj `if test -f 'simple_parser4_unittest.cc'; then $(CYGPATH_W) 'simple_parser4_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/simple_parser4_unittest.cc'; fi`
+
+dhcp4_unittests-get_config_unittest.o: get_config_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-get_config_unittest.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-get_config_unittest.Tpo -c -o dhcp4_unittests-get_config_unittest.o `test -f 'get_config_unittest.cc' || echo '$(srcdir)/'`get_config_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-get_config_unittest.Tpo $(DEPDIR)/dhcp4_unittests-get_config_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='get_config_unittest.cc' object='dhcp4_unittests-get_config_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-get_config_unittest.o `test -f 'get_config_unittest.cc' || echo '$(srcdir)/'`get_config_unittest.cc
+
+dhcp4_unittests-get_config_unittest.obj: get_config_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-get_config_unittest.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-get_config_unittest.Tpo -c -o dhcp4_unittests-get_config_unittest.obj `if test -f 'get_config_unittest.cc'; then $(CYGPATH_W) 'get_config_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/get_config_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-get_config_unittest.Tpo $(DEPDIR)/dhcp4_unittests-get_config_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='get_config_unittest.cc' object='dhcp4_unittests-get_config_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-get_config_unittest.obj `if test -f 'get_config_unittest.cc'; then $(CYGPATH_W) 'get_config_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/get_config_unittest.cc'; fi`
+
+dhcp4_unittests-shared_network_unittest.o: shared_network_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-shared_network_unittest.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-shared_network_unittest.Tpo -c -o dhcp4_unittests-shared_network_unittest.o `test -f 'shared_network_unittest.cc' || echo '$(srcdir)/'`shared_network_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-shared_network_unittest.Tpo $(DEPDIR)/dhcp4_unittests-shared_network_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='shared_network_unittest.cc' object='dhcp4_unittests-shared_network_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-shared_network_unittest.o `test -f 'shared_network_unittest.cc' || echo '$(srcdir)/'`shared_network_unittest.cc
+
+dhcp4_unittests-shared_network_unittest.obj: shared_network_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-shared_network_unittest.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-shared_network_unittest.Tpo -c -o dhcp4_unittests-shared_network_unittest.obj `if test -f 'shared_network_unittest.cc'; then $(CYGPATH_W) 'shared_network_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/shared_network_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-shared_network_unittest.Tpo $(DEPDIR)/dhcp4_unittests-shared_network_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='shared_network_unittest.cc' object='dhcp4_unittests-shared_network_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-shared_network_unittest.obj `if test -f 'shared_network_unittest.cc'; then $(CYGPATH_W) 'shared_network_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/shared_network_unittest.cc'; fi`
+
+dhcp4_unittests-host_unittest.o: host_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-host_unittest.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-host_unittest.Tpo -c -o dhcp4_unittests-host_unittest.o `test -f 'host_unittest.cc' || echo '$(srcdir)/'`host_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-host_unittest.Tpo $(DEPDIR)/dhcp4_unittests-host_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='host_unittest.cc' object='dhcp4_unittests-host_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-host_unittest.o `test -f 'host_unittest.cc' || echo '$(srcdir)/'`host_unittest.cc
+
+dhcp4_unittests-host_unittest.obj: host_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-host_unittest.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-host_unittest.Tpo -c -o dhcp4_unittests-host_unittest.obj `if test -f 'host_unittest.cc'; then $(CYGPATH_W) 'host_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/host_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-host_unittest.Tpo $(DEPDIR)/dhcp4_unittests-host_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='host_unittest.cc' object='dhcp4_unittests-host_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-host_unittest.obj `if test -f 'host_unittest.cc'; then $(CYGPATH_W) 'host_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/host_unittest.cc'; fi`
+
+dhcp4_unittests-vendor_opts_unittest.o: vendor_opts_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-vendor_opts_unittest.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-vendor_opts_unittest.Tpo -c -o dhcp4_unittests-vendor_opts_unittest.o `test -f 'vendor_opts_unittest.cc' || echo '$(srcdir)/'`vendor_opts_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-vendor_opts_unittest.Tpo $(DEPDIR)/dhcp4_unittests-vendor_opts_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='vendor_opts_unittest.cc' object='dhcp4_unittests-vendor_opts_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-vendor_opts_unittest.o `test -f 'vendor_opts_unittest.cc' || echo '$(srcdir)/'`vendor_opts_unittest.cc
+
+dhcp4_unittests-vendor_opts_unittest.obj: vendor_opts_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-vendor_opts_unittest.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-vendor_opts_unittest.Tpo -c -o dhcp4_unittests-vendor_opts_unittest.obj `if test -f 'vendor_opts_unittest.cc'; then $(CYGPATH_W) 'vendor_opts_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/vendor_opts_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-vendor_opts_unittest.Tpo $(DEPDIR)/dhcp4_unittests-vendor_opts_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='vendor_opts_unittest.cc' object='dhcp4_unittests-vendor_opts_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-vendor_opts_unittest.obj `if test -f 'vendor_opts_unittest.cc'; then $(CYGPATH_W) 'vendor_opts_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/vendor_opts_unittest.cc'; fi`
+
+dhcp4_unittests-client_handler_unittest.o: client_handler_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-client_handler_unittest.o -MD -MP -MF $(DEPDIR)/dhcp4_unittests-client_handler_unittest.Tpo -c -o dhcp4_unittests-client_handler_unittest.o `test -f 'client_handler_unittest.cc' || echo '$(srcdir)/'`client_handler_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-client_handler_unittest.Tpo $(DEPDIR)/dhcp4_unittests-client_handler_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='client_handler_unittest.cc' object='dhcp4_unittests-client_handler_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-client_handler_unittest.o `test -f 'client_handler_unittest.cc' || echo '$(srcdir)/'`client_handler_unittest.cc
+
+dhcp4_unittests-client_handler_unittest.obj: client_handler_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT dhcp4_unittests-client_handler_unittest.obj -MD -MP -MF $(DEPDIR)/dhcp4_unittests-client_handler_unittest.Tpo -c -o dhcp4_unittests-client_handler_unittest.obj `if test -f 'client_handler_unittest.cc'; then $(CYGPATH_W) 'client_handler_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/client_handler_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/dhcp4_unittests-client_handler_unittest.Tpo $(DEPDIR)/dhcp4_unittests-client_handler_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='client_handler_unittest.cc' object='dhcp4_unittests-client_handler_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(dhcp4_unittests_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o dhcp4_unittests-client_handler_unittest.obj `if test -f 'client_handler_unittest.cc'; then $(CYGPATH_W) 'client_handler_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/client_handler_unittest.cc'; fi`
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+
+# This directory's subdirectories are mostly independent; you can cd
+# into them and run 'make' without going through this Makefile.
+# To change the values of 'make' variables: instead of editing Makefiles,
+# (1) if the variable is set in 'config.status', edit 'config.status'
+# (which will cause the Makefiles to be regenerated when you run 'make');
+# (2) otherwise, pass the desired values on the 'make' command line.
+$(am__recursive_targets):
+ @fail=; \
+ if $(am__make_keepgoing); then \
+ failcom='fail=yes'; \
+ else \
+ failcom='exit 1'; \
+ fi; \
+ dot_seen=no; \
+ target=`echo $@ | sed s/-recursive//`; \
+ case "$@" in \
+ distclean-* | maintainer-clean-*) list='$(DIST_SUBDIRS)' ;; \
+ *) list='$(SUBDIRS)' ;; \
+ esac; \
+ for subdir in $$list; do \
+ echo "Making $$target in $$subdir"; \
+ if test "$$subdir" = "."; then \
+ dot_seen=yes; \
+ local_target="$$target-am"; \
+ else \
+ local_target="$$target"; \
+ fi; \
+ ($(am__cd) $$subdir && $(MAKE) $(AM_MAKEFLAGS) $$local_target) \
+ || eval $$failcom; \
+ done; \
+ if test "$$dot_seen" = "no"; then \
+ $(MAKE) $(AM_MAKEFLAGS) "$$target-am" || exit 1; \
+ fi; test -z "$$fail"
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-recursive
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ if ($(ETAGS) --etags-include --version) >/dev/null 2>&1; then \
+ include_option=--etags-include; \
+ empty_fix=.; \
+ else \
+ include_option=--include; \
+ empty_fix=; \
+ fi; \
+ list='$(SUBDIRS)'; for subdir in $$list; do \
+ if test "$$subdir" = .; then :; else \
+ test ! -f $$subdir/TAGS || \
+ set "$$@" "$$include_option=$$here/$$subdir/TAGS"; \
+ fi; \
+ done; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-recursive
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-recursive
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+check-TESTS: $(TESTS)
+ @failed=0; all=0; xfail=0; xpass=0; skip=0; \
+ srcdir=$(srcdir); export srcdir; \
+ list=' $(TESTS) '; \
+ $(am__tty_colors); \
+ if test -n "$$list"; then \
+ for tst in $$list; do \
+ if test -f ./$$tst; then dir=./; \
+ elif test -f $$tst; then dir=; \
+ else dir="$(srcdir)/"; fi; \
+ if $(TESTS_ENVIRONMENT) $${dir}$$tst $(AM_TESTS_FD_REDIRECT); then \
+ all=`expr $$all + 1`; \
+ case " $(XFAIL_TESTS) " in \
+ *[\ \ ]$$tst[\ \ ]*) \
+ xpass=`expr $$xpass + 1`; \
+ failed=`expr $$failed + 1`; \
+ col=$$red; res=XPASS; \
+ ;; \
+ *) \
+ col=$$grn; res=PASS; \
+ ;; \
+ esac; \
+ elif test $$? -ne 77; then \
+ all=`expr $$all + 1`; \
+ case " $(XFAIL_TESTS) " in \
+ *[\ \ ]$$tst[\ \ ]*) \
+ xfail=`expr $$xfail + 1`; \
+ col=$$lgn; res=XFAIL; \
+ ;; \
+ *) \
+ failed=`expr $$failed + 1`; \
+ col=$$red; res=FAIL; \
+ ;; \
+ esac; \
+ else \
+ skip=`expr $$skip + 1`; \
+ col=$$blu; res=SKIP; \
+ fi; \
+ echo "$${col}$$res$${std}: $$tst"; \
+ done; \
+ if test "$$all" -eq 1; then \
+ tests="test"; \
+ All=""; \
+ else \
+ tests="tests"; \
+ All="All "; \
+ fi; \
+ if test "$$failed" -eq 0; then \
+ if test "$$xfail" -eq 0; then \
+ banner="$$All$$all $$tests passed"; \
+ else \
+ if test "$$xfail" -eq 1; then failures=failure; else failures=failures; fi; \
+ banner="$$All$$all $$tests behaved as expected ($$xfail expected $$failures)"; \
+ fi; \
+ else \
+ if test "$$xpass" -eq 0; then \
+ banner="$$failed of $$all $$tests failed"; \
+ else \
+ if test "$$xpass" -eq 1; then passes=pass; else passes=passes; fi; \
+ banner="$$failed of $$all $$tests did not behave as expected ($$xpass unexpected $$passes)"; \
+ fi; \
+ fi; \
+ dashes="$$banner"; \
+ skipped=""; \
+ if test "$$skip" -ne 0; then \
+ if test "$$skip" -eq 1; then \
+ skipped="($$skip test was not run)"; \
+ else \
+ skipped="($$skip tests were not run)"; \
+ fi; \
+ test `echo "$$skipped" | wc -c` -le `echo "$$banner" | wc -c` || \
+ dashes="$$skipped"; \
+ fi; \
+ report=""; \
+ if test "$$failed" -ne 0 && test -n "$(PACKAGE_BUGREPORT)"; then \
+ report="Please report to $(PACKAGE_BUGREPORT)"; \
+ test `echo "$$report" | wc -c` -le `echo "$$banner" | wc -c` || \
+ dashes="$$report"; \
+ fi; \
+ dashes=`echo "$$dashes" | sed s/./=/g`; \
+ if test "$$failed" -eq 0; then \
+ col="$$grn"; \
+ else \
+ col="$$red"; \
+ fi; \
+ echo "$${col}$$dashes$${std}"; \
+ echo "$${col}$$banner$${std}"; \
+ test -z "$$skipped" || echo "$${col}$$skipped$${std}"; \
+ test -z "$$report" || echo "$${col}$$report$${std}"; \
+ echo "$${col}$$dashes$${std}"; \
+ test "$$failed" -eq 0; \
+ else :; fi
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+ @list='$(DIST_SUBDIRS)'; for subdir in $$list; do \
+ if test "$$subdir" = .; then :; else \
+ $(am__make_dryrun) \
+ || test -d "$(distdir)/$$subdir" \
+ || $(MKDIR_P) "$(distdir)/$$subdir" \
+ || exit 1; \
+ dir1=$$subdir; dir2="$(distdir)/$$subdir"; \
+ $(am__relativize); \
+ new_distdir=$$reldir; \
+ dir1=$$subdir; dir2="$(top_distdir)"; \
+ $(am__relativize); \
+ new_top_distdir=$$reldir; \
+ echo " (cd $$subdir && $(MAKE) $(AM_MAKEFLAGS) top_distdir="$$new_top_distdir" distdir="$$new_distdir" \\"; \
+ echo " am__remove_distdir=: am__skip_length_check=: am__skip_mode_fix=: distdir)"; \
+ ($(am__cd) $$subdir && \
+ $(MAKE) $(AM_MAKEFLAGS) \
+ top_distdir="$$new_top_distdir" \
+ distdir="$$new_distdir" \
+ am__remove_distdir=: \
+ am__skip_length_check=: \
+ am__skip_mode_fix=: \
+ distdir) \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) $(check_SCRIPTS)
+ $(MAKE) $(AM_MAKEFLAGS) check-TESTS
+check: check-recursive
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(SCRIPTS)
+installdirs: installdirs-recursive
+installdirs-am:
+install: install-recursive
+install-exec: install-exec-recursive
+install-data: install-data-recursive
+uninstall: uninstall-recursive
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-recursive
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+ -test -z "$(DISTCLEANFILES)" || rm -f $(DISTCLEANFILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-recursive
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-recursive
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-classify_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-client_handler_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-config_backend_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-config_parser_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-ctrl_dhcp4_srv_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-d2_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-decline_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-dhcp4_client.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-dhcp4_srv_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-dhcp4_test_utils.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-dhcp4_unittests.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-dhcp4to6_ipc_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-direct_client_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-dora_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-fqdn_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-get_config_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-hooks_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-host_options_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-host_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-inform_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-kea_controller_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-marker_file.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-out_of_range_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-parser_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-release_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-shared_network_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-simple_parser4_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-vendor_opts_unittest.Po
+ -rm -f ./$(DEPDIR)/libco1_la-callout_library_1.Plo
+ -rm -f ./$(DEPDIR)/libco2_la-callout_library_2.Plo
+ -rm -f ./$(DEPDIR)/libco3_la-callout_library_3.Plo
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-recursive
+
+dvi-am:
+
+html: html-recursive
+
+html-am:
+
+info: info-recursive
+
+info-am:
+
+install-data-am:
+
+install-dvi: install-dvi-recursive
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-recursive
+
+install-html-am:
+
+install-info: install-info-recursive
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-recursive
+
+install-pdf-am:
+
+install-ps: install-ps-recursive
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-recursive
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-classify_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-client_handler_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-config_backend_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-config_parser_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-ctrl_dhcp4_srv_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-d2_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-decline_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-dhcp4_client.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-dhcp4_srv_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-dhcp4_test_utils.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-dhcp4_unittests.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-dhcp4to6_ipc_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-direct_client_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-dora_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-fqdn_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-get_config_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-hooks_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-host_options_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-host_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-inform_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-kea_controller_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-marker_file.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-out_of_range_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-parser_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-release_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-shared_network_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-simple_parser4_unittest.Po
+ -rm -f ./$(DEPDIR)/dhcp4_unittests-vendor_opts_unittest.Po
+ -rm -f ./$(DEPDIR)/libco1_la-callout_library_1.Plo
+ -rm -f ./$(DEPDIR)/libco2_la-callout_library_2.Plo
+ -rm -f ./$(DEPDIR)/libco3_la-callout_library_3.Plo
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-recursive
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-recursive
+
+pdf-am:
+
+ps: ps-recursive
+
+ps-am:
+
+uninstall-am:
+
+.MAKE: $(am__recursive_targets) check-am install-am install-strip
+
+.PHONY: $(am__recursive_targets) CTAGS GTAGS TAGS all all-am \
+ am--depfiles check check-TESTS check-am clean clean-generic \
+ clean-libtool clean-noinstLTLIBRARIES clean-noinstPROGRAMS \
+ cscopelist-am ctags ctags-am distclean distclean-compile \
+ distclean-generic distclean-libtool distclean-tags distdir dvi \
+ dvi-am html html-am info info-am install install-am \
+ install-data install-data-am install-dvi install-dvi-am \
+ install-exec install-exec-am install-html install-html-am \
+ install-info install-info-am install-man install-pdf \
+ install-pdf-am install-ps install-ps-am install-strip \
+ installcheck installcheck-am installdirs installdirs-am \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am
+
+.PRECIOUS: Makefile
+
+
+# Use this target if you want to rebuild the get-config unit-tests.
+#
+# TODO: We could also automate the replacement step with some variation
+# of this: https://stackoverflow.com/questions/6790631
+@HAVE_GTEST_TRUE@rebuild-tests:
+@HAVE_GTEST_TRUE@ rm -f x u get_config_unittest.cc
+@HAVE_GTEST_TRUE@ cp -f get_config_unittest.cc.skel get_config_unittest.cc
+@HAVE_GTEST_TRUE@ $(MAKE) CXXFLAGS=-DEXTRACT_CONFIG V=1
+@HAVE_GTEST_TRUE@ ./dhcp4_unittests --gtest_filter="Dhcp4Parser*" > /dev/null 2> x
+@HAVE_GTEST_TRUE@ echo "Please copy content of x file into EXTRACTED_CONFIGS in get_config_unittest.cc"
+@HAVE_GTEST_TRUE@ read -p "Press ENTER when ready"
+@HAVE_GTEST_TRUE@ $(MAKE) CXXFLAGS=-DGENERATE_ACTION V=1
+@HAVE_GTEST_TRUE@ ./dhcp4_unittests --gtest_filter="Dhcp4GetConfig*" > /dev/null 2> u
+@HAVE_GTEST_TRUE@ echo "Please copy content of u file into UNPARSED_CONFIGS in get_config_unittest.cc"
+@HAVE_GTEST_TRUE@ read -p "Press ENTER when ready"
+@HAVE_GTEST_TRUE@ touch get_config_unittest.cc
+@HAVE_GTEST_TRUE@ $(MAKE)
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/bin/dhcp4/tests/callout_library_1.cc b/src/bin/dhcp4/tests/callout_library_1.cc
new file mode 100644
index 0000000..7c16b03
--- /dev/null
+++ b/src/bin/dhcp4/tests/callout_library_1.cc
@@ -0,0 +1,34 @@
+// Copyright (C) 2013-2023 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+/// @file
+/// @brief Marker file callout library
+///
+/// This is the source of a test library for the DHCP parser and configuration
+/// tests. See callout_common.cc for details.
+
+static const int LIBRARY_NUMBER = 1;
+#include <config.h>
+
+#include <dhcp4/tests/callout_library_common.h>
+
+// Functions accessed by the hooks framework use C linkage to avoid the name
+// mangling that accompanies use of the C++ compiler as well as to avoid
+// issues related to namespaces.
+extern "C" {
+
+int (*do_load)(isc::hooks::LibraryHandle& handle);
+
+int (*do_unload)();
+
+/// @brief This function is called to retrieve the multi-threading compatibility.
+///
+/// @return 1 which means compatible with multi-threading.
+int multi_threading_compatible() {
+ return (1);
+}
+
+} // end extern "C"
diff --git a/src/bin/dhcp4/tests/callout_library_2.cc b/src/bin/dhcp4/tests/callout_library_2.cc
new file mode 100644
index 0000000..6059340
--- /dev/null
+++ b/src/bin/dhcp4/tests/callout_library_2.cc
@@ -0,0 +1,24 @@
+// Copyright (C) 2013-2023 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+/// @file
+/// @brief Marker file callout library
+///
+/// This is the source of a test library for the DHCP parser and configuration
+/// tests. See callout_common.cc for details.
+
+static const int LIBRARY_NUMBER = 2;
+#include <config.h>
+
+#include <dhcp4/tests/callout_library_common.h>
+
+extern "C" {
+
+int (*do_load)(isc::hooks::LibraryHandle& handle);
+
+int (*do_unload)();
+
+}
diff --git a/src/bin/dhcp4/tests/callout_library_3.cc b/src/bin/dhcp4/tests/callout_library_3.cc
new file mode 100644
index 0000000..6bd0fa3
--- /dev/null
+++ b/src/bin/dhcp4/tests/callout_library_3.cc
@@ -0,0 +1,95 @@
+// Copyright (C) 2018-2023 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+/// @file
+/// @brief Callout library for testing execution of the dhcp4_srv_configured
+/// hook point.
+///
+static const int LIBRARY_NUMBER = 3;
+
+#include <config.h>
+
+#include <dhcp4/tests/callout_library_common.h>
+#include <dhcpsrv/srv_config.h>
+
+#include <string>
+#include <vector>
+
+using namespace isc::hooks;
+
+// Functions accessed by the hooks framework use C linkage to avoid the name
+// mangling that accompanies use of the C++ compiler as well as to avoid
+// issues related to namespaces.
+extern "C" {
+
+int
+do_load_impl(LibraryHandle& handle) {
+ // Determine if this callout is configured to fail.
+ isc::dhcp::SrvConfigPtr config;
+ isc::data::ConstElementPtr const& parameters(handle.getParameters());
+ isc::data::ConstElementPtr mode_element(parameters ? parameters->get("mode") : 0);
+ std::string mode(mode_element ? mode_element->stringValue() : "");
+ if (mode == "fail-on-load") {
+ return (1);
+ }
+ return (0);
+}
+
+int (*do_load)(isc::hooks::LibraryHandle& handle) = do_load_impl;
+
+int (*do_unload)();
+
+/// @brief Callout which appends library number and provided arguments to
+/// the marker file for dhcp4_srv_configured callout.
+///
+/// @param handle callout handle passed to the callout.
+///
+/// @return 0 on success, 1 otherwise.
+int
+dhcp4_srv_configured(CalloutHandle& handle) {
+
+ // Append library number.
+ if (appendDigit(SRV_CONFIG_MARKER_FILE)) {
+ return (1);
+ }
+
+ // Append argument names.
+ std::vector<std::string> args = handle.getArgumentNames();
+ for (auto arg = args.begin(); arg != args.end(); ++arg) {
+ if (appendArgument(SRV_CONFIG_MARKER_FILE, arg->c_str()) != 0) {
+ return (1);
+ }
+ }
+
+ // Determine if this callout is configured to fail.
+ isc::dhcp::SrvConfigPtr config;
+ handle.getArgument("server_config", config);
+ isc::data::ConstElementPtr mode_element(config ? config->toElement() ?
+ config->toElement()->find("Dhcp4/hooks-libraries") ?
+ config->toElement()->find("Dhcp4/hooks-libraries")->get(0) ?
+ config->toElement()->find("Dhcp4/hooks-libraries")->get(0)->get("parameters") ?
+ config->toElement()->find("Dhcp4/hooks-libraries")->get(0)->get("parameters")->get("mode")
+ : 0 : 0 : 0 : 0 : 0);
+ std::string mode(mode_element ? mode_element->stringValue() : "");
+ if (mode == "fail-without-error") {
+ handle.setStatus(CalloutHandle::NEXT_STEP_DROP);
+ } else if (mode == "fail-with-error") {
+ std::string error("user explicitly configured me to fail");
+ handle.setArgument("error", error);
+ handle.setStatus(CalloutHandle::NEXT_STEP_DROP);
+ }
+
+ return (0);
+}
+
+/// @brief This function is called to retrieve the multi-threading compatibility.
+///
+/// @return 0 which means not compatible with multi-threading.
+int multi_threading_compatible() {
+ return (0);
+}
+
+} // end extern "C"
diff --git a/src/bin/dhcp4/tests/callout_library_common.h b/src/bin/dhcp4/tests/callout_library_common.h
new file mode 100644
index 0000000..0732019
--- /dev/null
+++ b/src/bin/dhcp4/tests/callout_library_common.h
@@ -0,0 +1,110 @@
+// Copyright (C) 2013-2023 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+/// @file
+/// @brief Marker file callout library
+///
+/// This is the source of a test library for the DHCP parser and configuration
+/// tests.
+///
+/// To check that they libraries are loaded and unloaded correctly, the load
+/// and unload functions in this library maintain two marker files - the load
+/// marker file and the unload marker file. The functions append a single
+/// line to the file, creating the file if need be. In this way, the test code
+/// can determine whether the load/unload functions have been run and, if so,
+/// in what order.
+///
+/// The additional marker file is created for the dhcp4_srv_configured hook
+/// point. It records the library number and the names of the parameters
+/// provided to the callout.
+///
+/// This file is the common library file for the tests. It will not compile
+/// by itself - it is included into each callout library which specifies the
+/// missing constant LIBRARY_NUMBER before the inclusion.
+
+#include <config.h>
+#include <hooks/hooks.h>
+#include "marker_file.h"
+
+#include <fstream>
+
+extern "C" {
+
+extern int (*do_load)(isc::hooks::LibraryHandle& handle);
+
+extern int (*do_unload)();
+
+/// @brief Append digit to marker file
+///
+/// If the marker file does not exist, create it. Then append the single
+/// digit (given by the constant LIBRARY_NUMBER) defined earlier to it and
+/// close the file.
+///
+/// @param name Name of the file to open
+///
+/// @return 0 on success, non-zero on error.
+int
+appendDigit(const char* name) {
+ // Open the file and check if successful.
+ std::fstream file(name, std::fstream::out | std::fstream::app);
+ if (!file.good()) {
+ return (1);
+ }
+
+ // Add the library number to it and close.
+ file << LIBRARY_NUMBER;
+ file.close();
+
+ return (0);
+}
+
+/// @brief Append argument name passed to the callout to a marker file.
+///
+/// @param file_name Name of the file to open.
+/// @param parameter Parameter name.
+///
+/// @return 0 on success, non-zero on error.
+int appendArgument(const char* file_name, const char* argument) {
+ // Open the file and check if successful.
+ std::fstream file(file_name, std::fstream::out | std::fstream::app);
+ if (!file.good()) {
+ return (1);
+ }
+
+ // Add the library number to it and close.
+ file << argument;
+ file.close();
+
+ return (0);
+}
+
+// Framework functions
+int
+version() {
+ return (KEA_HOOKS_VERSION);
+}
+
+int
+load(isc::hooks::LibraryHandle& handle) {
+ int result = 0;
+ result = appendDigit(LOAD_MARKER_FILE);
+ if (result == 0 && do_load) {
+ result = do_load(handle);
+ }
+ return (result);
+}
+
+int
+unload() {
+ int result = 0;
+ result = appendDigit(UNLOAD_MARKER_FILE);
+ if (result == 0 && do_unload) {
+ result = do_unload();
+ }
+ return (result);
+}
+
+};
diff --git a/src/bin/dhcp4/tests/classify_unittest.cc b/src/bin/dhcp4/tests/classify_unittest.cc
new file mode 100644
index 0000000..2d06a49
--- /dev/null
+++ b/src/bin/dhcp4/tests/classify_unittest.cc
@@ -0,0 +1,1443 @@
+// Copyright (C) 2016-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <asiolink/io_address.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcp4/tests/dhcp4_client.h>
+#include <dhcp/option_int.h>
+#include <stats/stats_mgr.h>
+#include <algorithm>
+#include <vector>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace std;
+
+namespace {
+
+/// @brief Set of JSON configurations used throughout the classify tests.
+///
+/// - Configuration 0:
+/// - Used for testing dynamic assignment of client classes
+/// - 1 subnet: 10.0.0.0/24
+/// - 1 pool: 10.0.0.10-10.0.0.100
+/// - the following classes defined:
+/// option[93].hex == 0x0009, next-server set to 1.2.3.4
+/// option[93].hex == 0x0007, set server-hostname to deneb
+/// option[93].hex == 0x0006, set boot-file-name to pxelinux.0
+/// option[93].hex == 0x0001, set boot-file-name to ipxe.efi
+///
+/// - Configuration 1:
+/// - Used for testing reservations of client classes for a client
+/// - The following classes are defined:
+/// - 'pxe', next-server set to 1.2.3.4, assigned dynamically
+/// - 'reserved-class1', routers set to 10.0.0.200, reserved for a
+/// host using HW address 'aa:bb:cc:dd:ee:ff'
+/// - 'reserved-class2', domain-name-servers set to 10.0.0.201,
+/// also reserved for the host using HW address
+/// 'aa:bb:cc:dd:ee:ff'
+/// - Subnet of 10.0.0.0/24 with a single address pool
+///
+/// - Configuration 2:
+/// - Used for testing client class combination
+/// - 1 subnet: 10.0.0.0/24
+/// - 1 pool: 10.0.0.10-10.0.0.100
+/// - the following classes defined:
+/// not (option[93].hex == 0x0009)
+/// not member(<preceeding>), next-server set to 1.2.3.4
+/// option[93].hex == 0x0006
+/// option[93].hex == 0x0001
+/// or member(<last two>), set boot-file-name to pxelinux.0
+///
+/// - Configuration 3:
+/// - Used for required classification
+/// - 1 subnet: 10.0.0.0/24
+/// - 1 pool: 10.0.0.10-10.0.0.100
+/// - the following classes defined:
+/// option[93].hex == 0x0009, next-server set to 1.2.3.4
+/// option[93].hex == 0x0007, set server-hostname to deneb
+/// option[93].hex == 0x0006, set boot-file-name to pxelinux.0
+/// option[93].hex == 0x0001, set boot-file-name to ipxe.efi
+///
+/// - Configuration 4:
+/// - Used for complex membership (example taken from HA)
+/// - 1 subnet: 10.0.0.0/24
+/// - 4 pools: 10.0.0.10-10.0.0.49, 10.0.0.60-10.0.0.99,
+/// 10.0.0.110-10.0.0.149, 10.0.0.1.60-10.0.0.199
+/// - 4 classes to compose:
+/// server1 and server2 for each HA server
+/// option[93].hex == 0x0009 aka telephones
+/// option[93].hex == 0x0007 aka computers
+///
+/// - Configuration 5:
+/// - Used for the DROP class
+/// - 1 subnet: 10.0.0.0/24
+/// - 1 pool: 10.0.0.10-10.0.0.100
+/// - the following class defined: option[93].hex == 0x0009, DROP
+///
+/// - Configuration 6:
+/// - Used for the DROP class and reservation existence.
+/// - 1 subnet: 10.0.0.0/24
+/// - 1 pool: 10.0.0.10-10.0.0.100
+/// - 1 reservation for HW address 'aa:bb:cc:dd:ee:ff'
+/// - the following class defined: not member('KNOWN'), DROP
+/// (the not member also verifies that the DROP class is set only
+/// after the host reservation lookup)
+/// @note the reservation includes a hostname because raw reservations are
+/// not yet allowed.
+///
+/// - Configuration 7:
+/// - Used for the DROP class and reservation class.
+/// - 1 subnet: 10.0.0.0/24
+/// - 1 pool: 10.0.0.10-10.0.0.100
+/// - 1 reservation for HW address 'aa:bb:cc:dd:ee:ff'
+/// setting the allowed class
+/// - the following classes defined:
+/// - allowed
+/// - member('KNOWN') or member('UNKNOWN'), t
+/// - not member('allowed') and member('t'), DROP
+/// The function of the always true 't' class is to move the DROP
+/// evaluation to the classification point after the host reservation
+/// lookup, i.e. indirect KNOWN / UNKNOWN dependency.
+///
+/// - Configuration 8:
+/// - Used for the early global reservations lookup / select subnet.
+/// - 2 subnets: 10.0.0.0/24 guarded by first and 10.0.1.0/24
+/// - 2 pools: 10.0.0.10-10.0.0.100 and 10.0.1.10-10.0.1.100
+/// - 1 global reservation for HW address 'aa:bb:cc:dd:ee:ff'
+/// setting the first class
+/// - the following class defined: first
+///
+/// - Configuration 9:
+/// - Used for the early global reservations lookup / drop.
+/// - 1 subnet: 10.0.0.0/24
+/// - 1 pool: 10.0.0.10-10.0.0.100
+/// - 1 reservation for HW address 'aa:bb:cc:dd:ee:ff'
+/// setting the DROP class
+///
+const char* CONFIGS[] = {
+ // Configuration 0
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"client-classes\": ["
+ "{"
+ " \"name\": \"pxe1\","
+ " \"test\": \"option[93].hex == 0x0009\","
+ " \"next-server\": \"1.2.3.4\""
+ "},"
+ "{"
+ " \"name\": \"pxe2\","
+ " \"test\": \"option[93].hex == 0x0007\","
+ " \"server-hostname\": \"deneb\""
+ "},"
+ "{"
+ " \"name\": \"pxe3\","
+ " \"test\": \"option[93].hex == 0x0006\","
+ " \"boot-file-name\": \"pxelinux.0\""
+ "},"
+ "{"
+ " \"name\": \"pxe4\","
+ " \"test\": \"option[93].hex == 0x0001\","
+ " \"boot-file-name\": \"ipxe.efi\""
+ "}],"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ]"
+ " } ]"
+ "}",
+
+ // Configuration 1
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"client-classes\": ["
+ "{"
+ " \"name\": \"pxe\","
+ " \"test\": \"option[93].hex == 0x0009\","
+ " \"next-server\": \"1.2.3.4\""
+ "},"
+ "{"
+ " \"name\": \"reserved-class1\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"routers\","
+ " \"data\": \"10.0.0.200\""
+ " }"
+ " ]"
+ "},"
+ "{"
+ " \"name\": \"reserved-class2\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"10.0.0.201\""
+ " }"
+ " ]"
+ "}"
+ "],"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"reservations\": [ "
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"client-classes\": [ \"reserved-class1\", \"reserved-class2\" ]"
+ " }"
+ " ]"
+ " } ]"
+ "}",
+
+ // Configuration 2
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"client-classes\": ["
+ "{"
+ " \"name\": \"not-pxe1\","
+ " \"test\": \"not (option[93].hex == 0x0009)\""
+ "},"
+ "{"
+ " \"name\": \"pxe1\","
+ " \"test\": \"not member('not-pxe1')\","
+ " \"next-server\": \"1.2.3.4\""
+ "},"
+ "{"
+ " \"name\": \"pxe3\","
+ " \"test\": \"option[93].hex == 0x0006\""
+ "},"
+ "{"
+ " \"name\": \"pxe4\","
+ " \"test\": \"option[93].hex == 0x0001\""
+ "},"
+ "{"
+ " \"name\": \"pxe34\","
+ " \"test\": \"member('pxe3') or member('pxe4')\","
+ " \"boot-file-name\": \"pxelinux.0\""
+ "}],"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ]"
+ " } ]"
+ "}",
+
+ // Configuration 3
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"client-classes\": ["
+ "{"
+ " \"name\": \"pxe1\","
+ " \"test\": \"option[93].hex == 0x0009\","
+ " \"only-if-required\": true,"
+ " \"next-server\": \"1.2.3.4\""
+ "},"
+ "{"
+ " \"name\": \"pxe2\","
+ " \"test\": \"option[93].hex == 0x0007\","
+ " \"only-if-required\": true,"
+ " \"server-hostname\": \"deneb\""
+ "},"
+ "{"
+ " \"name\": \"pxe3\","
+ " \"test\": \"option[93].hex == 0x0006\","
+ " \"only-if-required\": false,"
+ " \"boot-file-name\": \"pxelinux.0\""
+ "},"
+ "{"
+ " \"name\": \"pxe4\","
+ " \"test\": \"option[93].hex == 0x0001\","
+ " \"boot-file-name\": \"ipxe.efi\""
+ "}],"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"require-client-classes\": [ \"pxe2\" ]"
+ " } ]"
+ "}",
+
+ // Configuration 4
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"client-classes\": ["
+ "{"
+ " \"name\": \"server1\""
+ "},"
+ "{"
+ " \"name\": \"server2\""
+ "},"
+ "{"
+ " \"name\": \"telephones\","
+ " \"test\": \"option[93].hex == 0x0009\""
+ "},"
+ "{"
+ " \"name\": \"computers\","
+ " \"test\": \"option[93].hex == 0x0007\""
+ "},"
+ "{"
+ " \"name\": \"server1_and_telephones\","
+ " \"test\": \"member('server1') and member('telephones')\""
+ "},"
+ "{"
+ " \"name\": \"server1_and_computers\","
+ " \"test\": \"member('server1') and member('computers')\""
+ "},"
+ "{"
+ " \"name\": \"server2_and_telephones\","
+ " \"test\": \"member('server2') and member('telephones')\""
+ "},"
+ "{"
+ " \"name\": \"server2_and_computers\","
+ " \"test\": \"member('server2') and member('computers')\""
+ "} ],"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ "
+ " { \"pool\": \"10.0.0.10-10.0.0.49\","
+ " \"client-class\": \"server1_and_telephones\" },"
+ " { \"pool\": \"10.0.0.60-10.0.0.99\","
+ " \"client-class\": \"server1_and_computers\" },"
+ " { \"pool\": \"10.0.0.110-10.0.0.149\","
+ " \"client-class\": \"server2_and_telephones\" },"
+ " { \"pool\": \"10.0.0.160-10.0.0.199\","
+ " \"client-class\": \"server2_and_computers\" } ]"
+ " } ]"
+ "}",
+
+ // Configuration 5
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"client-classes\": ["
+ "{"
+ " \"name\": \"DROP\","
+ " \"test\": \"option[93].hex == 0x0009\""
+ "}],"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ]"
+ " } ]"
+ "}",
+
+ // Configuration 6
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"client-classes\": ["
+ "{"
+ " \"name\": \"DROP\","
+ " \"test\": \"not member('KNOWN')\""
+ "}],"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"reservations\": [ {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"hostname\": \"allowed\" } ]"
+ " } ]"
+ "}",
+
+ // Configuration 7
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"client-classes\": ["
+ "{"
+ " \"name\": \"allowed\""
+ "},"
+ "{"
+ " \"name\": \"t\","
+ " \"test\": \"member('KNOWN') or member('UNKNOWN')\""
+ "},"
+ "{"
+ " \"name\": \"DROP\","
+ " \"test\": \"not member('allowed') and member('t')\""
+ "}],"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"reservations\": [ {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"client-classes\": [ \"allowed\" ] } ]"
+ " } ]"
+ "}",
+
+ // Configuration 8
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"early-global-reservations-lookup\": true,"
+ "\"client-classes\": ["
+ "{"
+ " \"name\": \"first\""
+ "}],"
+ "\"subnet4\": ["
+ "{"
+ " \"subnet\": \"10.0.0.0/24\","
+ " \"id\": 1,"
+ " \"interface\": \"eth0\","
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"client-class\": \"first\""
+ "},"
+ "{"
+ " \"subnet\": \"10.0.1.0/24\","
+ " \"interface\": \"eth0\","
+ " \"id\": 2,"
+ " \"pools\": [ { \"pool\": \"10.0.1.10-10.0.1.100\" } ]"
+ "}],"
+ "\"reservations\": [ {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"client-classes\": [ \"first\" ] } ]"
+ "}",
+
+ // Configuration 9
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"early-global-reservations-lookup\": true,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"interface\": \"eth0\","
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ] } ],"
+ "\"reservations\": [ {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"client-classes\": [ \"DROP\" ] } ]"
+ "}",
+
+};
+
+/// @brief Test fixture class for testing classification.
+///
+/// For the time being it covers only fixed fields, but it's going to be
+/// expanded to cover other cases.
+class ClassifyTest : public Dhcpv4SrvTest {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Sets up fake interfaces.
+ ClassifyTest()
+ : Dhcpv4SrvTest(),
+ iface_mgr_test_config_(true) {
+ }
+
+ /// @brief Destructor.
+ ///
+ ~ClassifyTest() {
+ }
+
+ /// @brief Does client exchanges and checks if fixed fields have expected values.
+ ///
+ /// Depending on the value of msgtype (allowed types: DHCPDISCOVER, DHCPREQUEST or
+ /// DHCPINFORM), this method sets up the server, then conducts specified exchange
+ /// and then checks if the response contains expected values of next-server, sname
+ /// and filename fields.
+ ///
+ /// @param config server configuration to be used
+ /// @param msgtype DHCPDISCOVER, DHCPREQUEST or DHCPINFORM
+ /// @param extra_opt option to include in client messages (optional)
+ /// @param exp_next_server expected value of the next-server field
+ /// @param exp_sname expected value of the sname field
+ /// @param exp_filename expected value of the filename field
+ void
+ testFixedFields(const char* config, uint8_t msgtype, const OptionPtr& extra_opt,
+ const std::string& exp_next_server, const std::string& exp_sname,
+ const std::string& exp_filename) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ // Configure DHCP server.
+ configure(config, *client.getServer());
+
+ if (extra_opt) {
+ client.addExtraOption(extra_opt);
+ }
+
+ switch (msgtype) {
+ case DHCPDISCOVER:
+ client.doDiscover();
+ break;
+ case DHCPREQUEST:
+ client.doDORA();
+ break;
+ case DHCPINFORM:
+ // Preconfigure the client with the IP address.
+ client.createLease(IOAddress("10.0.0.56"), 600);
+
+ client.doInform(false);
+ break;
+ }
+
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+
+ EXPECT_EQ(exp_next_server, resp->getSiaddr().toText());
+
+ // This is bizarre. If I use Pkt4::MAX_SNAME_LEN in the ASSERT_GE macro,
+ // the linker will complain about it being not defined.
+ const size_t max_sname = Pkt4::MAX_SNAME_LEN;
+
+ ASSERT_GE(max_sname, exp_sname.length());
+ vector<uint8_t> sname(max_sname, 0);
+ memcpy(&sname[0], &exp_sname[0], exp_sname.size());
+ EXPECT_TRUE(std::equal(sname.begin(), sname.end(),
+ resp->getSname().begin()));
+
+ const size_t max_filename = Pkt4::MAX_FILE_LEN;
+ ASSERT_GE(max_filename, exp_filename.length());
+ vector<uint8_t> filename(max_filename, 0);
+ memcpy(&filename[0], &exp_filename[0], exp_filename.size());
+ EXPECT_TRUE(std::equal(filename.begin(), filename.end(),
+ resp->getFile().begin()));
+ }
+
+ /// @brief Interface Manager's fake configuration control.
+ IfaceMgrTestConfig iface_mgr_test_config_;
+};
+
+
+// This test checks that an incoming DISCOVER that does not match any classes
+// will get the fixed fields empty.
+TEST_F(ClassifyTest, fixedFieldsDiscoverNoClasses) {
+ testFixedFields(CONFIGS[0], DHCPDISCOVER, OptionPtr(), "0.0.0.0", "", "");
+}
+// This test checks that an incoming REQUEST that does not match any classes
+// will get the fixed fields empty.
+TEST_F(ClassifyTest, fixedFieldsRequestNoClasses) {
+ testFixedFields(CONFIGS[0], DHCPREQUEST, OptionPtr(), "0.0.0.0", "", "");
+}
+// This test checks that an incoming INFORM that does not match any classes
+// will get the fixed fields empty.
+TEST_F(ClassifyTest, fixedFieldsInformNoClasses) {
+ testFixedFields(CONFIGS[0], DHCPINFORM, OptionPtr(), "0.0.0.0", "", "");
+}
+
+
+// This test checks that an incoming DISCOVER that does match a class that has
+// next-server specified will result in a response that has the next-server set.
+TEST_F(ClassifyTest, fixedFieldsDiscoverNextServer) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0009));
+
+ testFixedFields(CONFIGS[0], DHCPDISCOVER, pxe, "1.2.3.4", "", "");
+}
+// This test checks that an incoming REQUEST that does match a class that has
+// next-server specified will result in a response that has the next-server set.
+TEST_F(ClassifyTest, fixedFieldsRequestNextServer) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0009));
+
+ testFixedFields(CONFIGS[0], DHCPREQUEST, pxe, "1.2.3.4", "", "");
+}
+// This test checks that an incoming INFORM that does match a class that has
+// next-server specified will result in a response that has the next-server set.
+TEST_F(ClassifyTest, fixedFieldsInformNextServer) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0009));
+
+ testFixedFields(CONFIGS[0], DHCPINFORM, pxe, "1.2.3.4", "", "");
+}
+
+
+// This test checks that an incoming DISCOVER that does match a class that has
+// server-hostname specified will result in a response that has the sname field set.
+TEST_F(ClassifyTest, fixedFieldsDiscoverHostname) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0007));
+
+ testFixedFields(CONFIGS[0], DHCPDISCOVER, pxe, "0.0.0.0", "deneb", "");
+}
+// This test checks that an incoming REQUEST that does match a class that has
+// server-hostname specified will result in a response that has the sname field set.
+TEST_F(ClassifyTest, fixedFieldsRequestHostname) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0007));
+
+ testFixedFields(CONFIGS[0], DHCPREQUEST, pxe, "0.0.0.0", "deneb", "");
+}
+// This test checks that an incoming INFORM that does match a class that has
+// server-hostname specified will result in a response that has the sname field set.
+TEST_F(ClassifyTest, fixedFieldsInformHostname) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0007));
+
+ testFixedFields(CONFIGS[0], DHCPINFORM, pxe, "0.0.0.0", "deneb", "");
+}
+
+
+// This test checks that an incoming DISCOVER that does match a class that has
+// boot-file-name specified will result in a response that has the filename field set.
+TEST_F(ClassifyTest, fixedFieldsDiscoverFile1) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0006));
+
+ testFixedFields(CONFIGS[0], DHCPDISCOVER, pxe, "0.0.0.0", "", "pxelinux.0");
+}
+// This test checks that an incoming REQUEST that does match a class that has
+// boot-file-name specified will result in a response that has the filename field set.
+TEST_F(ClassifyTest, fixedFieldsRequestFile1) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0006));
+
+ testFixedFields(CONFIGS[0], DHCPREQUEST, pxe, "0.0.0.0", "", "pxelinux.0");
+}
+// This test checks that an incoming INFORM that does match a class that has
+// boot-file-name specified will result in a response that has the filename field set.
+TEST_F(ClassifyTest, fixedFieldsInformFile1) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0006));
+
+ testFixedFields(CONFIGS[0], DHCPDISCOVER, pxe, "0.0.0.0", "", "pxelinux.0");
+}
+
+
+// This test checks that an incoming DISCOVER that does match a different class that has
+// boot-file-name specified will result in a response that has the filename field set.
+TEST_F(ClassifyTest, fixedFieldsDiscoverFile2) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0001));
+
+ testFixedFields(CONFIGS[0], DHCPDISCOVER, pxe, "0.0.0.0", "", "ipxe.efi");
+}
+// This test checks that an incoming REQUEST that does match a different class that has
+// boot-file-name specified will result in a response that has the filename field set.
+TEST_F(ClassifyTest, fixedFieldsRequestFile2) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0001));
+
+ testFixedFields(CONFIGS[0], DHCPREQUEST, pxe, "0.0.0.0", "", "ipxe.efi");
+}
+// This test checks that an incoming INFORM that does match a different class that has
+// boot-file-name specified will result in a response that has the filename field set.
+TEST_F(ClassifyTest, fixedFieldsInformFile2) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0001));
+
+ testFixedFields(CONFIGS[0], DHCPINFORM, pxe, "0.0.0.0", "", "ipxe.efi");
+}
+
+// This test checks that it is possible to specify static reservations for
+// client classes.
+TEST_F(ClassifyTest, clientClassesInHostReservations) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Initially, the client uses hardware address for which there are
+ // no reservations.
+ client.setHWAddress("aa:bb:cc:dd:ee:fe");
+ // DNS servers have to be requested to be returned.
+ client.requestOptions(DHO_DOMAIN_NAME_SERVERS);
+
+ // Add option 93 that matches 'pxe' class in the configuration.
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0009));
+ client.addExtraOption(pxe);
+
+ // Configure DHCP server.
+ configure(CONFIGS[1], *client.getServer());
+
+ // Perform 4-way exchange. The client's HW address doesn't match the
+ // reservations, so we expect that only 'pxe' class will be matched.
+ ASSERT_NO_THROW(client.doDORA());
+
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+
+ // 'pxe' class matches so the siaddr should be set appropriately.
+ EXPECT_EQ("1.2.3.4", resp->getSiaddr().toText());
+ // This client has no reservations for the classes associated with
+ // DNS servers and Routers options.
+ EXPECT_EQ(0, client.config_.routers_.size());
+ EXPECT_EQ(0, client.config_.dns_servers_.size());
+
+ // Modify HW address to match the reservations.
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+ ASSERT_NO_THROW(client.doDORA());
+
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+
+ // This time, the client matches 3 classes (for two it has reservations).
+ EXPECT_EQ("1.2.3.4", resp->getSiaddr().toText());
+ EXPECT_EQ(1, client.config_.routers_.size());
+ EXPECT_EQ("10.0.0.200", client.config_.routers_[0].toText());
+ EXPECT_EQ(1, client.config_.dns_servers_.size());
+ EXPECT_EQ("10.0.0.201", client.config_.dns_servers_[0].toText());
+
+ // This should also work for DHCPINFORM case.
+ ASSERT_NO_THROW(client.doInform());
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+
+ EXPECT_EQ("1.2.3.4", resp->getSiaddr().toText());
+ EXPECT_EQ(1, client.config_.routers_.size());
+ EXPECT_EQ("10.0.0.200", client.config_.routers_[0].toText());
+ EXPECT_EQ(1, client.config_.dns_servers_.size());
+ EXPECT_EQ("10.0.0.201", client.config_.dns_servers_[0].toText());
+}
+
+// This test checks that an incoming DISCOVER that does not match any classes
+// will get the fixed fields empty.
+TEST_F(ClassifyTest, fixedFieldsDiscoverNoClasses2) {
+ testFixedFields(CONFIGS[2], DHCPDISCOVER, OptionPtr(), "0.0.0.0", "", "");
+}
+// This test checks that an incoming REQUEST that does not match any classes
+// will get the fixed fields empty.
+TEST_F(ClassifyTest, fixedFieldsRequestNoClasses2) {
+ testFixedFields(CONFIGS[2], DHCPREQUEST, OptionPtr(), "0.0.0.0", "", "");
+}
+// This test checks that an incoming INFORM that does not match any classes
+// will get the fixed fields empty.
+TEST_F(ClassifyTest, fixedFieldsInformNoClasses2) {
+ testFixedFields(CONFIGS[2], DHCPINFORM, OptionPtr(), "0.0.0.0", "", "");
+}
+
+
+// This test checks that an incoming DISCOVER that does match a class that has
+// next-server specified will result in a response that has the next-server set.
+TEST_F(ClassifyTest, fixedFieldsDiscoverNextServer2) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0009));
+
+ testFixedFields(CONFIGS[2], DHCPDISCOVER, pxe, "1.2.3.4", "", "");
+}
+// This test checks that an incoming REQUEST that does match a class that has
+// next-server specified will result in a response that has the next-server set.
+TEST_F(ClassifyTest, fixedFieldsRequestNextServer2) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0009));
+
+ testFixedFields(CONFIGS[2], DHCPREQUEST, pxe, "1.2.3.4", "", "");
+}
+// This test checks that an incoming INFORM that does match a class that has
+// next-server specified will result in a response that has the next-server set.
+TEST_F(ClassifyTest, fixedFieldsInformNextServer2) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0009));
+
+ testFixedFields(CONFIGS[2], DHCPINFORM, pxe, "1.2.3.4", "", "");
+}
+
+
+// This test checks that an incoming DISCOVER that does match a class that has
+// boot-file-name specified will result in a response that has the filename field set.
+TEST_F(ClassifyTest, fixedFieldsDiscoverFile21) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0006));
+
+ testFixedFields(CONFIGS[2], DHCPDISCOVER, pxe, "0.0.0.0", "", "pxelinux.0");
+}
+// This test checks that an incoming REQUEST that does match a class that has
+// boot-file-name specified will result in a response that has the filename field set.
+TEST_F(ClassifyTest, fixedFieldsRequestFile21) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0006));
+
+ testFixedFields(CONFIGS[2], DHCPREQUEST, pxe, "0.0.0.0", "", "pxelinux.0");
+}
+// This test checks that an incoming INFORM that does match a class that has
+// boot-file-name specified will result in a response that has the filename field set.
+TEST_F(ClassifyTest, fixedFieldsInformFile21) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0006));
+
+ testFixedFields(CONFIGS[2], DHCPDISCOVER, pxe, "0.0.0.0", "", "pxelinux.0");
+}
+
+
+// This test checks that an incoming DISCOVER that does match a different class that has
+// boot-file-name specified will result in a response that has the filename field set.
+TEST_F(ClassifyTest, fixedFieldsDiscoverFile22) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0001));
+
+ testFixedFields(CONFIGS[2], DHCPDISCOVER, pxe, "0.0.0.0", "", "pxelinux.0");
+}
+// This test checks that an incoming REQUEST that does match a different class that has
+// boot-file-name specified will result in a response that has the filename field set.
+TEST_F(ClassifyTest, fixedFieldsRequestFile22) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0001));
+
+ testFixedFields(CONFIGS[2], DHCPREQUEST, pxe, "0.0.0.0", "", "pxelinux.0");
+}
+// This test checks that an incoming INFORM that does match a different class that has
+// boot-file-name specified will result in a response that has the filename field set.
+TEST_F(ClassifyTest, fixedFieldsInformFile22) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0001));
+
+ testFixedFields(CONFIGS[2], DHCPINFORM, pxe, "0.0.0.0", "", "pxelinux.0");
+}
+
+// No class
+TEST_F(ClassifyTest, fixedFieldsDiscoverNoClasses3) {
+ testFixedFields(CONFIGS[3], DHCPDISCOVER, OptionPtr(), "0.0.0.0", "", "");
+}
+TEST_F(ClassifyTest, fixedFieldsRequestNoClasses3) {
+ testFixedFields(CONFIGS[3], DHCPREQUEST, OptionPtr(), "0.0.0.0", "", "");
+}
+TEST_F(ClassifyTest, fixedFieldsInformNoClasses3) {
+ testFixedFields(CONFIGS[3], DHCPINFORM, OptionPtr(), "0.0.0.0", "", "");
+}
+
+// Class 'pxe1' is only-if-required and not subject to required evaluation
+TEST_F(ClassifyTest, fixedFieldsDiscoverNextServer3) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0009));
+
+ testFixedFields(CONFIGS[3], DHCPDISCOVER, pxe, "0.0.0.0", "", "");
+}
+TEST_F(ClassifyTest, fixedFieldsRequestNextServer3) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0009));
+
+ testFixedFields(CONFIGS[3], DHCPREQUEST, pxe, "0.0.0.0", "", "");
+}
+TEST_F(ClassifyTest, fixedFieldsInformNextServer3) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0009));
+
+ testFixedFields(CONFIGS[3], DHCPINFORM, pxe, "0.0.0.0", "", "");
+}
+
+
+// Class pxe2 is only-if-required but the subnet requires its evaluation
+TEST_F(ClassifyTest, fixedFieldsDiscoverHostname3) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0007));
+
+ testFixedFields(CONFIGS[3], DHCPDISCOVER, pxe, "0.0.0.0", "deneb", "");
+}
+TEST_F(ClassifyTest, fixedFieldsRequestHostname3) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0007));
+
+ testFixedFields(CONFIGS[3], DHCPREQUEST, pxe, "0.0.0.0", "deneb", "");
+}
+TEST_F(ClassifyTest, fixedFieldsInformHostname3) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0007));
+
+ testFixedFields(CONFIGS[3], DHCPINFORM, pxe, "0.0.0.0", "deneb", "");
+}
+
+// No change from config #0 for pxe3 and pxe4
+TEST_F(ClassifyTest, fixedFieldsDiscoverFile31) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0006));
+
+ testFixedFields(CONFIGS[3], DHCPDISCOVER, pxe, "0.0.0.0", "", "pxelinux.0");
+}
+TEST_F(ClassifyTest, fixedFieldsRequestFile31) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0006));
+
+ testFixedFields(CONFIGS[3], DHCPREQUEST, pxe, "0.0.0.0", "", "pxelinux.0");
+}
+TEST_F(ClassifyTest, fixedFieldsInformFile31) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0006));
+
+ testFixedFields(CONFIGS[3], DHCPDISCOVER, pxe, "0.0.0.0", "", "pxelinux.0");
+}
+TEST_F(ClassifyTest, fixedFieldsDiscoverFile32) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0001));
+
+ testFixedFields(CONFIGS[3], DHCPDISCOVER, pxe, "0.0.0.0", "", "ipxe.efi");
+}
+TEST_F(ClassifyTest, fixedFieldsRequestFile32) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0001));
+
+ testFixedFields(CONFIGS[3], DHCPREQUEST, pxe, "0.0.0.0", "", "ipxe.efi");
+}
+TEST_F(ClassifyTest, fixedFieldsInformFile32) {
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0001));
+
+ testFixedFields(CONFIGS[3], DHCPINFORM, pxe, "0.0.0.0", "", "ipxe.efi");
+}
+
+// This test checks the complex membership from HA with server1 telephone.
+TEST_F(ClassifyTest, server1Telephone) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ // Configure DHCP server.
+ configure(CONFIGS[4], *client.getServer());
+
+ // Add option.
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0009));
+ client.addExtraOption(pxe);
+
+ // Add server1
+ client.addClass("server1");
+
+ // Get an address
+ client.doDORA();
+
+ // Check response.
+ Pkt4Ptr resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+
+ // The address is from the first pool.
+ EXPECT_EQ("10.0.0.10", resp->getYiaddr().toText());
+}
+
+// This test checks the complex membership from HA with server1 computer.
+TEST_F(ClassifyTest, server1computer) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ // Configure DHCP server.
+ configure(CONFIGS[4], *client.getServer());
+
+ // Add option.
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0007));
+ client.addExtraOption(pxe);
+
+ // Add server1
+ client.addClass("server1");
+
+ // Get an address
+ client.doDORA();
+
+ // Check response.
+ Pkt4Ptr resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+
+ // The address is from the second pool.
+ EXPECT_EQ("10.0.0.60", resp->getYiaddr().toText());
+}
+
+// This test checks the complex membership from HA with server2 telephone.
+TEST_F(ClassifyTest, server2Telephone) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ // Configure DHCP server.
+ configure(CONFIGS[4], *client.getServer());
+
+ // Add option.
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0009));
+ client.addExtraOption(pxe);
+
+ // Add server2
+ client.addClass("server2");
+
+ // Get an address
+ client.doDORA();
+
+ // Check response.
+ Pkt4Ptr resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+
+ // The address is from the third pool.
+ EXPECT_EQ("10.0.0.110", resp->getYiaddr().toText());
+}
+
+// This test checks the complex membership from HA with server2 computer.
+TEST_F(ClassifyTest, server2computer) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ // Configure DHCP server.
+ configure(CONFIGS[4], *client.getServer());
+
+ // Add option.
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0007));
+ client.addExtraOption(pxe);
+
+ // Add server2
+ client.addClass("server2");
+
+ // Get an address
+ client.doDORA();
+
+ // Check response.
+ Pkt4Ptr resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+
+ // The address is from the forth pool.
+ EXPECT_EQ("10.0.0.160", resp->getYiaddr().toText());
+}
+
+// This test checks the precedence order in required evaluation.
+// This order is: shared-network > subnet > pools
+TEST_F(ClassifyTest, precedenceNone) {
+ std::string config =
+ "{"
+ "\"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"client-classes\": ["
+ " {"
+ " \"name\": \"all\","
+ " \"test\": \"'' == ''\""
+ " },"
+ " {"
+ " \"name\": \"for-pool\","
+ " \"test\": \"member('all')\","
+ " \"only-if-required\": true,"
+ " \"option-data\": [ {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"10.0.0.1\""
+ " } ]"
+ " },"
+ " {"
+ " \"name\": \"for-subnet\","
+ " \"test\": \"member('all')\","
+ " \"only-if-required\": true,"
+ " \"option-data\": [ {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"10.0.0.2\""
+ " } ]"
+ " },"
+ " {"
+ " \"name\": \"for-network\","
+ " \"test\": \"member('all')\","
+ " \"only-if-required\": true,"
+ " \"option-data\": [ {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"10.0.0.3\""
+ " } ]"
+ " }"
+ "],"
+ "\"shared-networks\": [ {"
+ " \"name\": \"frog\","
+ " \"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\","
+ " \"id\": 1,"
+ " \"pools\": [ { "
+ " \"pool\": \"10.0.0.10-10.0.0.100\""
+ " } ]"
+ " } ]"
+ "} ]"
+ "}";
+
+ // Create a client requesting domain-name-servers option
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ client.requestOptions(DHO_DOMAIN_NAME_SERVERS);
+
+ // Load the config and perform a DORA
+ configure(config, *client.getServer());
+ ASSERT_NO_THROW(client.doDORA());
+
+ // Check response
+ Pkt4Ptr resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ EXPECT_EQ("10.0.0.10", resp->getYiaddr().toText());
+
+ // Check domain-name-servers option
+ OptionPtr opt = resp->getOption(DHO_DOMAIN_NAME_SERVERS);
+ EXPECT_FALSE(opt);
+}
+
+// This test checks the precedence order in required evaluation.
+// This order is: shared-network > subnet > pools
+TEST_F(ClassifyTest, precedencePool) {
+ std::string config =
+ "{"
+ "\"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"client-classes\": ["
+ " {"
+ " \"name\": \"all\","
+ " \"test\": \"'' == ''\""
+ " },"
+ " {"
+ " \"name\": \"for-pool\","
+ " \"test\": \"member('all')\","
+ " \"only-if-required\": true,"
+ " \"option-data\": [ {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"10.0.0.1\""
+ " } ]"
+ " },"
+ " {"
+ " \"name\": \"for-subnet\","
+ " \"test\": \"member('all')\","
+ " \"only-if-required\": true,"
+ " \"option-data\": [ {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"10.0.0.2\""
+ " } ]"
+ " },"
+ " {"
+ " \"name\": \"for-network\","
+ " \"test\": \"member('all')\","
+ " \"only-if-required\": true,"
+ " \"option-data\": [ {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"10.0.0.3\""
+ " } ]"
+ " }"
+ "],"
+ "\"shared-networks\": [ {"
+ " \"name\": \"frog\","
+ " \"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\","
+ " \"id\": 1,"
+ " \"pools\": [ { "
+ " \"pool\": \"10.0.0.10-10.0.0.100\","
+ " \"require-client-classes\": [ \"for-pool\" ]"
+ " } ]"
+ " } ]"
+ "} ]"
+ "}";
+
+ // Create a client requesting domain-name-servers option
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ client.requestOptions(DHO_DOMAIN_NAME_SERVERS);
+
+ // Load the config and perform a DORA
+ configure(config, *client.getServer());
+ ASSERT_NO_THROW(client.doDORA());
+
+ // Check response
+ Pkt4Ptr resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ EXPECT_EQ("10.0.0.10", resp->getYiaddr().toText());
+
+ // Check domain-name-servers option
+ OptionPtr opt = resp->getOption(DHO_DOMAIN_NAME_SERVERS);
+ ASSERT_TRUE(opt);
+ Option4AddrLstPtr servers =
+ boost::dynamic_pointer_cast<Option4AddrLst>(opt);
+ ASSERT_TRUE(servers);
+ auto addrs = servers->getAddresses();
+ ASSERT_EQ(1, addrs.size());
+ EXPECT_EQ("10.0.0.1", addrs[0].toText());
+}
+
+// This test checks the precedence order in required evaluation.
+// This order is: shared-network > subnet > pools
+TEST_F(ClassifyTest, precedenceSubnet) {
+ std::string config =
+ "{"
+ "\"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"client-classes\": ["
+ " {"
+ " \"name\": \"all\","
+ " \"test\": \"'' == ''\""
+ " },"
+ " {"
+ " \"name\": \"for-pool\","
+ " \"test\": \"member('all')\","
+ " \"only-if-required\": true,"
+ " \"option-data\": [ {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"10.0.0.1\""
+ " } ]"
+ " },"
+ " {"
+ " \"name\": \"for-subnet\","
+ " \"test\": \"member('all')\","
+ " \"only-if-required\": true,"
+ " \"option-data\": [ {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"10.0.0.2\""
+ " } ]"
+ " },"
+ " {"
+ " \"name\": \"for-network\","
+ " \"test\": \"member('all')\","
+ " \"only-if-required\": true,"
+ " \"option-data\": [ {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"10.0.0.3\""
+ " } ]"
+ " }"
+ "],"
+ "\"shared-networks\": [ {"
+ " \"name\": \"frog\","
+ " \"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\","
+ " \"id\": 1,"
+ " \"require-client-classes\": [ \"for-subnet\" ],"
+ " \"pools\": [ { "
+ " \"pool\": \"10.0.0.10-10.0.0.100\","
+ " \"require-client-classes\": [ \"for-pool\" ]"
+ " } ]"
+ " } ]"
+ "} ]"
+ "}";
+
+ // Create a client requesting domain-name-servers option
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ client.requestOptions(DHO_DOMAIN_NAME_SERVERS);
+
+ // Load the config and perform a DORA
+ configure(config, *client.getServer());
+ ASSERT_NO_THROW(client.doDORA());
+
+ // Check response
+ Pkt4Ptr resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ EXPECT_EQ("10.0.0.10", resp->getYiaddr().toText());
+
+ // Check domain-name-servers option
+ OptionPtr opt = resp->getOption(DHO_DOMAIN_NAME_SERVERS);
+ ASSERT_TRUE(opt);
+ Option4AddrLstPtr servers =
+ boost::dynamic_pointer_cast<Option4AddrLst>(opt);
+ ASSERT_TRUE(servers);
+ auto addrs = servers->getAddresses();
+ ASSERT_EQ(1, addrs.size());
+ EXPECT_EQ("10.0.0.2", addrs[0].toText());
+}
+
+// This test checks the precedence order in required evaluation.
+// This order is: shared-network > subnet > pools
+TEST_F(ClassifyTest, precedenceNetwork) {
+ std::string config =
+ "{"
+ "\"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"client-classes\": ["
+ " {"
+ " \"name\": \"all\","
+ " \"test\": \"'' == ''\""
+ " },"
+ " {"
+ " \"name\": \"for-pool\","
+ " \"test\": \"member('all')\","
+ " \"only-if-required\": true,"
+ " \"option-data\": [ {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"10.0.0.1\""
+ " } ]"
+ " },"
+ " {"
+ " \"name\": \"for-subnet\","
+ " \"test\": \"member('all')\","
+ " \"only-if-required\": true,"
+ " \"option-data\": [ {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"10.0.0.2\""
+ " } ]"
+ " },"
+ " {"
+ " \"name\": \"for-network\","
+ " \"test\": \"member('all')\","
+ " \"only-if-required\": true,"
+ " \"option-data\": [ {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"10.0.0.3\""
+ " } ]"
+ " }"
+ "],"
+ "\"shared-networks\": [ {"
+ " \"name\": \"frog\","
+ " \"require-client-classes\": [ \"for-network\" ],"
+ " \"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\","
+ " \"id\": 1,"
+ " \"require-client-classes\": [ \"for-subnet\" ],"
+ " \"pools\": [ { "
+ " \"pool\": \"10.0.0.10-10.0.0.100\","
+ " \"require-client-classes\": [ \"for-pool\" ]"
+ " } ]"
+ " } ]"
+ "} ]"
+ "}";
+
+ // Create a client requesting domain-name-servers option
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ client.requestOptions(DHO_DOMAIN_NAME_SERVERS);
+
+ // Load the config and perform a DORA
+ configure(config, *client.getServer());
+ ASSERT_NO_THROW(client.doDORA());
+
+ // Check response
+ Pkt4Ptr resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ EXPECT_EQ("10.0.0.10", resp->getYiaddr().toText());
+
+ // Check domain-name-servers option
+ OptionPtr opt = resp->getOption(DHO_DOMAIN_NAME_SERVERS);
+ ASSERT_TRUE(opt);
+ Option4AddrLstPtr servers =
+ boost::dynamic_pointer_cast<Option4AddrLst>(opt);
+ ASSERT_TRUE(servers);
+ auto addrs = servers->getAddresses();
+ ASSERT_EQ(1, addrs.size());
+ EXPECT_EQ("10.0.0.3", addrs[0].toText());
+}
+
+// This test checks the handling for the DROP special class.
+TEST_F(ClassifyTest, dropClass) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ // Configure DHCP server.
+ configure(CONFIGS[5], *client.getServer());
+
+ // Send the discover.
+ client.doDiscover();
+
+ // No option: no drop.
+ EXPECT_TRUE(client.getContext().response_);
+
+ // Retry with an option matching the DROP class.
+ Dhcp4Client client2(Dhcp4Client::SELECTING);
+
+ // Add the pxe option.
+ OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0009));
+ client2.addExtraOption(pxe);
+
+ // Send the discover.
+ client2.doDiscover();
+
+ // Option, dropped.
+ EXPECT_FALSE(client2.getContext().response_);
+
+ // There should also be pkt4-receive-drop stat bumped up.
+ stats::StatsMgr& mgr = stats::StatsMgr::instance();
+ stats::ObservationPtr drop_stat = mgr.getObservation("pkt4-receive-drop");
+
+ // This statistic must be present and must be set to 1.
+ ASSERT_TRUE(drop_stat);
+ EXPECT_EQ(1, drop_stat->getInteger().first);
+}
+
+// This test checks the handling for the DROP special class at the host
+// reservation classification point with KNOWN / UNKNOWN.
+TEST_F(ClassifyTest, dropClassUnknown) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ // Configure DHCP server.
+ configure(CONFIGS[6], *client.getServer());
+
+ // Set the HW address to the reservation.
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+
+ // Send the discover.
+ client.doDiscover();
+
+ // Reservation match: no drop.
+ EXPECT_TRUE(client.getContext().response_);
+
+ // Retry with another HW address.
+ Dhcp4Client client2(Dhcp4Client::SELECTING);
+ client2.setHWAddress("aa:bb:cc:dd:ee:fe");
+
+ // Send the discover.
+ client2.doDiscover();
+
+ // No reservation, dropped.
+ EXPECT_FALSE(client2.getContext().response_);
+
+ // There should also be pkt4-receive-drop stat bumped up.
+ stats::StatsMgr& mgr = stats::StatsMgr::instance();
+ stats::ObservationPtr drop_stat = mgr.getObservation("pkt4-receive-drop");
+
+ // This statistic must be present and must be set to 1.
+ ASSERT_TRUE(drop_stat);
+ EXPECT_EQ(1, drop_stat->getInteger().first);
+}
+
+// This test checks the handling for the DROP special class at the host
+// reservation classification point with a reserved class.
+TEST_F(ClassifyTest, dropClassReservedClass) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ // Configure DHCP server.
+ configure(CONFIGS[7], *client.getServer());
+
+ // Set the HW address to the reservation.
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+
+ // Send the discover.
+ client.doDiscover();
+
+ // Reservation match: no drop.
+ EXPECT_TRUE(client.getContext().response_);
+
+ // Retry with another HW address.
+ Dhcp4Client client2(Dhcp4Client::SELECTING);
+ client2.setHWAddress("aa:bb:cc:dd:ee:fe");
+
+ // Send the discover.
+ client2.doDiscover();
+
+ // No reservation, dropped.
+ EXPECT_FALSE(client2.getContext().response_);
+
+ // There should also be pkt4-receive-drop stat bumped up.
+ stats::StatsMgr& mgr = stats::StatsMgr::instance();
+ stats::ObservationPtr drop_stat = mgr.getObservation("pkt4-receive-drop");
+
+ // This statistic must be present and must be set to 1.
+ ASSERT_TRUE(drop_stat);
+ EXPECT_EQ(1, drop_stat->getInteger().first);
+}
+
+// This test checks the early global reservations lookup for selecting
+// a guarded subnet.
+TEST_F(ClassifyTest, earlySubnet) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ // Configure DHCP server.
+ configure(CONFIGS[8], *client.getServer());
+
+ // Set the HW address to the reservation.
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+
+ // Send the discover.
+ client.doDiscover();
+
+ // Check response.
+ Pkt4Ptr resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ EXPECT_EQ("10.0.0.10", resp->getYiaddr().toText());
+
+ // Try with a different HW address.
+ Dhcp4Client client2(Dhcp4Client::SELECTING);
+
+ // Set the HW address to another value.
+ client2.setHWAddress("aa:bb:cc:01:ee:ff");
+
+ // Send the discover.
+ client2.doDiscover();
+
+ // Check response.
+ resp = client2.getContext().response_;
+ ASSERT_TRUE(resp);
+ EXPECT_EQ("10.0.1.10", resp->getYiaddr().toText());
+}
+
+// This test checks the early global reservations lookup for dropping.
+TEST_F(ClassifyTest, earlyDrop) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ // Configure DHCP server.
+ configure(CONFIGS[9], *client.getServer());
+
+ // Set the HW address to the reservation.
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+
+ // Send the discover.
+ client.doDiscover();
+
+ // Match the reservation so dropped.
+ EXPECT_FALSE(client.getContext().response_);
+
+ // There should also be pkt4-receive-drop stat bumped up.
+ stats::StatsMgr& mgr = stats::StatsMgr::instance();
+ stats::ObservationPtr drop_stat = mgr.getObservation("pkt4-receive-drop");
+
+ // This statistic must be present and must be set to 1.
+ ASSERT_TRUE(drop_stat);
+ EXPECT_EQ(1, drop_stat->getInteger().first);
+
+ // Try with a different HW address.
+ Dhcp4Client client2(Dhcp4Client::SELECTING);
+
+ // Set the HW address to another value.
+ client2.setHWAddress("aa:bb:cc:01:ee:ff");
+
+ // Send the discover.
+ client2.doDiscover();
+
+ // Not matching so not dropped.
+ EXPECT_TRUE(client2.getContext().response_);
+}
+
+} // end of anonymous namespace
diff --git a/src/bin/dhcp4/tests/client_handler_unittest.cc b/src/bin/dhcp4/tests/client_handler_unittest.cc
new file mode 100644
index 0000000..50b3ba5
--- /dev/null
+++ b/src/bin/dhcp4/tests/client_handler_unittest.cc
@@ -0,0 +1,893 @@
+// Copyright (C) 2020-2021 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <dhcp/option.h>
+#include <dhcp4/client_handler.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <stats/stats_mgr.h>
+#include <util/multi_threading_mgr.h>
+#include <unistd.h>
+
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::stats;
+using namespace isc::util;
+
+namespace {
+
+/// @brief Test fixture class for testing client handler.
+class ClientHandleTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Creates the pkt4-receive-drop statistic.
+ ClientHandleTest() : called1_(false), called2_(false), called3_(false) {
+ MultiThreadingMgr::instance().apply(false, 0, 0);
+ StatsMgr::instance().setValue("pkt4-receive-drop", static_cast<int64_t>(0));
+ }
+
+ /// @brief Destructor.
+ ///
+ /// Removes statistics.
+ ~ClientHandleTest() {
+ MultiThreadingMgr::instance().apply(false, 0, 0);
+ StatsMgr::instance().removeAll();
+ }
+
+ /// @brief Generates a client-id option.
+ ///
+ /// (from dhcp4_test_utils.h)
+ ///
+ /// @return A random client-id option.
+ OptionPtr generateClientId(uint8_t base = 100) {
+ const size_t len = 32;
+ OptionBuffer duid(len);
+ for (size_t i = 0; i < len; ++i) {
+ duid[i] = base + i;
+ }
+ return (OptionPtr(new Option(Option::V4, DHO_DHCP_CLIENT_IDENTIFIER, duid)));
+ }
+
+ /// @brief Generates a hardware address.
+ ///
+ /// (from dhcp4_test_utils.h)
+ ///
+ /// @return A random hardware address.
+ HWAddrPtr generateHWAddr(uint8_t base = 50) {
+ const uint16_t hw_type = 6;
+ const size_t len = 6;
+ OptionBuffer mac(len);
+ for (size_t i = 0; i < len; ++i) {
+ mac[i] = base + i;
+ }
+ return (HWAddrPtr(new HWAddr(mac, hw_type)));
+ }
+
+ /// @brief Check statistics.
+ ///
+ /// @param bumped True if pkt4-receive-drop should have been bumped by one,
+ /// false otherwise.
+ void checkStat(bool bumped) {
+ ObservationPtr obs = StatsMgr::instance().getObservation("pkt4-receive-drop");
+ ASSERT_TRUE(obs);
+ if (bumped) {
+ EXPECT_EQ(1, obs->getInteger().first);
+ } else {
+ EXPECT_EQ(0, obs->getInteger().first);
+ }
+ }
+
+ /// @brief Waits for pending continuations.
+ void waitForThreads() {
+ MultiThreadingMgr::instance().getThreadPool().wait(3);
+ }
+
+ /// @brief Set called1_ to true.
+ void setCalled1() {
+ called1_ = true;
+ }
+
+ /// @brief Set called2_ to true.
+ void setCalled2() {
+ called2_ = true;
+ }
+
+ /// @brief Set called3_ to true.
+ void setCalled3() {
+ called3_ = true;
+ }
+
+ /// @brief The called flag number 1.
+ bool called1_;
+
+ /// @brief The called flag number 2.
+ bool called2_;
+
+ /// @brief The called flag number 3.
+ bool called3_;
+};
+
+// Verifies behavior with empty block.
+TEST_F(ClientHandleTest, empty) {
+ try {
+ // Get a client handler.
+ ClientHandler client_handler;
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+ checkStat(false);
+}
+
+// Verifies behavior with one query.
+TEST_F(ClientHandleTest, oneQuery) {
+ // Get a query.
+ Pkt4Ptr dis(new Pkt4(DHCPDISCOVER, 1234));
+ dis->addOption(generateClientId());
+ dis->setHWAddr(generateHWAddr());
+
+ try {
+ // Get a client handler.
+ ClientHandler client_handler;
+
+ // Try to lock it.
+ bool duplicate = false;
+ EXPECT_NO_THROW(duplicate = !client_handler.tryLock(dis));
+
+ // Should return false (no duplicate).
+ EXPECT_FALSE(duplicate);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+ checkStat(false);
+}
+
+// Verifies behavior with two queries for the same client (id).
+TEST_F(ClientHandleTest, sharedQueriesById) {
+ // Get two queries.
+ Pkt4Ptr dis(new Pkt4(DHCPDISCOVER, 1234));
+ Pkt4Ptr req(new Pkt4(DHCPREQUEST, 2345));
+ OptionPtr client_id = generateClientId();
+ // Same client ID: same client.
+ dis->addOption(client_id);
+ req->addOption(client_id);
+ dis->setHWAddr(generateHWAddr());
+ req->setHWAddr(generateHWAddr(55));
+
+ try {
+ // Get a client handler.
+ ClientHandler client_handler;
+
+ // Try to lock it with the discover.
+ bool duplicate = false;
+ EXPECT_NO_THROW(duplicate = !client_handler.tryLock(dis));
+
+ // Should return false (no duplicate).
+ EXPECT_FALSE(duplicate);
+
+ // Get a second client handler.
+ ClientHandler client_handler2;
+
+ // Try to lock it with a request.
+ EXPECT_NO_THROW(duplicate = !client_handler2.tryLock(req));
+
+ // Should return true (race with the duplicate).
+ EXPECT_TRUE(duplicate);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+ checkStat(true);
+}
+
+// Verifies behavior with two queries for the same client (hw).
+TEST_F(ClientHandleTest, sharedQueriesByHWAddr) {
+ // Get two queries.
+ Pkt4Ptr dis(new Pkt4(DHCPDISCOVER, 1234));
+ Pkt4Ptr req(new Pkt4(DHCPREQUEST, 2345));
+ dis->addOption(generateClientId());
+ req->addOption(generateClientId(111));
+ HWAddrPtr hwaddr = generateHWAddr();
+ // Same hardware address: same client.
+ dis->setHWAddr(hwaddr);
+ req->setHWAddr(hwaddr);
+
+ try {
+ // Get a client handler.
+ ClientHandler client_handler;
+
+ // Try to lock it with the discover.
+ bool duplicate = false;
+ EXPECT_NO_THROW(duplicate = !client_handler.tryLock(dis));
+
+ // Should return false (no duplicate).
+ EXPECT_FALSE(duplicate);
+
+ // Get a second client handler.
+ ClientHandler client_handler2;
+
+ // Try to lock it with a request.
+ EXPECT_NO_THROW(duplicate = !client_handler2.tryLock(req));
+
+ // Should return true (race with the duplicate).
+ EXPECT_TRUE(duplicate);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+ checkStat(true);
+}
+
+// Verifies behavior with two queries for the same client (hw only).
+TEST_F(ClientHandleTest, sharedQueriesByHWAddrOnly) {
+ // Get two queries.
+ Pkt4Ptr dis(new Pkt4(DHCPDISCOVER, 1234));
+ Pkt4Ptr req(new Pkt4(DHCPREQUEST, 2345));
+ // No client ID.
+ HWAddrPtr hwaddr = generateHWAddr();
+ // Same hardware address: same client.
+ dis->setHWAddr(hwaddr);
+ req->setHWAddr(hwaddr);
+
+ try {
+ // Get a client handler.
+ ClientHandler client_handler;
+
+ // Try to lock it with the discover.
+ bool duplicate = false;
+ EXPECT_NO_THROW(duplicate = !client_handler.tryLock(dis));
+
+ // Should return false (no duplicate).
+ EXPECT_FALSE(duplicate);
+
+ // Get a second client handler.
+ ClientHandler client_handler2;
+
+ // Try to lock it with a request.
+ EXPECT_NO_THROW(duplicate = !client_handler2.tryLock(req));
+
+ // Should return true (race with the duplicate).
+ EXPECT_TRUE(duplicate);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+ checkStat(true);
+}
+
+// Verifies behavior with a sequence of two queries.
+TEST_F(ClientHandleTest, sequence) {
+ // Get two queries.
+ Pkt4Ptr dis(new Pkt4(DHCPDISCOVER, 1234));
+ Pkt4Ptr req(new Pkt4(DHCPREQUEST, 2345));
+ OptionPtr client_id = generateClientId();
+ // Same client ID: same client.
+ dis->addOption(client_id);
+ req->addOption(client_id);
+ HWAddrPtr hwaddr = generateHWAddr();
+ // Same hardware address: same client.
+ dis->setHWAddr(hwaddr);
+ req->setHWAddr(hwaddr);
+
+ try {
+ // Get a client handler.
+ ClientHandler client_handler;
+
+ // Try to lock it with the discover.
+ bool duplicate = false;
+ EXPECT_NO_THROW(duplicate = !client_handler.tryLock(dis));
+
+ // Should return false (no duplicate).
+ EXPECT_FALSE(duplicate);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+
+ // As it is a different block the lock was released.
+
+ try {
+ // Get a second client handler.
+ ClientHandler client_handler2;
+
+ // Try to lock it with a request.
+ bool duplicate = false;
+ EXPECT_NO_THROW(duplicate = !client_handler2.tryLock(req));
+
+ // Should return false (no duplicate).
+ EXPECT_FALSE(duplicate);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+ checkStat(false);
+}
+
+// Verifies behavior with different clients.
+TEST_F(ClientHandleTest, notSharedQueries) {
+ // Get two queries.
+ Pkt4Ptr dis(new Pkt4(DHCPDISCOVER, 1234));
+ Pkt4Ptr req(new Pkt4(DHCPREQUEST, 2345));
+ OptionPtr client_id = generateClientId();
+ OptionPtr client_id2 = generateClientId(111);
+ HWAddrPtr hwaddr = generateHWAddr();
+ HWAddrPtr hwaddr1 = generateHWAddr(55);
+ // Different client ID and hardware address: different client.
+ dis->addOption(client_id);
+ req->addOption(client_id2);
+ dis->setHWAddr(hwaddr);
+ req->setHWAddr(hwaddr1);
+
+ try {
+ // Get a client handler.
+ ClientHandler client_handler;
+
+ // Try to lock it with the discover.
+ bool duplicate = false;
+ EXPECT_NO_THROW(duplicate = !client_handler.tryLock(dis));
+
+ // Should return false (no duplicate).
+ EXPECT_FALSE(duplicate);
+
+ // Get a second client handler.
+ ClientHandler client_handler2;
+
+ // Try to lock it with a request.
+ EXPECT_NO_THROW(duplicate = !client_handler2.tryLock(req));
+
+ // Should return false (no duplicate).
+ EXPECT_FALSE(duplicate);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+ checkStat(false);
+}
+
+// Verifies behavior without client ID nor hardware address.
+TEST_F(ClientHandleTest, noClientIdHWAddr) {
+ // Get two queries.
+ Pkt4Ptr dis(new Pkt4(DHCPDISCOVER, 1234));
+ Pkt4Ptr req(new Pkt4(DHCPREQUEST, 2345));
+ // No client id nor hardware address: nothing to recognize the client.
+
+ try {
+ // Get a client handler.
+ ClientHandler client_handler;
+
+ // Try to lock it with the discover.
+ bool duplicate = false;
+ EXPECT_NO_THROW(duplicate = !client_handler.tryLock(dis));
+
+ // Should return false (no duplicate).
+ EXPECT_FALSE(duplicate);
+
+ // Get a second client handler.
+ ClientHandler client_handler2;
+
+ // Try to lock it with a request.
+ EXPECT_NO_THROW(duplicate = !client_handler2.tryLock(req));
+
+ // Should return false (no duplicate).
+ EXPECT_FALSE(duplicate);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+ checkStat(false);
+}
+
+// Verifies the query is required.
+TEST_F(ClientHandleTest, noQuery) {
+ Pkt4Ptr no_pkt;
+
+ try {
+ // Get a client handler.
+ ClientHandler client_handler;
+
+ EXPECT_THROW(client_handler.tryLock(no_pkt), InvalidParameter);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+}
+
+// Verifies that double tryLock call fails (id only).
+TEST_F(ClientHandleTest, doubleTryLockById) {
+ // Get a query.
+ Pkt4Ptr dis(new Pkt4(DHCPDISCOVER, 1234));
+ dis->addOption(generateClientId());
+
+ try {
+ // Get a client handler.
+ ClientHandler client_handler;
+
+ // Try to lock it.
+ bool duplicate = false;
+ EXPECT_NO_THROW(duplicate = !client_handler.tryLock(dis));
+
+ // Should return false (no duplicate).
+ EXPECT_FALSE(duplicate);
+
+ // Try to lock a second time.
+ EXPECT_THROW(client_handler.tryLock(dis), Unexpected);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+}
+
+// Verifies that double tryLock call fails (hw only).
+TEST_F(ClientHandleTest, doubleTryLockByHWAddr) {
+ // Get a query without a client ID.
+ Pkt4Ptr dis(new Pkt4(DHCPDISCOVER, 1234));
+ dis->setHWAddr(generateHWAddr());
+
+ try {
+ // Get a client handler.
+ ClientHandler client_handler;
+
+ // Try to lock it.
+ bool duplicate = false;
+ EXPECT_NO_THROW(duplicate = !client_handler.tryLock(dis));
+
+ // Should return false (no duplicate).
+ EXPECT_FALSE(duplicate);
+
+ // Try to lock a second time.
+ EXPECT_THROW(client_handler.tryLock(dis), Unexpected);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+}
+
+// Cannot verifies that empty client ID fails because getClientId() handles
+// this condition and replaces it by no client ID.
+
+// Verifies behavior with two queries for the same client and
+// multi-threading, by client id option version.
+TEST_F(ClientHandleTest, serializeTwoQueriesById) {
+ // Get two queries.
+ Pkt4Ptr dis(new Pkt4(DHCPDISCOVER, 1234));
+ Pkt4Ptr req(new Pkt4(DHCPREQUEST, 2345));
+ OptionPtr client_id = generateClientId();
+ // Same client ID: same client.
+ dis->addOption(client_id);
+ req->addOption(client_id);
+ dis->setHWAddr(generateHWAddr());
+ req->setHWAddr(generateHWAddr(55));
+
+ // Start multi-threading.
+ EXPECT_NO_THROW(MultiThreadingMgr::instance().apply(true, 1, 0));
+
+ try {
+ // Get a client handler.
+ ClientHandler client_handler;
+
+ // Create a continuation.
+ ContinuationPtr cont1 =
+ makeContinuation(std::bind(&ClientHandleTest::setCalled1, this));
+
+ // Try to lock it with the discover.
+ bool duplicate = false;
+ EXPECT_NO_THROW(duplicate = !client_handler.tryLock(dis, cont1));
+
+ // Should return false (no duplicate).
+ EXPECT_FALSE(duplicate);
+
+ // Get a second client handler.
+ ClientHandler client_handler2;
+
+ // Create a continuation.
+ ContinuationPtr cont2 =
+ makeContinuation(std::bind(&ClientHandleTest::setCalled2, this));
+
+ // Try to lock it with a request.
+ EXPECT_NO_THROW(duplicate = !client_handler2.tryLock(req, cont2));
+
+ // Should return true (race with the duplicate).
+ EXPECT_TRUE(duplicate);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+
+ // Give the second continuation a chance.
+ waitForThreads();
+
+ // Force multi-threading to stop;
+ MultiThreadingCriticalSection cs;
+
+ checkStat(false);
+ EXPECT_FALSE(called1_);
+ EXPECT_TRUE(called2_);
+}
+
+// Verifies behavior with two queries for the same client and
+// multi-threading, by hardware address version.
+TEST_F(ClientHandleTest, serializeTwoQueriesByHWAddr) {
+ // Get two queries.
+ Pkt4Ptr dis(new Pkt4(DHCPDISCOVER, 1234));
+ Pkt4Ptr req(new Pkt4(DHCPREQUEST, 2345));
+ dis->addOption(generateClientId());
+ req->addOption(generateClientId(111));
+ HWAddrPtr hwaddr = generateHWAddr();
+ // Same hardware address: same client.
+ dis->setHWAddr(hwaddr);
+ req->setHWAddr(hwaddr);
+
+ // Start multi-threading.
+ EXPECT_NO_THROW(MultiThreadingMgr::instance().apply(true, 1, 0));
+
+ try {
+ // Get a client handler.
+ ClientHandler client_handler;
+
+ // Create a continuation.
+ ContinuationPtr cont1 =
+ makeContinuation(std::bind(&ClientHandleTest::setCalled1, this));
+
+ // Try to lock it with the discover.
+ bool duplicate = false;
+ EXPECT_NO_THROW(duplicate = !client_handler.tryLock(dis, cont1));
+
+ // Should return false (no duplicate).
+ EXPECT_FALSE(duplicate);
+
+ // Get a second client handler.
+ ClientHandler client_handler2;
+
+ // Create a continuation.
+ ContinuationPtr cont2 =
+ makeContinuation(std::bind(&ClientHandleTest::setCalled2, this));
+
+ // Try to lock it with a request.
+ EXPECT_NO_THROW(duplicate = !client_handler2.tryLock(req, cont2));
+
+ // Should return true (race with the duplicate).
+ EXPECT_TRUE(duplicate);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+
+ // Give the second continuation a chance.
+ waitForThreads();
+
+ // Force multi-threading to stop;
+ MultiThreadingCriticalSection cs;
+
+ checkStat(false);
+ EXPECT_FALSE(called1_);
+ EXPECT_TRUE(called2_);
+}
+
+// Verifies behavior with two queries for the same client and multi-threading.
+// Continuations are required for serialization. By client id option version.
+TEST_F(ClientHandleTest, serializeNoContById) {
+ // Get two queries.
+ Pkt4Ptr dis(new Pkt4(DHCPDISCOVER, 1234));
+ Pkt4Ptr req(new Pkt4(DHCPREQUEST, 2345));
+ OptionPtr client_id = generateClientId();
+ // Same client ID: same client.
+ dis->addOption(client_id);
+ req->addOption(client_id);
+ dis->setHWAddr(generateHWAddr());
+ req->setHWAddr(generateHWAddr(55));
+
+ // Start multi-threading.
+ EXPECT_NO_THROW(MultiThreadingMgr::instance().apply(true, 1, 0));
+
+ try {
+ // Get a client handler.
+ ClientHandler client_handler;
+
+ // Try to lock it with the discover.
+ bool duplicate = false;
+ EXPECT_NO_THROW(duplicate = !client_handler.tryLock(dis));
+
+ // Should return false (no duplicate).
+ EXPECT_FALSE(duplicate);
+
+ // Get a second client handler.
+ ClientHandler client_handler2;
+
+ // Try to lock it with a request.
+ EXPECT_NO_THROW(duplicate = !client_handler2.tryLock(req));
+
+ // Should return true (race with the duplicate).
+ EXPECT_TRUE(duplicate);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+
+ // Give the second continuation a chance even there is none...
+ waitForThreads();
+
+ // Force multi-threading to stop;
+ MultiThreadingCriticalSection cs;
+
+ checkStat(true);
+}
+
+// Verifies behavior with two queries for the same client and multi-threading.
+// Continuations are required for serialization. By hardware address version.
+TEST_F(ClientHandleTest, serializeNoContByHWAddr) {
+ // Get two queries.
+ Pkt4Ptr dis(new Pkt4(DHCPDISCOVER, 1234));
+ Pkt4Ptr req(new Pkt4(DHCPREQUEST, 2345));
+ dis->addOption(generateClientId());
+ req->addOption(generateClientId(111));
+ HWAddrPtr hwaddr = generateHWAddr();
+ // Same hardware address: same client.
+ dis->setHWAddr(hwaddr);
+ req->setHWAddr(hwaddr);
+
+ // Start multi-threading.
+ EXPECT_NO_THROW(MultiThreadingMgr::instance().apply(true, 1, 0));
+
+ try {
+ // Get a client handler.
+ ClientHandler client_handler;
+
+ // Try to lock it with the discover.
+ bool duplicate = false;
+ EXPECT_NO_THROW(duplicate = !client_handler.tryLock(dis));
+
+ // Should return false (no duplicate).
+ EXPECT_FALSE(duplicate);
+
+ // Get a second client handler.
+ ClientHandler client_handler2;
+
+ // Try to lock it with a request.
+ EXPECT_NO_THROW(duplicate = !client_handler2.tryLock(req));
+
+ // Should return true (race with the duplicate).
+ EXPECT_TRUE(duplicate);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+
+ // Give the second continuation a chance even there is none...
+ waitForThreads();
+
+ // Force multi-threading to stop;
+ MultiThreadingCriticalSection cs;
+
+ checkStat(true);
+}
+
+// Verifies behavior with three queries for the same client and
+// multi-threading: currently we accept only two queries,
+// a third one replaces second so we get the first (oldest) query and
+// the last (newest) query when the client is busy.
+// By client id option version.
+TEST_F(ClientHandleTest, serializeThreeQueriesById) {
+ // Get two queries.
+ Pkt4Ptr dis(new Pkt4(DHCPDISCOVER, 1234));
+ Pkt4Ptr req(new Pkt4(DHCPREQUEST, 2345));
+ Pkt4Ptr rel(new Pkt4(DHCPRELEASE, 3456));
+ OptionPtr client_id = generateClientId();
+ // Same client ID: same client.
+ dis->addOption(client_id);
+ req->addOption(client_id);
+ rel->addOption(client_id);
+ dis->setHWAddr(generateHWAddr());
+ req->setHWAddr(generateHWAddr(55));
+ rel->setHWAddr(generateHWAddr(66));
+
+ // Start multi-threading.
+ EXPECT_NO_THROW(MultiThreadingMgr::instance().apply(true, 1, 0));
+
+ try {
+ // Get a client handler.
+ ClientHandler client_handler;
+
+ // Create a continuation.
+ ContinuationPtr cont1 =
+ makeContinuation(std::bind(&ClientHandleTest::setCalled1, this));
+
+ // Try to lock it with the discover.
+ bool duplicate = false;
+ EXPECT_NO_THROW(duplicate = !client_handler.tryLock(dis, cont1));
+
+ // Should return false (no duplicate).
+ EXPECT_FALSE(duplicate);
+
+ // Get a second client handler.
+ ClientHandler client_handler2;
+
+ // Create a continuation.
+ ContinuationPtr cont2 =
+ makeContinuation(std::bind(&ClientHandleTest::setCalled2, this));
+
+ // Try to lock it with a request.
+ EXPECT_NO_THROW(duplicate = !client_handler2.tryLock(req, cont2));
+
+ // Should return true (race with the duplicate).
+ EXPECT_TRUE(duplicate);
+
+ // Get a third client handler.
+ ClientHandler client_handler3;
+
+ // Create a continuation.
+ ContinuationPtr cont3 =
+ makeContinuation(std::bind(&ClientHandleTest::setCalled3, this));
+
+ // Try to lock it with a release.
+ EXPECT_NO_THROW(duplicate = !client_handler3.tryLock(rel, cont3));
+
+ // Should return true (race with the duplicate).
+ EXPECT_TRUE(duplicate);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+
+ // Give the second continuation a chance.
+ waitForThreads();
+
+ // Force multi-threading to stop;
+ MultiThreadingCriticalSection cs;
+
+ checkStat(true);
+ EXPECT_FALSE(called1_);
+ EXPECT_FALSE(called2_);
+ EXPECT_TRUE(called3_);
+}
+
+// Verifies behavior with three queries for the same client and
+// multi-threading: currently we accept only two queries,
+// a third one replaces second so we get the first (oldest) query and
+// the last (newest) query when the client is busy.
+// By hardware address version.
+TEST_F(ClientHandleTest, serializeThreeQueriesHWAddr) {
+ // Get two queries.
+ Pkt4Ptr dis(new Pkt4(DHCPDISCOVER, 1234));
+ Pkt4Ptr req(new Pkt4(DHCPREQUEST, 2345));
+ Pkt4Ptr rel(new Pkt4(DHCPRELEASE, 3456));
+ dis->addOption(generateClientId());
+ req->addOption(generateClientId(111));
+ rel->addOption(generateClientId(99));
+ HWAddrPtr hwaddr = generateHWAddr();
+ // Same hardware address: same client.
+ dis->setHWAddr(hwaddr);
+ req->setHWAddr(hwaddr);
+ rel->setHWAddr(hwaddr);
+
+ // Start multi-threading.
+ EXPECT_NO_THROW(MultiThreadingMgr::instance().apply(true, 1, 0));
+
+ try {
+ // Get a client handler.
+ ClientHandler client_handler;
+
+ // Create a continuation.
+ ContinuationPtr cont1 =
+ makeContinuation(std::bind(&ClientHandleTest::setCalled1, this));
+
+ // Try to lock it with the discover.
+ bool duplicate = false;
+ EXPECT_NO_THROW(duplicate = !client_handler.tryLock(dis, cont1));
+
+ // Should return false (no duplicate).
+ EXPECT_FALSE(duplicate);
+
+ // Get a second client handler.
+ ClientHandler client_handler2;
+
+ // Create a continuation.
+ ContinuationPtr cont2 =
+ makeContinuation(std::bind(&ClientHandleTest::setCalled2, this));
+
+ // Try to lock it with a request.
+ EXPECT_NO_THROW(duplicate = !client_handler2.tryLock(req, cont2));
+
+ // Should return true (race with the duplicate).
+ EXPECT_TRUE(duplicate);
+
+ // Get a third client handler.
+ ClientHandler client_handler3;
+
+ // Create a continuation.
+ ContinuationPtr cont3 =
+ makeContinuation(std::bind(&ClientHandleTest::setCalled3, this));
+
+ // Try to lock it with a release.
+ EXPECT_NO_THROW(duplicate = !client_handler3.tryLock(rel, cont3));
+
+ // Should return true (race with the duplicate).
+ EXPECT_TRUE(duplicate);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+
+ // Give the second continuation a chance.
+ waitForThreads();
+
+ // Force multi-threading to stop;
+ MultiThreadingCriticalSection cs;
+
+ checkStat(true);
+ EXPECT_FALSE(called1_);
+ EXPECT_FALSE(called2_);
+ EXPECT_TRUE(called3_);
+}
+
+// Verifies behavior with three queries for the same client and
+// multi-threading: currently we accept only two queries,
+// a third one replaces second so we get the first (oldest) query and
+// the last (newest) query when the client is busy.
+// Mixed version (hardware address then client id option duplicates).
+// Note the system is transitive because further races are detected
+// when serialized packet processing is performed.
+TEST_F(ClientHandleTest, serializeThreeQueriesMixed) {
+ // Get two queries.
+ Pkt4Ptr dis(new Pkt4(DHCPDISCOVER, 1234));
+ Pkt4Ptr req(new Pkt4(DHCPREQUEST, 2345));
+ Pkt4Ptr rel(new Pkt4(DHCPRELEASE, 3456));
+ HWAddrPtr hwaddr = generateHWAddr();
+ // Same hardware address: same client for discover and request.
+ dis->setHWAddr(hwaddr);
+ req->setHWAddr(hwaddr);
+ rel->setHWAddr(generateHWAddr(55));
+ OptionPtr client_id = generateClientId();
+ // Same client ID: same client for discover and release.
+ dis->addOption(client_id);
+ req->addOption(generateClientId(111));
+ rel->addOption(client_id);
+
+ // Start multi-threading.
+ EXPECT_NO_THROW(MultiThreadingMgr::instance().apply(true, 1, 0));
+
+ try {
+ // Get a client handler.
+ ClientHandler client_handler;
+
+ // Create a continuation.
+ ContinuationPtr cont1 =
+ makeContinuation(std::bind(&ClientHandleTest::setCalled1, this));
+
+ // Try to lock it with the discover.
+ bool duplicate = false;
+ EXPECT_NO_THROW(duplicate = !client_handler.tryLock(dis, cont1));
+
+ // Should return false (no duplicate).
+ EXPECT_FALSE(duplicate);
+
+ // Get a second client handler.
+ ClientHandler client_handler2;
+
+ // Create a continuation.
+ ContinuationPtr cont2 =
+ makeContinuation(std::bind(&ClientHandleTest::setCalled2, this));
+
+ // Try to lock it with a request.
+ EXPECT_NO_THROW(duplicate = !client_handler2.tryLock(req, cont2));
+
+ // Should return true (race with the duplicate).
+ EXPECT_TRUE(duplicate);
+
+ // Get a third client handler.
+ ClientHandler client_handler3;
+
+ // Create a continuation.
+ ContinuationPtr cont3 =
+ makeContinuation(std::bind(&ClientHandleTest::setCalled3, this));
+
+ // Try to lock it with a release.
+ EXPECT_NO_THROW(duplicate = !client_handler3.tryLock(rel, cont3));
+
+ // Should return true (race with the duplicate).
+ EXPECT_TRUE(duplicate);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+
+ // Give the second continuation a chance.
+ waitForThreads();
+
+ // Force multi-threading to stop;
+ MultiThreadingCriticalSection cs;
+
+ checkStat(true);
+ EXPECT_FALSE(called1_);
+ EXPECT_FALSE(called2_);
+ EXPECT_TRUE(called3_);
+}
+
+} // end of anonymous namespace
diff --git a/src/bin/dhcp4/tests/config_backend_unittest.cc b/src/bin/dhcp4/tests/config_backend_unittest.cc
new file mode 100644
index 0000000..f3aa486
--- /dev/null
+++ b/src/bin/dhcp4/tests/config_backend_unittest.cc
@@ -0,0 +1,566 @@
+// Copyright (C) 2019-2023 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <arpa/inet.h>
+#include <gtest/gtest.h>
+
+#include <database/backend_selector.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcp4/dhcp4_srv.h>
+#include <dhcp4/ctrl_dhcp4_srv.h>
+#include <dhcp4/json_config_parser.h>
+#include <dhcpsrv/subnet.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/cfg_subnets4.h>
+#include <dhcpsrv/testutils/generic_backend_unittest.h>
+#include <dhcpsrv/testutils/test_config_backend_dhcp4.h>
+
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <dhcp4/tests/get_config_unittest.h>
+
+#include <boost/foreach.hpp>
+#include <boost/scoped_ptr.hpp>
+
+#include <iostream>
+#include <fstream>
+#include <sstream>
+#include <limits.h>
+
+using namespace isc::asiolink;
+using namespace isc::config;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::db;
+using namespace std;
+
+namespace {
+
+/// @brief Test fixture for testing external configuration merging
+class Dhcp4CBTest : public GenericBackendTest {
+protected:
+ /// @brief Pre test set up
+ /// Called prior to each test. It creates two configuration backends
+ /// that differ by host name ("db1" and "db2"). It then registers
+ /// a backend factory that will return them rather than create
+ /// new instances. The backends need to pre-exist so they can be
+ /// populated prior to calling server configure. It uses
+ /// TestConfigBackend instances but with a type of "memfile" to pass
+ /// parsing. Doing it all here allows us to use ASSERTs if we feel like
+ /// it.
+ virtual void SetUp() {
+ DatabaseConnection::ParameterMap params;
+ params[std::string("type")] = std::string("memfile");
+ params[std::string("host")] = std::string("db1");
+ db1_.reset(new TestConfigBackendDHCPv4(params));
+
+ params[std::string("host")] = std::string("db2");
+ db2_.reset(new TestConfigBackendDHCPv4(params));
+
+ ConfigBackendDHCPv4Mgr::instance().registerBackendFactory("memfile",
+ [this](const DatabaseConnection::ParameterMap& params)
+ -> ConfigBackendDHCPv4Ptr {
+ auto host = params.find("host");
+ if (host != params.end()) {
+ if (host->second == "db1") {
+ return (db1_);
+ } else if (host->second == "db2") {
+ return (db2_);
+ }
+ }
+
+ // Apparently we're looking for a new one.
+ return (TestConfigBackendDHCPv4Ptr(new TestConfigBackendDHCPv4(params)));
+ });
+ }
+
+ /// @brief Clean up after each test
+ virtual void TearDown() {
+ // Unregister the factory to be tidy.
+ ConfigBackendDHCPv4Mgr::instance().unregisterBackendFactory("memfile");
+ }
+
+public:
+
+ /// Constructor
+ Dhcp4CBTest()
+ : rcode_(-1), db1_selector("db1"), db2_selector("db1") {
+ // Open port 0 means to not do anything at all. We don't want to
+ // deal with sockets here, just check if configuration handling
+ // is sane.
+ srv_.reset(new ControlledDhcpv4Srv(0));
+
+ // Create fresh context.
+ resetConfiguration();
+ }
+
+ /// Destructor
+ virtual ~Dhcp4CBTest() {
+ resetConfiguration();
+ };
+
+ /// @brief Reset configuration singletons.
+ void resetConfiguration() {
+ CfgMgr::instance().clear();
+ ConfigBackendDHCPv4Mgr::destroy();
+ }
+
+ /// @brief Convenience method for running configuration
+ ///
+ /// This method does not throw, but signals errors using gtest macros.
+ ///
+ /// @param config text to be parsed as JSON
+ /// @param expected_code expected code (see cc/command_interpreter.h)
+ /// @param exp_error expected text error (check skipped if empty)
+ void configure(std::string config, int expected_code,
+ std::string exp_error = "") {
+ ConstElementPtr json;
+ try {
+ json = parseDHCP4(config, true);
+ } catch(const std::exception& ex) {
+ ADD_FAILURE() << "parseDHCP4 failed: " << ex.what();
+ }
+
+ ConstElementPtr status;
+ ASSERT_NO_THROW(status = configureDhcp4Server(*srv_, json));
+ ASSERT_TRUE(status);
+
+ int rcode;
+ ConstElementPtr comment = parseAnswerText(rcode, status);
+ ASSERT_EQ(expected_code, rcode) << " comment: "
+ << comment->stringValue();
+
+ string text;
+ ASSERT_NO_THROW(text = comment->stringValue());
+
+ if (expected_code != rcode) {
+ std::cout << "Reported status: " << text << std::endl;
+ }
+
+ if ((rcode != 0)) {
+ if (!exp_error.empty()) {
+ ASSERT_EQ(exp_error, text);
+ }
+ }
+ }
+
+ boost::scoped_ptr<Dhcpv4Srv> srv_; ///< DHCP4 server under test
+ int rcode_; ///< Return code from element parsing
+ ConstElementPtr comment_; ///< Reason for parse fail
+
+ BackendSelector db1_selector; ///< BackendSelector by host for first config backend
+ BackendSelector db2_selector; ///< BackendSelector by host for second config backend
+
+ TestConfigBackendDHCPv4Ptr db1_; ///< First configuration backend instance
+ TestConfigBackendDHCPv4Ptr db2_; ///< Second configuration backend instance
+};
+
+// This test verifies that externally configured globals are
+// merged correctly into staging configuration.
+TEST_F(Dhcp4CBTest, mergeGlobals) {
+ string base_config =
+ "{ \n"
+ " \"interfaces-config\": { \n"
+ " \"interfaces\": [\"*\" ] \n"
+ " }, \n"
+ " \"echo-client-id\": false, \n"
+ " \"decline-probation-period\": 7000, \n"
+ " \"valid-lifetime\": 1000, \n"
+ " \"rebind-timer\": 800, \n"
+ " \"server-hostname\": \"overwrite.me.com\", \n"
+ " \"config-control\": { \n"
+ " \"config-databases\": [ { \n"
+ " \"type\": \"memfile\", \n"
+ " \"host\": \"db1\" \n"
+ " },{ \n"
+ " \"type\": \"memfile\", \n"
+ " \"host\": \"db2\" \n"
+ " } \n"
+ " ] \n"
+ " } \n"
+ "} \n";
+
+ extractConfig(base_config);
+
+ // Make some globals:
+ StampedValuePtr server_hostname(new StampedValue("server-hostname", "isc.example.org"));
+ StampedValuePtr decline_period(new StampedValue("decline-probation-period", Element::create(86400)));
+ StampedValuePtr calc_tee_times(new StampedValue("calculate-tee-times", Element::create(bool(false))));
+ StampedValuePtr t2_percent(new StampedValue("t2-percent", Element::create(0.75)));
+ StampedValuePtr renew_timer(new StampedValue("renew-timer", Element::create(500)));
+
+ // Let's add all of the globals to the second backend. This will verify
+ // we find them there.
+ db2_->createUpdateGlobalParameter4(ServerSelector::ALL(), server_hostname);
+ db2_->createUpdateGlobalParameter4(ServerSelector::ALL(), decline_period);
+ db2_->createUpdateGlobalParameter4(ServerSelector::ALL(), calc_tee_times);
+ db2_->createUpdateGlobalParameter4(ServerSelector::ALL(), t2_percent);
+ db2_->createUpdateGlobalParameter4(ServerSelector::ALL(), renew_timer);
+
+ // Should parse and merge without error.
+ ASSERT_NO_FATAL_FAILURE(configure(base_config, CONTROL_RESULT_SUCCESS, ""));
+
+ // Verify the composite staging is correct. (Remember that
+ // CfgMgr::instance().commit() hasn't been called)
+ SrvConfigPtr staging_cfg = CfgMgr::instance().getStagingCfg();
+
+ // echo-client-id is set explicitly in the original config, meanwhile
+ // the backend config does not set it, so the explicit value wins.
+ EXPECT_FALSE(staging_cfg->getEchoClientId());
+
+ // decline-probation-period is an explicit member that should come
+ // from the backend.
+ EXPECT_EQ(86400, staging_cfg->getDeclinePeriod());
+
+ // Verify that the implicit globals from JSON are there.
+ ASSERT_NO_FATAL_FAILURE(checkConfiguredGlobal(staging_cfg, "valid-lifetime",
+ Element::create(1000)));
+ ASSERT_NO_FATAL_FAILURE(checkConfiguredGlobal(staging_cfg, "rebind-timer",
+ Element::create(800)));
+
+ // Verify that the implicit globals from the backend are there.
+ ASSERT_NO_FATAL_FAILURE(checkConfiguredGlobal(staging_cfg, server_hostname));
+ ASSERT_NO_FATAL_FAILURE(checkConfiguredGlobal(staging_cfg, calc_tee_times));
+ ASSERT_NO_FATAL_FAILURE(checkConfiguredGlobal(staging_cfg, t2_percent));
+ ASSERT_NO_FATAL_FAILURE(checkConfiguredGlobal(staging_cfg, renew_timer));
+}
+
+// This test verifies that externally configured option definitions
+// merged correctly into staging configuration.
+TEST_F(Dhcp4CBTest, mergeOptionDefs) {
+ string base_config =
+ "{ \n"
+ " \"option-def\": [ { \n"
+ " \"name\": \"one\", \n"
+ " \"code\": 1, \n"
+ " \"type\": \"ipv4-address\", \n"
+ " \"space\": \"isc\" \n"
+ " }, \n"
+ " { \n"
+ " \"name\": \"two\", \n"
+ " \"code\": 2, \n"
+ " \"type\": \"string\", \n"
+ " \"space\": \"isc\" \n"
+ " } \n"
+ " ], \n"
+ " \"config-control\": { \n"
+ " \"config-databases\": [ { \n"
+ " \"type\": \"memfile\", \n"
+ " \"host\": \"db1\" \n"
+ " },{ \n"
+ " \"type\": \"memfile\", \n"
+ " \"host\": \"db2\" \n"
+ " } \n"
+ " ] \n"
+ " } \n"
+ "} \n";
+
+ extractConfig(base_config);
+
+ // Create option one replacement and add it to first backend.
+ OptionDefinitionPtr def;
+ def.reset(new OptionDefinition("one", 101, "isc", "uint16"));
+ db1_->createUpdateOptionDef4(ServerSelector::ALL(), def);
+
+ // Create option three and add it to first backend.
+ def.reset(new OptionDefinition("three", 3, "isc", "string"));
+ db1_->createUpdateOptionDef4(ServerSelector::ALL(), def);
+
+ // Create option four and add it to second backend.
+ def.reset(new OptionDefinition("four", 4, "isc", "string"));
+ db2_->createUpdateOptionDef4(ServerSelector::ALL(), def);
+
+ // Should parse and merge without error.
+ ASSERT_NO_FATAL_FAILURE(configure(base_config, CONTROL_RESULT_SUCCESS, ""));
+
+ // Verify the composite staging is correct.
+ SrvConfigPtr staging_cfg = CfgMgr::instance().getStagingCfg();
+ ConstCfgOptionDefPtr option_defs = staging_cfg->getCfgOptionDef();
+
+ // Definition "one" from first backend should be there.
+ OptionDefinitionPtr found_def = option_defs->get("isc", "one");
+ ASSERT_TRUE(found_def);
+ EXPECT_EQ(101, found_def->getCode());
+ EXPECT_EQ(OptionDataType::OPT_UINT16_TYPE, found_def->getType());
+
+ // Definition "two" from JSON config should be there.
+ found_def = option_defs->get("isc", "two");
+ ASSERT_TRUE(found_def);
+ EXPECT_EQ(2, found_def->getCode());
+
+ // Definition "three" from first backend should be there.
+ found_def = option_defs->get("isc", "three");
+ ASSERT_TRUE(found_def);
+ EXPECT_EQ(3, found_def->getCode());
+
+ // Definition "four" from first backend should not be there.
+ found_def = option_defs->get("isc", "four");
+ ASSERT_FALSE(found_def);
+}
+
+// This test verifies that externally configured options
+// merged correctly into staging configuration.
+TEST_F(Dhcp4CBTest, mergeOptions) {
+ string base_config =
+ "{ \n"
+ " \"option-data\": [ { \n"
+ " \"name\": \"dhcp-message\", \n"
+ " \"data\": \"0A0B0C0D\", \n"
+ " \"csv-format\": false \n"
+ " },{ \n"
+ " \"name\": \"host-name\", \n"
+ " \"data\": \"old.example.com\", \n"
+ " \"csv-format\": true \n"
+ " } \n"
+ " ], \n"
+ " \"config-control\": { \n"
+ " \"config-databases\": [ { \n"
+ " \"type\": \"memfile\", \n"
+ " \"host\": \"db1\" \n"
+ " },{ \n"
+ " \"type\": \"memfile\", \n"
+ " \"host\": \"db2\" \n"
+ " } \n"
+ " ] \n"
+ " } \n"
+ "} \n";
+
+ extractConfig(base_config);
+
+ OptionDescriptorPtr opt;
+
+ // Add host-name to the first backend.
+ opt.reset(new OptionDescriptor(
+ createOption<OptionString>(Option::V4, DHO_HOST_NAME,
+ true, false, false,
+ "new.example.com")));
+ opt->space_name_ = DHCP4_OPTION_SPACE;
+ db1_->createUpdateOption4(ServerSelector::ALL(), opt);
+
+ // Add boot-file-name to the first backend.
+ opt.reset(new OptionDescriptor(
+ createOption<OptionString>(Option::V4, DHO_BOOT_FILE_NAME,
+ true, false, false,
+ "my-boot-file")));
+ opt->space_name_ = DHCP4_OPTION_SPACE;
+ db1_->createUpdateOption4(ServerSelector::ALL(), opt);
+
+ // Add boot-file-name to the second backend.
+ opt.reset(new OptionDescriptor(
+ createOption<OptionString>(Option::V4, DHO_BOOT_FILE_NAME,
+ true, false, false,
+ "your-boot-file")));
+ opt->space_name_ = DHCP4_OPTION_SPACE;
+ db2_->createUpdateOption4(ServerSelector::ALL(), opt);
+
+ // Should parse and merge without error.
+ ASSERT_NO_FATAL_FAILURE(configure(base_config, CONTROL_RESULT_SUCCESS, ""));
+
+ // Verify the composite staging is correct.
+ SrvConfigPtr staging_cfg = CfgMgr::instance().getStagingCfg();
+
+ // Option definition from JSON should be there.
+ CfgOptionPtr options = staging_cfg->getCfgOption();
+
+ // dhcp-message should come from the original config.
+ OptionDescriptor found_opt =
+ options->get(DHCP4_OPTION_SPACE, DHO_DHCP_MESSAGE);
+ ASSERT_TRUE(found_opt.option_);
+ EXPECT_EQ("0x0A0B0C0D", found_opt.option_->toHexString());
+
+ // host-name should come from the first back end,
+ // (overwriting the original).
+ found_opt = options->get(DHCP4_OPTION_SPACE, DHO_HOST_NAME);
+ ASSERT_TRUE(found_opt.option_);
+ EXPECT_EQ("new.example.com", found_opt.option_->toString());
+
+ // booth-file-name should come from the first back end.
+ found_opt = options->get(DHCP4_OPTION_SPACE, DHO_BOOT_FILE_NAME);
+ ASSERT_TRUE(found_opt.option_);
+ EXPECT_EQ("my-boot-file", found_opt.option_->toString());
+}
+
+// This test verifies that DHCP options fetched from the config backend
+// encapsulate their suboptions.
+TEST_F(Dhcp4CBTest, mergeOptionsWithSuboptions) {
+ string base_config =
+ "{ \n"
+ " \"option-def\": [ { \n"
+ " \"name\": \"vendor-suboption-1\", \n"
+ " \"code\": 1, \n"
+ " \"type\": \"string\", \n"
+ " \"space\": \"vendor-encapsulated-options-space\" \n"
+ " }], \n"
+ " \"config-control\": { \n"
+ " \"config-databases\": [ { \n"
+ " \"type\": \"memfile\", \n"
+ " \"host\": \"db1\" \n"
+ " },{ \n"
+ " \"type\": \"memfile\", \n"
+ " \"host\": \"db2\" \n"
+ " } \n"
+ " ] \n"
+ " } \n"
+ "} \n";
+
+ extractConfig(base_config);
+
+ // Create option 43 instance and store it in the database.
+ OptionDescriptorPtr opt;
+ opt.reset(new OptionDescriptor(
+ createEmptyOption(Option::V4, DHO_VENDOR_ENCAPSULATED_OPTIONS,
+ true, false)));
+ opt->space_name_ = DHCP4_OPTION_SPACE;
+ db1_->createUpdateOption4(ServerSelector::ALL(), opt);
+
+ // Create option 43 suboption and store it in the database.
+ opt.reset(new OptionDescriptor(
+ createOption<OptionString>(Option::V4, 1, true, false, false,
+ "http://server:8080")
+ )
+ );
+ opt->space_name_ = VENDOR_ENCAPSULATED_OPTION_SPACE;
+ db1_->createUpdateOption4(ServerSelector::ALL(), opt);
+
+ // Fetch the configuration from the config backend.
+ ASSERT_NO_FATAL_FAILURE(configure(base_config, CONTROL_RESULT_SUCCESS, ""));
+
+ auto staging_cfg = CfgMgr::instance().getStagingCfg();
+
+ // Make sure that option 43 has been fetched.
+ auto found_opt_desc = staging_cfg->getCfgOption()->
+ get(DHCP4_OPTION_SPACE, DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ ASSERT_TRUE(found_opt_desc.option_);
+
+ // Make sure that the option 43 contains its suboption.
+ auto found_subopt = found_opt_desc.option_->getOption(1);
+ EXPECT_TRUE(found_subopt);
+}
+
+// This test verifies that externally configured shared-networks are
+// merged correctly into staging configuration.
+TEST_F(Dhcp4CBTest, mergeSharedNetworks) {
+ string base_config =
+ "{ \n"
+ " \"interfaces-config\": { \n"
+ " \"interfaces\": [\"*\" ] \n"
+ " }, \n"
+ " \"valid-lifetime\": 4000, \n"
+ " \"config-control\": { \n"
+ " \"config-databases\": [ { \n"
+ " \"type\": \"memfile\", \n"
+ " \"host\": \"db1\" \n"
+ " },{ \n"
+ " \"type\": \"memfile\", \n"
+ " \"host\": \"db2\" \n"
+ " } \n"
+ " ] \n"
+ " }, \n"
+ " \"shared-networks\": [ { \n"
+ " \"name\": \"two\" \n"
+ " }] \n"
+ "} \n";
+
+ extractConfig(base_config);
+
+ // Make a few networks
+ SharedNetwork4Ptr network1(new SharedNetwork4("one"));
+ SharedNetwork4Ptr network3(new SharedNetwork4("three"));
+
+ // Add network1 to db1 and network3 to db2
+ db1_->createUpdateSharedNetwork4(ServerSelector::ALL(), network1);
+ db2_->createUpdateSharedNetwork4(ServerSelector::ALL(), network3);
+
+ // Should parse and merge without error.
+ ASSERT_NO_FATAL_FAILURE(configure(base_config, CONTROL_RESULT_SUCCESS, ""));
+
+ // Verify the composite staging is correct. (Remember that
+ // CfgMgr::instance().commit() hasn't been called)
+ SrvConfigPtr staging_cfg = CfgMgr::instance().getStagingCfg();
+
+ CfgSharedNetworks4Ptr networks = staging_cfg->getCfgSharedNetworks4();
+ SharedNetwork4Ptr staged_network;
+
+ // SharedNetwork One should have been added from db1 config
+ staged_network = networks->getByName("one");
+ ASSERT_TRUE(staged_network);
+
+ // Subnet2 should have come from the json config
+ staged_network = networks->getByName("two");
+ ASSERT_TRUE(staged_network);
+
+ // Subnet3, which is in db2 should not have been merged.
+ // We queried db1 first and the query returned data. In
+ // other words, we iterate over the backends, asking for
+ // data. We use the first data, we find.
+ staged_network = networks->getByName("three");
+ ASSERT_FALSE(staged_network);
+}
+
+// This test verifies that externally configured subnets are
+// merged correctly into staging configuration.
+TEST_F(Dhcp4CBTest, mergeSubnets) {
+ string base_config =
+ "{ \n"
+ " \"interfaces-config\": { \n"
+ " \"interfaces\": [\"*\" ] \n"
+ " }, \n"
+ " \"valid-lifetime\": 4000, \n"
+ " \"config-control\": { \n"
+ " \"config-databases\": [ { \n"
+ " \"type\": \"memfile\", \n"
+ " \"host\": \"db1\" \n"
+ " },{ \n"
+ " \"type\": \"memfile\", \n"
+ " \"host\": \"db2\" \n"
+ " } \n"
+ " ] \n"
+ " }, \n"
+ " \"subnet4\": [ \n"
+ " { \n"
+ " \"id\": 2,\n"
+ " \"subnet\": \"192.0.3.0/24\" \n"
+ " } ]\n"
+ "} \n";
+
+ extractConfig(base_config);
+
+ // Make a few subnets
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, SubnetID(1)));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.4.0"), 26, 1, 2, 3, SubnetID(3)));
+
+ // Add subnet1 to db1 and subnet3 to db2
+ db1_->createUpdateSubnet4(ServerSelector::ALL(), subnet1);
+ db2_->createUpdateSubnet4(ServerSelector::ALL(), subnet3);
+
+ // Should parse and merge without error.
+ configure(base_config, CONTROL_RESULT_SUCCESS, "");
+
+ // Verify the composite staging is correct. (Remember that
+ // CfgMgr::instance().commit() hasn't been called)
+
+ SrvConfigPtr staging_cfg = CfgMgr::instance().getStagingCfg();
+
+ CfgSubnets4Ptr subnets = staging_cfg->getCfgSubnets4();
+ ConstSubnet4Ptr staged_subnet;
+
+ // Subnet1 should have been added from db1 config
+ staged_subnet = subnets->getBySubnetId(1);
+ ASSERT_TRUE(staged_subnet);
+
+ // Subnet2 should have come from the json config
+ staged_subnet = subnets->getBySubnetId(2);
+ ASSERT_TRUE(staged_subnet);
+
+ // Subnet3, which is in db2 should not have been merged, since it is
+ // first found, first used?
+ staged_subnet = subnets->getBySubnetId(3);
+ ASSERT_FALSE(staged_subnet);
+}
+
+}
diff --git a/src/bin/dhcp4/tests/config_parser_unittest.cc b/src/bin/dhcp4/tests/config_parser_unittest.cc
new file mode 100644
index 0000000..2d98963
--- /dev/null
+++ b/src/bin/dhcp4/tests/config_parser_unittest.cc
@@ -0,0 +1,7835 @@
+// Copyright (C) 2012-2023 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <arpa/inet.h>
+#include <gtest/gtest.h>
+
+#include <cc/command_interpreter.h>
+#include <dhcp4/dhcp4_srv.h>
+#include <dhcp4/ctrl_dhcp4_srv.h>
+#include <dhcp4/json_config_parser.h>
+#include <dhcp/option4_addrlst.h>
+#include <dhcp/option_custom.h>
+#include <dhcp/option_int.h>
+#include <dhcp/docsis3_option_defs.h>
+#include <dhcp/classify.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcpsrv/subnet.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/cfg_expiration.h>
+#include <dhcpsrv/cfg_hosts.h>
+#include <dhcpsrv/cfg_subnets4.h>
+#include <dhcpsrv/parsers/simple_parser4.h>
+#include <dhcpsrv/testutils/config_result_check.h>
+#include <dhcpsrv/testutils/test_config_backend_dhcp4.h>
+#include <process/config_ctl_info.h>
+#include <hooks/hooks_manager.h>
+#include <stats/stats_mgr.h>
+#include <testutils/log_utils.h>
+#include <testutils/gtest_utils.h>
+#include <testutils/test_to_element.h>
+#include <util/chrono_time_utils.h>
+#include <util/doubles.h>
+
+#include "marker_file.h"
+#include "test_libraries.h"
+#include "test_data_files_config.h"
+#include "dhcp4_test_utils.h"
+#include "get_config_unittest.h"
+
+#include <boost/foreach.hpp>
+#include <boost/scoped_ptr.hpp>
+
+#include <iostream>
+#include <fstream>
+#include <sstream>
+#include <limits.h>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::config;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::hooks;
+using namespace std;
+
+namespace {
+
+const char* PARSER_CONFIGS[] = {
+ // CONFIGURATION 0: one subnet with one pool, no user contexts
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [\"*\" ]"
+ " },"
+ " \"valid-lifetime\": 4000,"
+ " \"rebind-timer\": 2000,"
+ " \"renew-timer\": 1000,"
+ " \"subnet4\": [ {"
+ " \"pools\": [ "
+ " { \"pool\": \"192.0.2.0/28\" }"
+ " ],"
+ " \"id\": 1,"
+ " \"subnet\": \"192.0.2.0/24\""
+ " } ]"
+ "}",
+
+ // Configuration 1: one pool with empty user context
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [\"*\" ]"
+ " },"
+ " \"valid-lifetime\": 4000,"
+ " \"rebind-timer\": 2000,"
+ " \"renew-timer\": 1000,"
+ " \"subnet4\": [ {"
+ " \"pools\": [ "
+ " { \"pool\": \"192.0.2.0/28\","
+ " \"user-context\": {"
+ " }"
+ " }"
+ " ],"
+ " \"id\": 1,"
+ " \"subnet\": \"192.0.2.0/24\""
+ " } ]"
+ "}",
+
+ // Configuration 2: one pool with user context containing lw4over6 parameters
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [\"*\" ]"
+ " },"
+ " \"valid-lifetime\": 4000,"
+ " \"rebind-timer\": 2000,"
+ " \"renew-timer\": 1000,"
+ " \"subnet4\": [ {"
+ " \"pools\": [ "
+ " { \"pool\": \"192.0.2.0/28\","
+ " \"user-context\": {"
+ " \"integer-param\": 42,"
+ " \"string-param\": \"Sagittarius\","
+ " \"bool-param\": true"
+ " }"
+ " }"
+ " ],"
+ " \"id\": 1,"
+ " \"subnet\": \"192.0.2.0/24\""
+ " } ]"
+ "}",
+
+ // Configuration 3: one min-max pool with user context containing lw4over6 parameters
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [\"*\" ]"
+ " },"
+ " \"valid-lifetime\": 4000,"
+ " \"rebind-timer\": 2000,"
+ " \"renew-timer\": 1000,"
+ " \"subnet4\": [ {"
+ " \"pools\": [ "
+ " { \"pool\": \"192.0.2.0 - 192.0.2.15\","
+ " \"user-context\": {"
+ " \"integer-param\": 42,"
+ " \"string-param\": \"Sagittarius\","
+ " \"bool-param\": true"
+ " }"
+ " }"
+ " ],"
+ " \"id\": 1,"
+ " \"subnet\": \"192.0.2.0/24\""
+ " } ]"
+ "}",
+
+ // Configuration 4: two host databases
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [\"*\" ]"
+ " },"
+ " \"valid-lifetime\": 4000,"
+ " \"rebind-timer\": 2000,"
+ " \"renew-timer\": 1000,"
+ " \"hosts-databases\": [ {"
+ " \"type\": \"mysql\","
+ " \"name\": \"keatest1\","
+ " \"user\": \"keatest\","
+ " \"password\": \"keatest\""
+ " },{"
+ " \"type\": \"mysql\","
+ " \"name\": \"keatest2\","
+ " \"user\": \"keatest\","
+ " \"password\": \"keatest\""
+ " }"
+ " ]"
+ "}",
+
+ // Configuration 5 for comments
+ "{"
+ " \"comment\": \"A DHCPv4 server\","
+ " \"interfaces-config\": {"
+ " \"comment\": \"Use wildcard\","
+ " \"interfaces\": [ \"*\" ] },"
+ " \"option-def\": [ {"
+ " \"comment\": \"An option definition\","
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"ipv4-address\","
+ " \"space\": \"isc\""
+ " } ],"
+ " \"option-data\": [ {"
+ " \"comment\": \"Set option value\","
+ " \"name\": \"dhcp-message\","
+ " \"data\": \"ABCDEF0105\","
+ " \"csv-format\": false"
+ " } ],"
+ " \"client-classes\": ["
+ " {"
+ " \"comment\": \"match all\","
+ " \"name\": \"all\","
+ " \"test\": \"'' == ''\""
+ " },"
+ " {"
+ " \"name\": \"none\""
+ " },"
+ " {"
+ " \"comment\": \"a comment\","
+ " \"name\": \"both\","
+ " \"user-context\": {"
+ " \"version\": 1"
+ " }"
+ " }"
+ " ],"
+ " \"control-socket\": {"
+ " \"socket-type\": \"unix\","
+ " \"socket-name\": \"/tmp/kea4-ctrl-socket\","
+ " \"user-context\": { \"comment\": \"Indirect comment\" }"
+ " },"
+ " \"shared-networks\": [ {"
+ " \"comment\": \"A shared network\","
+ " \"name\": \"foo\","
+ " \"subnet4\": ["
+ " { "
+ " \"comment\": \"A subnet\","
+ " \"subnet\": \"192.0.1.0/24\","
+ " \"id\": 100,"
+ " \"pools\": ["
+ " {"
+ " \"comment\": \"A pool\","
+ " \"pool\": \"192.0.1.1-192.0.1.10\""
+ " }"
+ " ],"
+ " \"reservations\": ["
+ " {"
+ " \"comment\": \"A host reservation\","
+ " \"hw-address\": \"AA:BB:CC:DD:EE:FF\","
+ " \"hostname\": \"foo.example.com\","
+ " \"option-data\": [ {"
+ " \"comment\": \"An option in a reservation\","
+ " \"name\": \"domain-name\","
+ " \"data\": \"example.com\""
+ " } ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " } ],"
+ " \"dhcp-ddns\": {"
+ " \"comment\": \"No dynamic DNS\","
+ " \"enable-updates\": false"
+ " }"
+ "}",
+
+ // Configuration 6: config databases
+ "{ \n"
+ " \"interfaces-config\": { \n"
+ " \"interfaces\": [\"*\" ] \n"
+ " }, \n"
+ " \"valid-lifetime\": 4000, \n"
+ " \"rebind-timer\": 2000, \n"
+ " \"renew-timer\": 1000, \n"
+ " \"config-control\": { \n"
+ " \"config-fetch-wait-time\": 10, \n"
+ " \"config-databases\": [ { \n"
+ " \"type\": \"mysql\", \n"
+ " \"name\": \"keatest1\", \n"
+ " \"user\": \"keatest\", \n"
+ " \"password\": \"keatest\" \n"
+ " },{ \n"
+ " \"type\": \"mysql\", \n"
+ " \"name\": \"keatest2\", \n"
+ " \"user\": \"keatest\", \n"
+ " \"password\": \"keatest\" \n"
+ " } \n"
+ " ] \n"
+ " } \n"
+ "} \n"
+};
+
+class Dhcp4ParserTest : public LogContentTest {
+protected:
+ // Check that no hooks libraries are loaded. This is a pre-condition for
+ // a number of tests, so is checked in one place. As this uses an
+ // ASSERT call - and it is not clear from the documentation that Gtest
+ // predicates can be used in a constructor - the check is placed in SetUp.
+ virtual void SetUp() {
+ std::vector<std::string> libraries = HooksManager::getLibraryNames();
+ ASSERT_TRUE(libraries.empty());
+ }
+
+public:
+ Dhcp4ParserTest()
+ : rcode_(-1) {
+ // Open port 0 means to not do anything at all. We don't want to
+ // deal with sockets here, just check if configuration handling
+ // is sane.
+ srv_.reset(new ControlledDhcpv4Srv(0));
+ // Create fresh context.
+ resetConfiguration();
+ }
+
+public:
+
+ // Checks if the result of DHCP server configuration has
+ // expected code (0 for success, other for failures).
+ // Also stores result in rcode_ and comment_.
+ void checkResult(ConstElementPtr status, int expected_code) {
+ ASSERT_TRUE(status);
+ comment_ = parseAnswerText(rcode_, status);
+ EXPECT_EQ(expected_code, rcode_) << "error text:" << comment_->stringValue();
+ }
+
+ // Checks if the result of DHCP server configuration has
+ // expected code (0 for success, other for failures) and
+ // the text part. Also stores result in rcode_ and comment_.
+ void checkResult(ConstElementPtr status, int expected_code,
+ string expected_txt) {
+ ASSERT_TRUE(status);
+ comment_ = parseAnswer(rcode_, status);
+ EXPECT_EQ(expected_code, rcode_) << "error text:" << comment_->stringValue();
+ ASSERT_TRUE(comment_);
+ ASSERT_EQ(Element::string, comment_->getType());
+ EXPECT_EQ(expected_txt, comment_->stringValue());
+ }
+
+ /// @brief Convenience method for running configuration
+ ///
+ /// This method does not throw, but signals errors using gtest macros.
+ ///
+ /// @param config text to be parsed as JSON
+ /// @param expected_code expected code (see cc/command_interpreter.h)
+ /// @param exp_error expected text error (check skipped if empty)
+ void configure(std::string config, int expected_code,
+ std::string exp_error = "") {
+ ConstElementPtr json;
+ try {
+ json = parseDHCP4(config, true);
+ } catch(const std::exception& ex) {
+ ADD_FAILURE() << "parseDHCP4 failed: " << ex.what();
+ }
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+
+ int rcode;
+ ConstElementPtr comment = parseAnswerText(rcode, status);
+ EXPECT_EQ(expected_code, rcode);
+
+ string text;
+ ASSERT_TRUE(comment);
+ ASSERT_NO_THROW(text = comment->stringValue());
+
+ if (expected_code != rcode) {
+ std::cout << "Reported status: " << text << std::endl;
+ }
+
+ if ((rcode != 0)) {
+ if (!exp_error.empty()) {
+ EXPECT_EQ(exp_error, text);
+ }
+ }
+ }
+
+ ~Dhcp4ParserTest() {
+ resetConfiguration();
+
+ // ... and delete the hooks library marker files if present
+ static_cast<void>(remove(LOAD_MARKER_FILE));
+ static_cast<void>(remove(UNLOAD_MARKER_FILE));
+ };
+
+ /// @brief Returns an interface configuration used by the most of the
+ /// unit tests.
+ std::string genIfaceConfig() const {
+ return ("\"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "}");
+ }
+
+ /// @brief Create the simple configuration with single option.
+ ///
+ /// This function allows to set one of the parameters that configure
+ /// option value. These parameters are: "name", "code", "data",
+ /// "csv-format" and "space".
+ ///
+ /// @param param_value string holding option parameter value to be
+ /// injected into the configuration string.
+ /// @param parameter name of the parameter to be configured with
+ /// param value.
+ /// @return configuration string containing custom values of parameters
+ /// describing an option.
+ std::string createConfigWithOption(const std::string& param_value,
+ const std::string& parameter) {
+ std::map<std::string, std::string> params;
+ if (parameter == "name") {
+ params["name"] = param_value;
+ params["space"] = DHCP4_OPTION_SPACE;
+ params["code"] = "56";
+ params["data"] = "ABCDEF0105";
+ params["csv-format"] = "false";
+ } else if (parameter == "space") {
+ params["name"] = "dhcp-message";
+ params["space"] = param_value;
+ params["code"] = "56";
+ params["data"] = "ABCDEF0105";
+ params["csv-format"] = "false";
+ } else if (parameter == "code") {
+ params["name"] = "dhcp-message";
+ params["space"] = DHCP4_OPTION_SPACE;
+ params["code"] = param_value;
+ params["data"] = "ABCDEF0105";
+ params["csv-format"] = "false";
+ } else if (parameter == "data") {
+ params["name"] = "dhcp-message";
+ params["space"] = DHCP4_OPTION_SPACE;
+ params["code"] = "56";
+ params["data"] = param_value;
+ params["csv-format"] = "false";
+ } else if (parameter == "csv-format") {
+ params["name"] = "dhcp-message";
+ params["space"] = DHCP4_OPTION_SPACE;
+ params["code"] = "56";
+ params["data"] = "ABCDEF0105";
+ params["csv-format"] = param_value;
+ }
+ return (createConfigWithOption(params));
+ }
+
+ /// @brief Create simple configuration with single option.
+ ///
+ /// This function creates a configuration for a single option with
+ /// custom values for all parameters that describe the option.
+ ///
+ /// @params params map holding parameters and their values.
+ /// @return configuration string containing custom values of parameters
+ /// describing an option.
+ std::string createConfigWithOption(const std::map<std::string, std::string>& params) {
+ std::ostringstream stream;
+ stream << "{ " << genIfaceConfig() << "," <<
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"option-data\": [ {";
+ bool first = true;
+ typedef std::pair<std::string, std::string> ParamPair;
+ BOOST_FOREACH(ParamPair param, params) {
+ if (!first) {
+ stream << ", ";
+ } else {
+ // cppcheck-suppress unreadVariable
+ first = false;
+ }
+ if (param.first == "name") {
+ stream << "\"name\": \"" << param.second << "\"";
+ } else if (param.first == "space") {
+ stream << "\"space\": \"" << param.second << "\"";
+ } else if (param.first == "code") {
+ stream << "\"code\": " << param.second << "";
+ } else if (param.first == "data") {
+ stream << "\"data\": \"" << param.second << "\"";
+ } else if (param.first == "csv-format") {
+ stream << "\"csv-format\": " << param.second;
+ }
+ }
+ stream <<
+ " } ]"
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+ return (stream.str());
+ }
+
+ /// @brief Returns an option from the subnet.
+ ///
+ /// This function returns an option from a subnet to which the
+ /// specified subnet address belongs. The option is identified
+ /// by its code.
+ ///
+ /// @param subnet_address Address which belongs to the subnet from
+ /// which the option is to be returned.
+ /// @param option_code Code of the option to be returned.
+ /// @param expected_options_count Expected number of options in
+ /// the particular subnet.
+ ///
+ /// @return Descriptor of the option. If the descriptor holds a
+ /// NULL option pointer, it means that there was no such option
+ /// in the subnet.
+ OptionDescriptor
+ getOptionFromSubnet(const IOAddress& subnet_address,
+ const uint16_t option_code,
+ const uint16_t expected_options_count = 1) {
+ Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
+ getCfgSubnets4()->selectSubnet(subnet_address);
+ if (!subnet) {
+ /// @todo replace toText() with the use of operator <<.
+ ADD_FAILURE() << "A subnet for the specified address "
+ << subnet_address.toText()
+ << "does not exist in Config Manager";
+ }
+ OptionContainerPtr options =
+ subnet->getCfgOption()->getAll(DHCP4_OPTION_SPACE);
+ if (expected_options_count != options->size()) {
+ ADD_FAILURE() << "The number of options in the subnet '"
+ << subnet_address.toText() << "' is different "
+ " than expected number of options '"
+ << expected_options_count << "'";
+ }
+
+ // Get the search index. Index #1 is to search using option code.
+ const OptionContainerTypeIndex& idx = options->get<1>();
+
+ // Get the options for specified index. Expecting one option to be
+ // returned but in theory we may have multiple options with the same
+ // code so we get the range.
+ std::pair<OptionContainerTypeIndex::const_iterator,
+ OptionContainerTypeIndex::const_iterator> range =
+ idx.equal_range(option_code);
+ if (std::distance(range.first, range.second) > 1) {
+ ADD_FAILURE() << "There is more than one option having the"
+ " option code '" << option_code << "' in a subnet '"
+ << subnet_address.toText() << "'. Expected "
+ " at most one option";
+ } else if (std::distance(range.first, range.second) == 0) {
+ return (OptionDescriptor(OptionPtr(), false, false));
+ }
+
+ return (*range.first);
+ }
+
+ /// @brief Test invalid option parameter value.
+ ///
+ /// This test function constructs the simple configuration
+ /// string and injects invalid option configuration into it.
+ /// It expects that parser will fail with provided option code.
+ ///
+ /// @param param_value string holding invalid option parameter value
+ /// to be injected into configuration string.
+ /// @param parameter name of the parameter to be configured with
+ /// param_value (can be any of "name", "code", "data")
+ void testInvalidOptionParam(const std::string& param_value,
+ const std::string& parameter) {
+ ConstElementPtr x;
+ std::string config = createConfigWithOption(param_value, parameter);
+ ConstElementPtr json = parseDHCP4(config);
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 1);
+ EXPECT_TRUE(errorContainsPosition(x, "<string>"));
+ }
+
+ /// @brief Test invalid option parameter value.
+ ///
+ /// This test function constructs the simple configuration
+ /// string and injects invalid option configuration into it.
+ /// It expects that parser will fail with provided option code.
+ ///
+ /// @param params Map of parameters defining an option.
+ void
+ testInvalidOptionParam(const std::map<std::string, std::string>& params) {
+ ConstElementPtr x;
+ std::string config = createConfigWithOption(params);
+ ConstElementPtr json = parseDHCP4(config);
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 1);
+ EXPECT_TRUE(errorContainsPosition(x, "<string>"));
+ }
+
+ /// @brief Test option against given code and data.
+ ///
+ /// @param option_desc option descriptor that carries the option to
+ /// be tested.
+ /// @param expected_code expected code of the option.
+ /// @param expected_data expected data in the option.
+ /// @param expected_data_len length of the reference data.
+ /// @param extra_data if true extra data is allowed in an option
+ /// after tested data.
+ void testOption(const OptionDescriptor& option_desc,
+ uint16_t expected_code, const uint8_t* expected_data,
+ size_t expected_data_len,
+ bool extra_data = false) {
+ // Check if option descriptor contains valid option pointer.
+ ASSERT_TRUE(option_desc.option_);
+ // Verify option type.
+ EXPECT_EQ(expected_code, option_desc.option_->getType());
+ // We may have many different option types being created. Some of them
+ // have dedicated classes derived from Option class. In such case if
+ // we want to verify the option contents against expected_data we have
+ // to prepare raw buffer with the contents of the option. The easiest
+ // way is to call pack() which will prepare on-wire data.
+ util::OutputBuffer buf(option_desc.option_->getData().size());
+ option_desc.option_->pack(buf);
+ if (extra_data) {
+ // The length of the buffer must be at least equal to size of the
+ // reference data but it can sometimes be greater than that. This is
+ // because some options carry suboptions that increase the overall
+ // length.
+ ASSERT_GE(buf.getLength() - option_desc.option_->getHeaderLen(),
+ expected_data_len);
+ } else {
+ ASSERT_EQ(buf.getLength() - option_desc.option_->getHeaderLen(),
+ expected_data_len);
+ }
+ // Verify that the data is correct. Do not verify suboptions and a header.
+ const uint8_t* data = static_cast<const uint8_t*>(buf.getData());
+ EXPECT_EQ(0, memcmp(expected_data, data + option_desc.option_->getHeaderLen(),
+ expected_data_len));
+ }
+
+ /// @brief Test option configuration.
+ ///
+ /// This function creates a configuration for a specified option using
+ /// a map of parameters specified as the argument. The map holds
+ /// name/value pairs which identifies option's configuration parameters:
+ /// - name
+ /// - space
+ /// - code
+ /// - data
+ /// - csv-format.
+ /// This function applies a new server configuration and checks that the
+ /// option being configured is inserted into CfgMgr. The raw contents of
+ /// this option are compared with the binary data specified as expected
+ /// data passed to this function.
+ ///
+ /// @param params Map of parameters defining an option.
+ /// @param option_code Option code.
+ /// @param expected_data Array containing binary data expected to be stored
+ /// in the configured option.
+ /// @param expected_data_len Length of the array holding reference data.
+ void testConfiguration(const std::map<std::string, std::string>& params,
+ const uint16_t option_code,
+ const uint8_t* expected_data,
+ const size_t expected_data_len) {
+ std::string config = createConfigWithOption(params);
+ ASSERT_TRUE(executeConfiguration(config, "parse option configuration"));
+ // The subnet should now hold one option with the specified option code.
+ OptionDescriptor desc =
+ getOptionFromSubnet(IOAddress("192.0.2.24"), option_code);
+ ASSERT_TRUE(desc.option_);
+ testOption(desc, option_code, expected_data, expected_data_len);
+ }
+
+ /// @brief Parse and Execute configuration
+ ///
+ /// Parses a configuration and executes a configuration of the server.
+ /// If the operation fails, the current test will register a failure.
+ ///
+ /// @param config Configuration to parse
+ /// @param operation Operation being performed. In the case of an error,
+ /// the error text will include the string "unable to <operation>.".
+ ///
+ /// @return true if the configuration succeeded, false if not. In the
+ /// latter case, a failure will have been added to the current test.
+ bool
+ executeConfiguration(const std::string& config, const char* operation) {
+ CfgMgr::instance().clear();
+ ConstElementPtr json;
+ ConstElementPtr status;
+ try {
+ json = parseJSON(config);
+ status = Dhcpv4SrvTest::configure(*srv_, json);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "Unable to " << operation << ". "
+ << "The following configuration was used: " << std::endl
+ << config << std::endl
+ << " and the following error message was returned:"
+ << ex.what() << std::endl;
+ return (false);
+ }
+
+ // The status object must not be NULL
+ if (!status) {
+ ADD_FAILURE() << "Unable to " << operation << ". "
+ << "The configuration function returned a null pointer.";
+ return (false);
+ }
+
+ // Store the answer if we need it.
+
+ // Returned value should be 0 (configuration success)
+ comment_ = parseAnswerText(rcode_, status);
+ if (rcode_ != 0) {
+ string reason = "";
+ if (comment_) {
+ reason = string(" (") + comment_->stringValue() + string(")");
+ }
+ ADD_FAILURE() << "Unable to " << operation << ". "
+ << "The configuration function returned error code "
+ << rcode_ << reason;
+ return (false);
+ }
+
+ return (true);
+ }
+
+ /// @brief Reset configuration database.
+ ///
+ /// This function resets configuration data base by
+ /// removing all subnets and option-data. Reset must
+ /// be performed after each test to make sure that
+ /// contents of the database do not affect result of
+ /// subsequent tests.
+ void resetConfiguration() {
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"hooks-libraries\": [ ], "
+ "\"valid-lifetime\": 4000, "
+ "\"subnet4\": [ ], "
+ "\"dhcp-ddns\": { \"enable-updates\" : false }, "
+ "\"option-def\": [ ], "
+ "\"option-data\": [ ] }";
+ CfgMgr::instance().rollback();
+ static_cast<void>(executeConfiguration(config,
+ "reset configuration database"));
+ CfgMgr::instance().clear();
+ }
+
+ /// @brief Retrieve an option associated with a host.
+ ///
+ /// The option is retrieved from the "dhcp4" option space.
+ ///
+ /// @param host Reference to a host for which an option should be retrieved.
+ /// @param option_code Option code.
+ /// @tparam ReturnType Type of the pointer object returned.
+ ///
+ /// @return Pointer to an option or NULL pointer if not found.
+ template<typename ReturnType>
+ ReturnType
+ retrieveOption(const Host& host, const uint16_t option_code) const {
+ return (retrieveOption<ReturnType>(host, DHCP4_OPTION_SPACE, option_code));
+ }
+
+ /// @brief Retrieve an option associated with a host.
+ ///
+ /// @param host Reference to a host for which an option should be retrieved.
+ /// @param space Option space from which option should be retrieved.
+ /// @param option_code Option code.
+ /// @tparam ReturnType Type of the pointer object returned.
+ ///
+ /// @return Pointer to an option or NULL pointer if not found.
+ template<typename ReturnType>
+ ReturnType
+ retrieveOption(const Host& host, const std::string& space,
+ const uint16_t option_code) const {
+ ConstCfgOptionPtr cfg_option = host.getCfgOption4();
+ if (cfg_option) {
+ OptionDescriptor opt_desc = cfg_option->get(space, option_code);
+ if (opt_desc.option_) {
+ return (boost::dynamic_pointer_cast<
+ typename ReturnType::element_type>(opt_desc.option_));
+ }
+ }
+ return (ReturnType());
+ }
+
+ /// @brief Checks if specified subnet is part of the collection
+ ///
+ /// @tparam CollectionType type of subnet4 collections i.e.
+ /// either Subnet4SimpleCollection or Subnet4Collection
+ /// @param col collection of subnets to be inspected
+ /// @param subnet text notation (e.g. 192.0.2.0/24)
+ /// @param t1 expected renew-timer value
+ /// @param t2 expected rebind-timer value
+ /// @param valid expected valid-lifetime value
+ /// @param min_valid expected min-valid-lifetime value
+ /// (0 (default) means same as valid)
+ /// @param max_valid expected max-valid-lifetime value
+ /// (0 (default) means same as valid)
+ /// @return the subnet that was examined
+ template <typename CollectionType>
+ Subnet4Ptr
+ checkSubnet(const CollectionType& col, std::string subnet,
+ uint32_t t1, uint32_t t2, uint32_t valid,
+ uint32_t min_valid = 0, uint32_t max_valid = 0) {
+ const auto& index = col.template get<SubnetPrefixIndexTag>();
+ auto subnet_it = index.find(subnet);
+ if (subnet_it == index.cend()) {
+ ADD_FAILURE() << "Unable to find expected subnet " << subnet;
+ return (Subnet4Ptr());
+ }
+ Subnet4Ptr s = *subnet_it;
+
+ EXPECT_EQ(t1, s->getT1().get());
+ EXPECT_EQ(t2, s->getT2().get());
+ EXPECT_EQ(valid, s->getValid().get());
+ EXPECT_EQ(min_valid ? min_valid : valid, s->getValid().getMin());
+ EXPECT_EQ(max_valid ? max_valid : valid, s->getValid().getMax());
+
+ return (s);
+ }
+
+ /// @brief This utility method attempts to configure using specified
+ /// config and then returns requested pool from requested subnet
+ ///
+ /// @param config configuration to be applied
+ /// @param subnet_index index of the subnet to be returned (0 - the first subnet)
+ /// @param pool_index index of the pool within a subnet (0 - the first pool)
+ /// @param pool [out] Pool pointer will be stored here (if found)
+ void getPool(const std::string& config, size_t subnet_index,
+ size_t pool_index, PoolPtr& pool) {
+ ConstElementPtr status;
+ ConstElementPtr json;
+
+ EXPECT_NO_THROW(json = parseDHCP4(config, true));
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ ConstCfgSubnets4Ptr subnets4 = CfgMgr::instance().getStagingCfg()->getCfgSubnets4();
+ ASSERT_TRUE(subnets4);
+
+ const Subnet4Collection* subnets = subnets4->getAll();
+ ASSERT_TRUE(subnets);
+ ASSERT_GE(subnets->size(), subnet_index + 1);
+
+ auto subnet = subnets->begin();
+ // std::advance is not available for subnets iterators.
+ for (size_t i = 0; i < subnet_index; ++i) {
+ subnet = std::next(subnet);
+ }
+ const PoolCollection pools = (*subnet)->getPools(Lease::TYPE_V4);
+ ASSERT_GE(pools.size(), pool_index + 1);
+
+ pool = pools.at(pool_index);
+ EXPECT_TRUE(pool);
+ }
+
+ /// @brief Tests if the current config has a given global parameter value
+ /// @param name name of the global parameter expected to exist
+ /// @param value expected value of the global parameter
+ template <typename ValueType>
+ void checkGlobal(const std::string name, ValueType value) {
+ ConstElementPtr param;
+ ConstElementPtr exp_value;
+ param = CfgMgr::instance().getStagingCfg()->getConfiguredGlobal(name);
+ ASSERT_TRUE(param) << "global: " << name << ", expected but not found";
+ ASSERT_NO_THROW(exp_value = Element::create(value));
+ EXPECT_TRUE(param->equals(*exp_value)) << "global: " << name
+ << isc::data::prettyPrint(param)
+ << " does not match expected: "
+ << isc::data::prettyPrint(exp_value);
+ }
+
+ boost::scoped_ptr<Dhcpv4Srv> srv_; ///< DHCP4 server under test
+ int rcode_; ///< Return code from element parsing
+ ConstElementPtr comment_; ///< Reason for parse fail
+ isc::dhcp::ClientClasses classify_; ///< used in client classification
+};
+
+/// The goal of this test is to verify that the code accepts only
+/// valid commands and malformed or unsupported parameters are rejected.
+TEST_F(Dhcp4ParserTest, bogusCommand) {
+
+ ConstElementPtr x;
+
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_,
+ parseJSON("{\"bogus\": 5}")));
+
+ // returned value must be 1 (configuration parse error)
+ checkResult(x, 1);
+
+ // it should be refused by syntax too
+ EXPECT_THROW(parseDHCP4("{\"bogus\": 5}"), Dhcp4ParseError);
+}
+
+/// The goal of this test is to verify empty interface-config is accepted.
+TEST_F(Dhcp4ParserTest, emptyInterfaceConfig) {
+
+ ConstElementPtr json;
+ EXPECT_NO_THROW(json = parseDHCP4("{ \"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"valid-lifetime\": 4000 }"));
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // returned value should be 0 (success)
+ checkResult(status, 0);
+}
+
+/// The goal of this test is to verify if wrongly defined subnet will
+/// be rejected. Properly defined subnet must include at least one
+/// pool definition.
+TEST_F(Dhcp4ParserTest, emptySubnet) {
+
+ std::string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ ], "
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ EXPECT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // returned value should be 0 (success)
+ checkResult(status, 0);
+}
+
+/// Check that valid-lifetime must be between min-valid-lifetime and
+/// max-valid-lifetime when a bound is specified, *AND* a subnet is
+/// specified (boundary check is done when lifetimes are applied).
+TEST_F(Dhcp4ParserTest, outBoundValidLifetime) {
+
+ string too_small = "{ " + genIfaceConfig() + "," +
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 1000, \"min-valid-lifetime\": 2000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(too_small));
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ string expected = "subnet configuration failed: "
+ "the value of min-valid-lifetime (2000) is not "
+ "less than (default) valid-lifetime (1000)";
+ checkResult(status, 1, expected);
+ resetConfiguration();
+
+ string too_large = "{ " + genIfaceConfig() + "," +
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 2000, \"max-valid-lifetime\": 1000 }";
+
+ ASSERT_NO_THROW(json = parseDHCP4(too_large));
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ expected = "subnet configuration failed: "
+ "the value of (default) valid-lifetime (2000) is not "
+ "less than max-valid-lifetime (1000)";
+ checkResult(status, 1, expected);
+ resetConfiguration();
+
+ string before = "{ " + genIfaceConfig() + "," +
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 1000, \"min-valid-lifetime\": 2000, "
+ "\"max-valid-lifetime\": 4000 }";
+
+ ASSERT_NO_THROW(json = parseDHCP4(before));
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ expected = "subnet configuration failed: "
+ "the value of (default) valid-lifetime (1000) is not "
+ "between min-valid-lifetime (2000) and max-valid-lifetime (4000)";
+ checkResult(status, 1, expected);
+ resetConfiguration();
+
+ string after = "{ " + genIfaceConfig() + "," +
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 5000, \"min-valid-lifetime\": 1000, "
+ "\"max-valid-lifetime\": 4000 }";
+
+ ASSERT_NO_THROW(json = parseDHCP4(after));
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ expected = "subnet configuration failed: "
+ "the value of (default) valid-lifetime (5000) is not "
+ "between min-valid-lifetime (1000) and max-valid-lifetime (4000)";
+ checkResult(status, 1, expected);
+ resetConfiguration();
+
+ string crossed = "{ " + genIfaceConfig() + "," +
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 1500, \"min-valid-lifetime\": 2000, "
+ "\"max-valid-lifetime\": 1000 }";
+
+ ASSERT_NO_THROW(json = parseDHCP4(crossed));
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ expected = "subnet configuration failed: "
+ "the value of min-valid-lifetime (2000) is not "
+ "less than max-valid-lifetime (1000)";
+ checkResult(status, 1, expected);
+}
+
+/// Check that valid-lifetime must be between min-valid-lifetime and
+/// max-valid-lifetime when a bound is specified. Check on global
+/// parameters only.
+TEST_F(Dhcp4ParserTest, outBoundGlobalValidLifetime) {
+
+ string too_small = "{ " + genIfaceConfig() + "," +
+ "\"valid-lifetime\": 1000, \"min-valid-lifetime\": 2000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(too_small));
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ string expected =
+ "the value of min-valid-lifetime (2000) is not "
+ "less than (default) valid-lifetime (1000)";
+ checkResult(status, 1, expected);
+ resetConfiguration();
+
+ string too_large = "{ " + genIfaceConfig() + "," +
+ "\"valid-lifetime\": 2000, \"max-valid-lifetime\": 1000 }";
+
+ ASSERT_NO_THROW(json = parseDHCP4(too_large));
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ expected =
+ "the value of (default) valid-lifetime (2000) is not "
+ "less than max-valid-lifetime (1000)";
+ checkResult(status, 1, expected);
+ resetConfiguration();
+
+ string before = "{ " + genIfaceConfig() + "," +
+ "\"valid-lifetime\": 1000, \"min-valid-lifetime\": 2000, "
+ "\"max-valid-lifetime\": 4000 }";
+
+ ASSERT_NO_THROW(json = parseDHCP4(before));
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ expected =
+ "the value of (default) valid-lifetime (1000) is not "
+ "between min-valid-lifetime (2000) and max-valid-lifetime (4000)";
+ checkResult(status, 1, expected);
+ resetConfiguration();
+
+ string after = "{ " + genIfaceConfig() + "," +
+ "\"valid-lifetime\": 5000, \"min-valid-lifetime\": 1000, "
+ "\"max-valid-lifetime\": 4000 }";
+
+ ASSERT_NO_THROW(json = parseDHCP4(after));
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ expected =
+ "the value of (default) valid-lifetime (5000) is not "
+ "between min-valid-lifetime (1000) and max-valid-lifetime (4000)";
+ checkResult(status, 1, expected);
+ resetConfiguration();
+
+ string crossed = "{ " + genIfaceConfig() + "," +
+ "\"valid-lifetime\": 1500, \"min-valid-lifetime\": 2000, "
+ "\"max-valid-lifetime\": 1000 }";
+
+ ASSERT_NO_THROW(json = parseDHCP4(crossed));
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ expected =
+ "the value of min-valid-lifetime (2000) is not "
+ "less than max-valid-lifetime (1000)";
+ checkResult(status, 1, expected);
+}
+
+/// Check that the renew-timer doesn't have to be specified, in which case
+/// it is marked unspecified in the Subnet.
+TEST_F(Dhcp4ParserTest, unspecifiedRenewTimer) {
+
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // returned value should be 0 (success)
+ checkResult(status, 0);
+
+ Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
+ getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.200"));
+ ASSERT_TRUE(subnet);
+
+ EXPECT_TRUE(subnet->getT1().unspecified());
+ EXPECT_FALSE(subnet->getT2().unspecified());
+ EXPECT_EQ(2000, subnet->getT2().get());
+ EXPECT_EQ(4000, subnet->getValid().get());
+
+ // Check that subnet-id is 1
+ EXPECT_EQ(1, subnet->getID());
+}
+
+/// Check that the rebind-timer doesn't have to be specified, in which case
+/// it is marked unspecified in the Subnet.
+TEST_F(Dhcp4ParserTest, unspecifiedRebindTimer) {
+
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // returned value should be 0 (success)
+ checkResult(status, 0);
+
+ Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
+ getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.200"));
+ ASSERT_TRUE(subnet);
+ EXPECT_FALSE(subnet->getT1().unspecified());
+ EXPECT_EQ(1000, subnet->getT1().get());
+ EXPECT_TRUE(subnet->getT2().unspecified());
+ EXPECT_EQ(4000, subnet->getValid().get());
+
+ // Check that subnet-id is 1
+ EXPECT_EQ(1, subnet->getID());
+}
+
+/// The goal of this test is to verify if defined subnet uses global
+/// parameter timer definitions.
+TEST_F(Dhcp4ParserTest, subnetGlobalDefaults) {
+
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000,"
+ "\"min-valid-lifetime\": 3000,"
+ "\"max-valid-lifetime\": 5000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // check if returned status is OK
+ checkResult(status, 0);
+
+ // Now check if the configuration was indeed handled and we have
+ // expected pool configured.
+ Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
+ getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.200"));
+ ASSERT_TRUE(subnet);
+ EXPECT_EQ(1000, subnet->getT1().get());
+ EXPECT_EQ(2000, subnet->getT2().get());
+ EXPECT_EQ(4000, subnet->getValid().get());
+ EXPECT_EQ(3000, subnet->getValid().getMin());
+ EXPECT_EQ(5000, subnet->getValid().getMax());
+
+ // Check that subnet-id is 1
+ EXPECT_EQ(1, subnet->getID());
+}
+
+// Goal of this test is to verify that multiple subnets get unique
+// subnet-ids. Also, test checks that it's possible to do reconfiguration
+// multiple times.
+TEST_F(Dhcp4ParserTest, multipleSubnets) {
+ ConstElementPtr x;
+ // Collection of four subnets for which subnet ids should be
+ // autogenerated - ids are unspecified or set to 0.
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" "
+ " },"
+ " {"
+ " \"pools\": [ { \"pool\": \"192.0.3.101 - 192.0.3.150\" } ],"
+ " \"subnet\": \"192.0.3.0/24\", "
+ " \"id\": 0 "
+ " },"
+ " {"
+ " \"pools\": [ { \"pool\": \"192.0.4.101 - 192.0.4.150\" } ],"
+ " \"subnet\": \"192.0.4.0/24\" "
+ " },"
+ " {"
+ " \"pools\": [ { \"pool\": \"192.0.5.101 - 192.0.5.150\" } ],"
+ " \"subnet\": \"192.0.5.0/24\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+
+ int cnt = 0; // Number of reconfigurations
+
+ do {
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 0);
+
+ CfgMgr::instance().commit();
+
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(4, subnets->size()); // We expect 4 subnets
+
+ // Check subnet-ids of each subnet (it should be monotonously increasing)
+ auto subnet = subnets->begin();
+ EXPECT_EQ(1, (*subnet)->getID());
+ EXPECT_EQ(2, (*++subnet)->getID());
+ EXPECT_EQ(3, (*++subnet)->getID());
+ EXPECT_EQ(4, (*++subnet)->getID());
+
+ // Repeat reconfiguration process 10 times and check that the subnet-id
+ // is set to the same value. Technically, just two iterations would be
+ // sufficient, but it's nice to have a test that exercises reconfiguration
+ // a bit.
+ } while (++cnt < 10);
+}
+
+// This test checks that it is possible to assign arbitrary ids for subnets.
+TEST_F(Dhcp4ParserTest, multipleSubnetsExplicitIDs) {
+ ConstElementPtr x;
+ // Four subnets with arbitrary subnet ids.
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"id\": 1024 "
+ " },"
+ " {"
+ " \"pools\": [ { \"pool\": \"192.0.3.101 - 192.0.3.150\" } ],"
+ " \"subnet\": \"192.0.3.0/24\", "
+ " \"id\": 100 "
+ " },"
+ " {"
+ " \"pools\": [ { \"pool\": \"192.0.4.101 - 192.0.4.150\" } ],"
+ " \"subnet\": \"192.0.4.0/24\", "
+ " \"id\": 1 "
+ " },"
+ " {"
+ " \"pools\": [ { \"pool\": \"192.0.5.101 - 192.0.5.150\" } ],"
+ " \"subnet\": \"192.0.5.0/24\", "
+ " \"id\": 34 "
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ int cnt = 0; // Number of reconfigurations
+ do {
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 0);
+
+ CfgMgr::instance().commit();
+
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(4, subnets->size()); // We expect 4 subnets
+
+ // Verify that subnet ids are as expected.
+ // Now the subnet order is the subnet id one.
+ auto subnet = subnets->begin();
+ EXPECT_EQ(1, (*subnet)->getID());
+ EXPECT_EQ(34, (*++subnet)->getID());
+ EXPECT_EQ(100, (*++subnet)->getID());
+ EXPECT_EQ(1024, (*++subnet)->getID());
+
+ // Repeat reconfiguration process 10 times and check that the subnet-id
+ // is set to the same value.
+ } while (++cnt < 3);
+}
+
+// Check that the configuration with two subnets having the same id is rejected.
+TEST_F(Dhcp4ParserTest, multipleSubnetsOverlappingIDs) {
+ ConstElementPtr x;
+ // Four subnets, two of them having the same id.
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"id\": 1024 "
+ " },"
+ " {"
+ " \"pools\": [ { \"pool\": \"192.0.3.101 - 192.0.3.150\" } ],"
+ " \"subnet\": \"192.0.3.0/24\", "
+ " \"id\": 100 "
+ " },"
+ " {"
+ " \"pools\": [ { \"pool\": \"192.0.4.101 - 192.0.4.150\" } ],"
+ " \"subnet\": \"192.0.4.0/24\", "
+ " \"id\": 1024 "
+ " },"
+ " {"
+ " \"pools\": [ { \"pool\": \"192.0.5.101 - 192.0.5.150\" } ],"
+ " \"subnet\": \"192.0.5.0/24\", "
+ " \"id\": 34 "
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 1);
+ EXPECT_TRUE(errorContainsPosition(x, "<string>"));
+}
+
+// Goal of this test is to verify that a previously configured subnet can be
+// deleted in subsequent reconfiguration.
+TEST_F(Dhcp4ParserTest, reconfigureRemoveSubnet) {
+ ConstElementPtr x;
+
+ // All four subnets
+ string config4 = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"id\": 1 "
+ " },"
+ " {"
+ " \"pools\": [ { \"pool\": \"192.0.3.101 - 192.0.3.150\" } ],"
+ " \"subnet\": \"192.0.3.0/24\", "
+ " \"id\": 2 "
+ " },"
+ " {"
+ " \"pools\": [ { \"pool\": \"192.0.4.101 - 192.0.4.150\" } ],"
+ " \"subnet\": \"192.0.4.0/24\", "
+ " \"id\": 3 "
+ " },"
+ " {"
+ " \"pools\": [ { \"pool\": \"192.0.5.101 - 192.0.5.150\" } ],"
+ " \"subnet\": \"192.0.5.0/24\", "
+ " \"id\": 4 "
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ // Three subnets (the last one removed)
+ string config_first3 = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"id\": 1 "
+ " },"
+ " {"
+ " \"pools\": [ { \"pool\": \"192.0.3.101 - 192.0.3.150\" } ],"
+ " \"subnet\": \"192.0.3.0/24\", "
+ " \"id\": 2 "
+ " },"
+ " {"
+ " \"pools\": [ { \"pool\": \"192.0.4.101 - 192.0.4.150\" } ],"
+ " \"subnet\": \"192.0.4.0/24\", "
+ " \"id\": 3 "
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ // Second subnet removed
+ string config_second_removed = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"id\": 1 "
+ " },"
+ " {"
+ " \"pools\": [ { \"pool\": \"192.0.4.101 - 192.0.4.150\" } ],"
+ " \"subnet\": \"192.0.4.0/24\", "
+ " \"id\": 3 "
+ " },"
+ " {"
+ " \"pools\": [ { \"pool\": \"192.0.5.101 - 192.0.5.150\" } ],"
+ " \"subnet\": \"192.0.5.0/24\", "
+ " \"id\": 4 "
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ // CASE 1: Configure 4 subnets, then reconfigure and remove the
+ // last one.
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config4));
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 0);
+
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->getAll();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(4, subnets->size()); // We expect 4 subnets
+
+ CfgMgr::instance().clear();
+
+ // Do the reconfiguration (the last subnet is removed)
+ ASSERT_NO_THROW(json = parseDHCP4(config_first3));
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 0);
+
+ subnets = CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->getAll();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(3, subnets->size()); // We expect 3 subnets now (4th is removed)
+
+ // Check subnet-ids of each subnet (it should be monotonously increasing)
+ auto subnet = subnets->begin();
+ EXPECT_EQ(1, (*subnet)->getID());
+ EXPECT_EQ(2, (*++subnet)->getID());
+ EXPECT_EQ(3, (*++subnet)->getID());
+
+ CfgMgr::instance().clear();
+
+ /// CASE 2: Configure 4 subnets, then reconfigure and remove one
+ /// from in between (not first, not last)
+ ASSERT_NO_THROW(json = parseDHCP4(config4));
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 0);
+
+ CfgMgr::instance().clear();
+
+ // Do reconfiguration
+ ASSERT_NO_THROW(json = parseDHCP4(config_second_removed));
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 0);
+
+ subnets = CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->getAll();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(3, subnets->size()); // We expect 4 subnets
+
+ auto subnet_it = subnets->begin();
+ EXPECT_EQ(1, (*subnet_it)->getID());
+ // The second subnet (with subnet-id = 2) is no longer there
+ EXPECT_EQ(3, (*++subnet_it)->getID());
+ EXPECT_EQ(4, (*++subnet_it)->getID());
+}
+
+/// @todo: implement subnet removal test as part of #3281.
+
+// Checks if the next-server and other fixed BOOTP fields defined as
+// global parameter are taken into consideration.
+TEST_F(Dhcp4ParserTest, nextServerGlobal) {
+
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"next-server\": \"1.2.3.4\", "
+ "\"server-hostname\": \"foo\", "
+ "\"boot-file-name\": \"bar\", "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // check if returned status is OK
+ checkResult(status, 0);
+
+ // Now check if the configuration was indeed handled and we have
+ // expected pool configured.
+ Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
+ getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.200"));
+ ASSERT_TRUE(subnet);
+ // Reset the fetch global function to staging (vs current) config.
+ subnet->setFetchGlobalsFn([]() -> ConstCfgGlobalsPtr {
+ return (CfgMgr::instance().getStagingCfg()->getConfiguredGlobals());
+ });
+ EXPECT_EQ("1.2.3.4", subnet->getSiaddr().get().toText());
+ EXPECT_EQ("foo", subnet->getSname().get());
+ EXPECT_EQ("bar", subnet->getFilename().get());
+}
+
+// Checks if the next-server and other fixed BOOTP fields defined as
+// subnet parameter are taken into consideration.
+TEST_F(Dhcp4ParserTest, nextServerSubnet) {
+
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"next-server\": \"1.2.3.4\", "
+ " \"server-hostname\": \"foo\", "
+ " \"boot-file-name\": \"bar\", "
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // check if returned status is OK
+ checkResult(status, 0);
+
+ // Now check if the configuration was indeed handled and we have
+ // expected pool configured.
+ Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
+ getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.200"));
+ ASSERT_TRUE(subnet);
+ EXPECT_EQ("1.2.3.4", subnet->getSiaddr().get().toText());
+ EXPECT_EQ("foo", subnet->getSname().get());
+ EXPECT_EQ("bar", subnet->getFilename().get());
+}
+
+// Test checks several negative scenarios for next-server configuration: bogus
+// address, IPv6 address and empty string.
+TEST_F(Dhcp4ParserTest, nextServerNegative) {
+ IfaceMgrTestConfig test_config(true);
+
+ // Config with junk instead of next-server address
+ string config_bogus1 = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"rebind-timer\": 2000, "
+ " \"renew-timer\": 1000, "
+ " \"next-server\": \"a.b.c.d\", "
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ // Config with IPv6 next server address
+ string config_bogus2 = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"rebind-timer\": 2000, "
+ " \"renew-timer\": 1000, "
+ " \"next-server\": \"2001:db8::1\", "
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ // Config with empty next server address
+ string config_bogus3 = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"rebind-timer\": 2000, "
+ " \"renew-timer\": 1000, "
+ " \"next-server\": \"\", "
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ // Config with too large server-hostname
+ string bigsname(Pkt4::MAX_SNAME_LEN + 1, ' ');
+ string config_bogus4 = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"rebind-timer\": 2000, "
+ " \"renew-timer\": 1000, "
+ " \"server-hostname\": \"" + bigsname + "\", " +
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ // Config with too large boot-file-hostname
+ string bigfilename(Pkt4::MAX_FILE_LEN + 1, ' ');
+ string config_bogus5 = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"rebind-timer\": 2000, "
+ " \"renew-timer\": 1000, "
+ " \"boot-file-name\": \"" + bigfilename + "\", " +
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json1;
+ ASSERT_NO_THROW(json1 = parseDHCP4(config_bogus1));
+ ConstElementPtr json2;
+ ASSERT_NO_THROW(json2 = parseDHCP4(config_bogus2));
+ ConstElementPtr json3;
+ ASSERT_NO_THROW(json3 = parseDHCP4(config_bogus3));
+ ConstElementPtr json4;
+ ASSERT_NO_THROW(json4 = parseDHCP4(config_bogus4));
+ ConstElementPtr json5;
+ ASSERT_NO_THROW(json5 = parseDHCP4(config_bogus5));
+
+ // check if returned status is always a failure
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json1));
+ checkResult(status, 1);
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+
+ CfgMgr::instance().clear();
+
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json2));
+ checkResult(status, 1);
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+
+ CfgMgr::instance().clear();
+
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json3));
+ checkResult(status, 0);
+ EXPECT_FALSE(errorContainsPosition(status, "<string>"));
+
+ CfgMgr::instance().clear();
+
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json4));
+ checkResult(status, 1);
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+
+ CfgMgr::instance().clear();
+
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json5));
+ checkResult(status, 1);
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+}
+
+// Checks if the next-server defined as global value is overridden by subnet
+// specific value.
+TEST_F(Dhcp4ParserTest, nextServerOverride) {
+
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"next-server\": \"192.0.0.1\", "
+ "\"server-hostname\": \"nohost\","
+ "\"boot-file-name\": \"nofile\","
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"next-server\": \"1.2.3.4\", "
+ " \"server-hostname\": \"some-name.example.org\","
+ " \"boot-file-name\": \"bootfile.efi\","
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // check if returned status is OK
+ checkResult(status, 0);
+
+ // Now check if the configuration was indeed handled and we have
+ // expected pool configured.
+ Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
+ getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.200"));
+ ASSERT_TRUE(subnet);
+ EXPECT_EQ("1.2.3.4", subnet->getSiaddr().get().toText());
+ EXPECT_EQ("some-name.example.org", subnet->getSname().get());
+ EXPECT_EQ("bootfile.efi", subnet->getFilename().get());
+}
+
+// Check whether it is possible to configure echo-client-id
+TEST_F(Dhcp4ParserTest, echoClientId) {
+
+ string config_false = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"echo-client-id\": false,"
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ string config_true = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"echo-client-id\": true,"
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json_false;
+ ASSERT_NO_THROW(json_false = parseDHCP4(config_false));
+ extractConfig(config_false);
+ ConstElementPtr json_true;
+ ASSERT_NO_THROW(json_true = parseDHCP4(config_true));
+ extractConfig(config_true);
+
+ // Let's check the default. It should be true
+ ASSERT_TRUE(CfgMgr::instance().getStagingCfg()->getEchoClientId());
+
+ // Now check that "false" configuration is really applied.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json_false));
+ checkResult(status, 0);
+ ASSERT_FALSE(CfgMgr::instance().getStagingCfg()->getEchoClientId());
+
+ CfgMgr::instance().clear();
+
+ // Now check that "true" configuration is really applied.
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json_true));
+ checkResult(status, 0);
+ ASSERT_TRUE(CfgMgr::instance().getStagingCfg()->getEchoClientId());
+
+ // In any case revert back to the default value (true)
+ CfgMgr::instance().getStagingCfg()->setEchoClientId(true);
+}
+
+// Check whether it is possible to configure compatibility flags.
+TEST_F(Dhcp4ParserTest, compatibility) {
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"compatibility\": { "
+ " \"lenient-option-parsing\": true,"
+ " \"ignore-dhcp-server-identifier\": true,"
+ " \"ignore-rai-link-selection\": true,"
+ " \"exclude-first-last-24\": true"
+ "},"
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config)) << "bad config: " << config;
+ extractConfig(config);
+
+ // Check defaults: they should be false.
+ EXPECT_FALSE(CfgMgr::instance().getStagingCfg()->getLenientOptionParsing());
+ EXPECT_FALSE(CfgMgr::instance().getStagingCfg()->getIgnoreServerIdentifier());
+ EXPECT_FALSE(CfgMgr::instance().getStagingCfg()->getIgnoreRAILinkSelection());
+ EXPECT_FALSE(CfgMgr::instance().getStagingCfg()->getExcludeFirstLast24());
+
+ // Check the configuration was really applied.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(status, 0);
+
+ EXPECT_TRUE(CfgMgr::instance().getStagingCfg()->getLenientOptionParsing());
+ EXPECT_TRUE(CfgMgr::instance().getStagingCfg()->getIgnoreServerIdentifier());
+ EXPECT_TRUE(CfgMgr::instance().getStagingCfg()->getIgnoreRAILinkSelection());
+ EXPECT_TRUE(CfgMgr::instance().getStagingCfg()->getExcludeFirstLast24());
+}
+
+// Check that unknown compatibility flag raises error.
+TEST_F(Dhcp4ParserTest, compatibilityUnknown) {
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"compatibility\": { "
+ " \"foo-bar\": true"
+ "},"
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ // Syntax is incorrect.
+ EXPECT_THROW(parseDHCP4(config), Dhcp4ParseError);
+ ConstElementPtr json;
+ EXPECT_NO_THROW(json = parseJSON(config));
+
+ // Unknown keyword is detected.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ string expected = "unsupported compatibility parameter: ";
+ expected += "foo-bar (<string>:1:127)";
+ checkResult(status, 1, expected);
+}
+
+// Check that not boolean compatibility flag value raises error.
+TEST_F(Dhcp4ParserTest, compatibilityNotBool) {
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"compatibility\": { "
+ " \"lenient-option-parsing\": 1"
+ "},"
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ // Syntax is incorrect.
+ EXPECT_THROW(parseDHCP4(config), Dhcp4ParseError);
+ ConstElementPtr json;
+ EXPECT_NO_THROW(json = parseJSON(config));
+
+ // Bad value type is detected.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ string expected = "compatibility parameter values must be boolean ";
+ expected += "(lenient-option-parsing at <string>:1:142)";
+ checkResult(status, 1, expected);
+}
+
+// This test checks that the global match-client-id parameter is optional
+// and that values under the subnet are used.
+TEST_F(Dhcp4ParserTest, matchClientIdNoGlobal) {
+ std::string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ "
+ "{"
+ " \"id\": 1,"
+ " \"match-client-id\": true,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\""
+ "},"
+ "{"
+ " \"id\": 2,"
+ " \"match-client-id\": false,"
+ " \"pools\": [ { \"pool\": \"192.0.3.1 - 192.0.3.100\" } ],"
+ " \"subnet\": \"192.0.3.0/24\""
+ "} ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ ASSERT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(status, 0);
+
+ CfgSubnets4Ptr cfg = CfgMgr::instance().getStagingCfg()->getCfgSubnets4();
+ Subnet4Ptr subnet1 = cfg->selectSubnet(IOAddress("192.0.2.1"));
+ ASSERT_TRUE(subnet1);
+ // Reset the fetch global function to staging (vs current) config.
+ subnet1->setFetchGlobalsFn([]() -> ConstCfgGlobalsPtr {
+ return (CfgMgr::instance().getStagingCfg()->getConfiguredGlobals());
+ });
+ EXPECT_TRUE(subnet1->getMatchClientId());
+
+ Subnet4Ptr subnet2 = cfg->selectSubnet(IOAddress("192.0.3.1"));
+ ASSERT_TRUE(subnet2);
+ // Reset the fetch global function to staging (vs current) config.
+ subnet2->setFetchGlobalsFn([]() -> ConstCfgGlobalsPtr {
+ return (CfgMgr::instance().getStagingCfg()->getConfiguredGlobals());
+ });
+ EXPECT_FALSE(subnet2->getMatchClientId());
+}
+
+// This test checks that the global match-client-id parameter is used
+// when there is no such parameter under subnet and that the parameter
+// specified for a subnet overrides the global setting.
+TEST_F(Dhcp4ParserTest, matchClientIdGlobal) {
+ std::string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"match-client-id\": true,"
+ "\"subnet4\": [ "
+ "{"
+ " \"id\": 1,"
+ " \"match-client-id\": false,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\""
+ "},"
+ "{"
+ " \"id\": 2,"
+ " \"pools\": [ { \"pool\": \"192.0.3.1 - 192.0.3.100\" } ],"
+ " \"subnet\": \"192.0.3.0/24\""
+ "} ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ ASSERT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(status, 0);
+
+ CfgSubnets4Ptr cfg = CfgMgr::instance().getStagingCfg()->getCfgSubnets4();
+ Subnet4Ptr subnet1 = cfg->selectSubnet(IOAddress("192.0.2.1"));
+ ASSERT_TRUE(subnet1);
+ // Reset the fetch global function to staging (vs current) config.
+ subnet1->setFetchGlobalsFn([]() -> ConstCfgGlobalsPtr {
+ return (CfgMgr::instance().getStagingCfg()->getConfiguredGlobals());
+ });
+ EXPECT_FALSE(subnet1->getMatchClientId());
+
+ Subnet4Ptr subnet2 = cfg->selectSubnet(IOAddress("192.0.3.1"));
+ ASSERT_TRUE(subnet2);
+ // Reset the fetch global function to staging (vs current) config.
+ subnet2->setFetchGlobalsFn([]() -> ConstCfgGlobalsPtr {
+ return (CfgMgr::instance().getStagingCfg()->getConfiguredGlobals());
+ });
+ EXPECT_TRUE(subnet2->getMatchClientId());
+}
+
+// This test checks that the global authoritative parameter is optional
+// and that values under the subnet are used.
+TEST_F(Dhcp4ParserTest, authoritativeNoGlobal) {
+ std::string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ "
+ "{"
+ " \"id\": 1,"
+ " \"authoritative\": true,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\""
+ "},"
+ "{"
+ " \"id\": 2,"
+ " \"authoritative\": false,"
+ " \"pools\": [ { \"pool\": \"192.0.3.1 - 192.0.3.100\" } ],"
+ " \"subnet\": \"192.0.3.0/24\""
+ "} ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ ASSERT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(status, 0);
+
+ CfgSubnets4Ptr cfg = CfgMgr::instance().getStagingCfg()->getCfgSubnets4();
+ Subnet4Ptr subnet1 = cfg->selectSubnet(IOAddress("192.0.2.1"));
+ ASSERT_TRUE(subnet1);
+ // Reset the fetch global function to staging (vs current) config.
+ subnet1->setFetchGlobalsFn([]() -> ConstCfgGlobalsPtr {
+ return (CfgMgr::instance().getStagingCfg()->getConfiguredGlobals());
+ });
+ EXPECT_TRUE(subnet1->getAuthoritative());
+
+ Subnet4Ptr subnet2 = cfg->selectSubnet(IOAddress("192.0.3.1"));
+ ASSERT_TRUE(subnet2);
+ // Reset the fetch global function to staging (vs current) config.
+ subnet2->setFetchGlobalsFn([]() -> ConstCfgGlobalsPtr {
+ return (CfgMgr::instance().getStagingCfg()->getConfiguredGlobals());
+ });
+ EXPECT_FALSE(subnet2->getAuthoritative());
+}
+
+// This test checks that the global authoritative parameter is used
+// when there is no such parameter under subnet and that the parameter
+// specified for a subnet overrides the global setting.
+TEST_F(Dhcp4ParserTest, authoritativeGlobal) {
+ std::string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"authoritative\": true,"
+ "\"subnet4\": [ "
+ "{"
+ " \"id\": 1,"
+ " \"authoritative\": false,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\""
+ "},"
+ "{"
+ " \"id\": 2,"
+ " \"pools\": [ { \"pool\": \"192.0.3.1 - 192.0.3.100\" } ],"
+ " \"subnet\": \"192.0.3.0/24\""
+ "} ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ ASSERT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(status, 0);
+
+ CfgSubnets4Ptr cfg = CfgMgr::instance().getStagingCfg()->getCfgSubnets4();
+ Subnet4Ptr subnet1 = cfg->selectSubnet(IOAddress("192.0.2.1"));
+ ASSERT_TRUE(subnet1);
+ // Reset the fetch global function to staging (vs current) config.
+ subnet1->setFetchGlobalsFn([]() -> ConstCfgGlobalsPtr {
+ return (CfgMgr::instance().getStagingCfg()->getConfiguredGlobals());
+ });
+ EXPECT_FALSE(subnet1->getAuthoritative());
+
+ Subnet4Ptr subnet2 = cfg->selectSubnet(IOAddress("192.0.3.1"));
+ ASSERT_TRUE(subnet2);
+ // Reset the fetch global function to staging (vs current) config.
+ subnet2->setFetchGlobalsFn([]() -> ConstCfgGlobalsPtr {
+ return (CfgMgr::instance().getStagingCfg()->getConfiguredGlobals());
+ });
+ EXPECT_TRUE(subnet2->getAuthoritative());
+}
+
+// This test checks if it is possible to override global values
+// on a per subnet basis.
+TEST_F(Dhcp4ParserTest, subnetLocal) {
+
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"renew-timer\": 1, "
+ " \"rebind-timer\": 2, "
+ " \"valid-lifetime\": 4,"
+ " \"min-valid-lifetime\": 3,"
+ " \"max-valid-lifetime\": 5,"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000,"
+ "\"min-valid-lifetime\": 3000,"
+ "\"max-valid-lifetime\": 5000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // returned value should be 0 (configuration success)
+ checkResult(status, 0);
+
+ Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
+ getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.200"));
+ ASSERT_TRUE(subnet);
+ EXPECT_EQ(1, subnet->getT1().get());
+ EXPECT_EQ(2, subnet->getT2().get());
+ EXPECT_EQ(4, subnet->getValid().get());
+ EXPECT_EQ(3, subnet->getValid().getMin());
+ EXPECT_EQ(5, subnet->getValid().getMax());
+}
+
+// This test checks that multiple pools can be defined and handled properly.
+// The test defines 2 subnets, each with 2 pools.
+TEST_F(Dhcp4ParserTest, multiplePools) {
+
+ // Collection with two subnets, each with 2 pools.
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ "
+ " { \"pool\": \"192.0.2.0/28\" },"
+ " { \"pool\": \"192.0.2.200-192.0.2.255\" }"
+ " ],"
+ " \"subnet\": \"192.0.2.0/24\" "
+ " },"
+ " {"
+ " \"id\": 2,"
+ " \"pools\": [ "
+ " { \"pool\": \"192.0.3.0/25\" },"
+ " { \"pool\": \"192.0.3.128/25\" }"
+ " ],"
+ " \"subnet\": \"192.0.3.0/24\""
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ ASSERT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(status, 0);
+
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->getAll();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(2, subnets->size()); // We expect 2 subnets
+
+ // Check the first subnet
+ auto subnet = subnets->begin();
+ const PoolCollection& pools1 = (*subnet)->getPools(Lease::TYPE_V4);
+ ASSERT_EQ(2, pools1.size());
+ EXPECT_EQ("type=V4, 192.0.2.0-192.0.2.15",
+ pools1[0]->toText());
+ EXPECT_EQ("type=V4, 192.0.2.200-192.0.2.255",
+ pools1[1]->toText());
+ // There shouldn't be any TA or PD pools
+ EXPECT_THROW((*subnet)->getPools(Lease::TYPE_TA), BadValue);
+ EXPECT_THROW((*subnet)->getPools(Lease::TYPE_PD), BadValue);
+
+ // Check the second subnet
+ ++subnet;
+ const PoolCollection& pools2 = (*subnet)->getPools(Lease::TYPE_V4);
+ ASSERT_EQ(2, pools2.size());
+ EXPECT_EQ("type=V4, 192.0.3.0-192.0.3.127",
+ pools2[0]->toText());
+ EXPECT_EQ("type=V4, 192.0.3.128-192.0.3.255",
+ pools2[1]->toText());
+ // There shouldn't be any TA or PD pools
+ EXPECT_THROW((*subnet)->getPools(Lease::TYPE_TA), BadValue);
+ EXPECT_THROW((*subnet)->getPools(Lease::TYPE_PD), BadValue);
+}
+
+// Test verifies that a subnet with pool values that do not belong to that
+// pool are rejected.
+TEST_F(Dhcp4ParserTest, poolOutOfSubnet) {
+
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.4.0/28\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // returned value must be 1 (values error)
+ // as the pool does not belong to that subnet
+ checkResult(status, 1);
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+}
+
+// Goal of this test is to verify if pools can be defined
+// using prefix/length notation. There is no separate test for min-max
+// notation as it was tested in several previous tests.
+TEST_F(Dhcp4ParserTest, poolPrefixLen) {
+
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.128/28\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // returned value must be 0 (configuration accepted)
+ checkResult(status, 0);
+
+ Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
+ getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.200"));
+ ASSERT_TRUE(subnet);
+ EXPECT_EQ(1000, subnet->getT1().get());
+ EXPECT_EQ(2000, subnet->getT2().get());
+ EXPECT_EQ(4000, subnet->getValid().get());
+}
+
+// Goal of this test is to verify if invalid pool definitions
+// return a location in the error message.
+TEST_F(Dhcp4ParserTest, badPools) {
+
+ // not a prefix
+ string config_bogus1 = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"foo/28\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ // not a length
+ string config_bogus2 = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.128/foo\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ // invalid prefix length
+ string config_bogus3 = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.128/100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ // not a prefix nor a min-max
+ string config_bogus4 = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"foo\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ // not an address
+ string config_bogus5 = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"foo - bar\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ // min > max
+ string config_bogus6 = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.200 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ // out of range prefix length (new check)
+ string config_bogus7 = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.128/1052\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json1;
+ ASSERT_NO_THROW(json1 = parseDHCP4(config_bogus1));
+ ConstElementPtr json2;
+ ASSERT_NO_THROW(json2 = parseDHCP4(config_bogus2));
+ ConstElementPtr json3;
+ ASSERT_NO_THROW(json3 = parseDHCP4(config_bogus3));
+ ConstElementPtr json4;
+ ASSERT_NO_THROW(json4 = parseDHCP4(config_bogus4));
+ ConstElementPtr json5;
+ ASSERT_NO_THROW(json5 = parseDHCP4(config_bogus5));
+ ConstElementPtr json6;
+ ASSERT_NO_THROW(json6 = parseDHCP4(config_bogus6));
+ ConstElementPtr json7;
+ ASSERT_NO_THROW(json7 = parseDHCP4(config_bogus7));
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json1));
+
+ // check if returned status is always a failure
+ checkResult(status, 1);
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+
+ CfgMgr::instance().clear();
+
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json2));
+ checkResult(status, 1);
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+
+ CfgMgr::instance().clear();
+
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json3));
+ checkResult(status, 1);
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+
+ CfgMgr::instance().clear();
+
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json4));
+ checkResult(status, 1);
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+
+ CfgMgr::instance().clear();
+
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json5));
+ checkResult(status, 1);
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+
+ CfgMgr::instance().clear();
+
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json6));
+ checkResult(status, 1);
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+
+ CfgMgr::instance().clear();
+
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json7));
+ checkResult(status, 1);
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+}
+
+// Goal of this test is to verify no pool definitions is invalid
+// and returns a location in the error message.
+TEST_F(Dhcp4ParserTest, noPools) {
+
+ // Configuration string.
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"user-context\": { } } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ EXPECT_THROW(parseDHCP4(config, true), Dhcp4ParseError);
+}
+
+// Goal of this test is to verify that invalid subnet fails to be parsed.
+TEST_F(Dhcp4ParserTest, badSubnetValues) {
+
+ // Contains parts needed for a single test scenario.
+ struct Scenario {
+ std::string description_;
+ std::string config_json_;
+ std::string exp_error_msg_;
+ };
+
+ // Vector of scenarios.
+ std::vector<Scenario> scenarios = {
+ {
+ "IP is not an address",
+ "{ \"subnet4\": [ { "
+ " \"subnet\": \"not an address/24\" } ],"
+ "\"valid-lifetime\": 4000 }",
+ "subnet configuration failed: "
+ "Failed to convert string to address 'notanaddress': Invalid argument"
+ },
+ {
+ "IP is Invalid",
+ "{ \"subnet4\": [ { "
+ " \"subnet\": \"256.16.1.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }",
+ "subnet configuration failed: "
+ "Failed to convert string to address '256.16.1.0': Invalid argument"
+ },
+ {
+ "Missing prefix",
+ "{ \"subnet4\": [ { "
+ " \"subnet\": \"192.0.2.0\" } ],"
+ "\"valid-lifetime\": 4000 }",
+ "subnet configuration failed: "
+ "Invalid subnet syntax (prefix/len expected):192.0.2.0 (<string>:1:32)"
+ },
+ {
+ "Prefix not an integer (2 slashes)",
+ "{ \"subnet4\": [ { "
+ " \"subnet\": \"192.0.2.0//24\" } ],"
+ "\"valid-lifetime\": 4000 }",
+ "subnet configuration failed: "
+ "prefix length: '/24' is not an integer (<string>:1:32)"
+ },
+ {
+ "Prefix value is insane",
+ "{ \"subnet4\": [ { "
+ " \"subnet\": \"192.0.2.0/45938\" } ],"
+ "\"valid-lifetime\": 4000 }",
+ "subnet configuration failed: "
+ "Invalid prefix length specified for subnet: 45938 (<string>:1:32)"
+ }
+ };
+
+ // Iterate over the list of scenarios. Each should fail to parse with
+ // a specific error message.
+ for (auto scenario = scenarios.begin(); scenario != scenarios.end(); ++scenario) {
+ {
+ SCOPED_TRACE((*scenario).description_);
+ ConstElementPtr config;
+ ASSERT_NO_THROW(config = parseDHCP4((*scenario).config_json_))
+ << "invalid json, broken test";
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, config));
+ checkResult(status, 1);
+ ASSERT_TRUE(comment_);
+ EXPECT_EQ(comment_->stringValue(), (*scenario).exp_error_msg_);
+ }
+ }
+}
+
+// Goal of this test is to verify that unknown interface fails
+// to be parsed.
+TEST_F(Dhcp4ParserTest, unknownInterface) {
+
+ // Configuration string.
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ ],"
+ " \"subnet\": \"192.0.2.0/24\","
+ " \"interface\": \"ethX\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config, true));
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(status, 1);
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+}
+
+// The goal of this test is to check whether an option definition
+// that defines an option carrying an IPv4 address can be created.
+TEST_F(Dhcp4ParserTest, optionDefIpv4Address) {
+
+ // Configuration string.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"ipv4-address\","
+ " \"space\": \"isc\""
+ " } ]"
+ "}";
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseOPTION_DEFS(config, true));
+ extractConfig(config);
+
+ // Make sure that the particular option definition does not exist.
+ OptionDefinitionPtr def = CfgMgr::instance().getStagingCfg()->
+ getCfgOptionDef()->get("isc", 100);
+ ASSERT_FALSE(def);
+
+ // Use the configuration string to create new option definition.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ // We need to commit option definitions because later in this test we
+ // will be checking if they get removed when "option-def" parameter
+ // is removed from a configuration.
+ LibDHCP::commitRuntimeOptionDefs();
+
+ // The option definition should now be available in the CfgMgr.
+ def = CfgMgr::instance().getStagingCfg()->getCfgOptionDef()->get("isc", 100);
+ ASSERT_TRUE(def);
+
+ // Verify that the option definition data is valid.
+ EXPECT_EQ("foo", def->getName());
+ EXPECT_EQ(100, def->getCode());
+ EXPECT_FALSE(def->getArrayType());
+ EXPECT_EQ(OPT_IPV4_ADDRESS_TYPE, def->getType());
+ EXPECT_TRUE(def->getEncapsulatedSpace().empty());
+
+ // The copy of the option definition should be available in the libdhcp++.
+ OptionDefinitionPtr def_libdhcp = LibDHCP::getRuntimeOptionDef("isc", 100);
+ ASSERT_TRUE(def_libdhcp);
+
+ // Both definitions should be held in distinct pointers but they should
+ // be equal.
+ EXPECT_TRUE(def_libdhcp != def);
+ EXPECT_TRUE(*def_libdhcp == *def);
+
+ // Let's apply empty configuration. This removes the option definitions
+ // configuration and should result in removal of the option 100 from the
+ // libdhcp++. Note DHCP4 or OPTION_DEFS parsers do not accept empty maps.
+ json.reset(new MapElement());
+ ASSERT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(status, 0);
+
+ EXPECT_FALSE(LibDHCP::getRuntimeOptionDef("isc", 100));
+}
+
+// The goal of this test is to check whether an option definition
+// that defines an option carrying a record of data fields can
+// be created.
+TEST_F(Dhcp4ParserTest, optionDefRecord) {
+
+ // Configuration string.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"record\","
+ " \"record-types\": \"uint16, ipv4-address, ipv6-address, string\","
+ " \"space\": \"isc\""
+ " } ]"
+ "}";
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseOPTION_DEFS(config));
+ extractConfig(config);
+
+ // Make sure that the particular option definition does not exist.
+ OptionDefinitionPtr def = CfgMgr::instance().getStagingCfg()->
+ getCfgOptionDef()->get("isc", 100);
+ ASSERT_FALSE(def);
+
+ // Use the configuration string to create new option definition.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ // The option definition should now be available in the CfgMgr.
+ def = CfgMgr::instance().getStagingCfg()->getCfgOptionDef()->get("isc", 100);
+ ASSERT_TRUE(def);
+
+ // Check the option data.
+ EXPECT_EQ("foo", def->getName());
+ EXPECT_EQ(100, def->getCode());
+ EXPECT_EQ(OPT_RECORD_TYPE, def->getType());
+ EXPECT_FALSE(def->getArrayType());
+ EXPECT_TRUE(def->getEncapsulatedSpace().empty());
+
+ // The option comprises the record of data fields. Verify that all
+ // fields are present and they are of the expected types.
+ const OptionDefinition::RecordFieldsCollection& record_fields =
+ def->getRecordFields();
+ ASSERT_EQ(4, record_fields.size());
+ EXPECT_EQ(OPT_UINT16_TYPE, record_fields[0]);
+ EXPECT_EQ(OPT_IPV4_ADDRESS_TYPE, record_fields[1]);
+ EXPECT_EQ(OPT_IPV6_ADDRESS_TYPE, record_fields[2]);
+ EXPECT_EQ(OPT_STRING_TYPE, record_fields[3]);
+}
+
+// The goal of this test is to verify that multiple option definitions
+// can be created.
+TEST_F(Dhcp4ParserTest, optionDefMultiple) {
+ // Configuration string.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"uint32\","
+ " \"space\": \"isc\""
+ " },"
+ " {"
+ " \"name\": \"foo-2\","
+ " \"code\": 101,"
+ " \"type\": \"ipv4-address\","
+ " \"space\": \"isc\""
+ " } ]"
+ "}";
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseOPTION_DEFS(config));
+ extractConfig(config);
+
+ // Make sure that the option definitions do not exist yet.
+ ASSERT_FALSE(CfgMgr::instance().getStagingCfg()->
+ getCfgOptionDef()->get("isc", 100));
+ ASSERT_FALSE(CfgMgr::instance().getStagingCfg()->
+ getCfgOptionDef()->get("isc", 101));
+
+ // Use the configuration string to create new option definitions.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ // Check the first definition we have created.
+ OptionDefinitionPtr def1 = CfgMgr::instance().getStagingCfg()->
+ getCfgOptionDef()->get("isc", 100);
+ ASSERT_TRUE(def1);
+
+ // Check the option data.
+ EXPECT_EQ("foo", def1->getName());
+ EXPECT_EQ(100, def1->getCode());
+ EXPECT_EQ(OPT_UINT32_TYPE, def1->getType());
+ EXPECT_FALSE(def1->getArrayType());
+ EXPECT_TRUE(def1->getEncapsulatedSpace().empty());
+
+ // Check the second option definition we have created.
+ OptionDefinitionPtr def2 = CfgMgr::instance().getStagingCfg()->
+ getCfgOptionDef()->get("isc", 101);
+ ASSERT_TRUE(def2);
+
+ // Check the option data.
+ EXPECT_EQ("foo-2", def2->getName());
+ EXPECT_EQ(101, def2->getCode());
+ EXPECT_EQ(OPT_IPV4_ADDRESS_TYPE, def2->getType());
+ EXPECT_FALSE(def2->getArrayType());
+ EXPECT_TRUE(def2->getEncapsulatedSpace().empty());
+}
+
+// The goal of this test is to verify that the duplicated option
+// definition is not accepted.
+TEST_F(Dhcp4ParserTest, optionDefDuplicate) {
+ // Preconfigure libdhcp++ with option definitions. The new configuration
+ // should override it, but when the new configuration fails, it should
+ // revert to this original configuration.
+ OptionDefSpaceContainer defs;
+ OptionDefinitionPtr def(new OptionDefinition("bar", 233, "isc", "string"));
+ defs.addItem(def);
+ LibDHCP::setRuntimeOptionDefs(defs);
+ LibDHCP::commitRuntimeOptionDefs();
+
+ // Configuration string. Both option definitions have
+ // the same code and belong to the same option space.
+ // This configuration should not be accepted.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"uint32\","
+ " \"space\": \"isc\""
+ " },"
+ " {"
+ " \"name\": \"foo-2\","
+ " \"code\": 100,"
+ " \"type\": \"ipv4-address\","
+ " \"space\": \"isc\""
+ " } ]"
+ "}";
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseOPTION_DEFS(config));
+
+ // Make sure that the option definition does not exist yet.
+ ASSERT_FALSE(CfgMgr::instance().getStagingCfg()->
+ getCfgOptionDef()->get("isc", 100));
+
+ // Use the configuration string to create new option definitions.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 1);
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+
+ // Specific check for incorrect report using default config pair
+ // as option-def is parsed first.
+ string expected = "failed to create or run parser for configuration ";
+ expected += "element option-def: option definition with code '100' ";
+ expected += "already exists in option space 'isc'";
+ EXPECT_EQ(1, countFile(expected));
+
+ // The new configuration should have inserted option 100, but
+ // once configuration failed (on the duplicate option definition)
+ // the original configuration in libdhcp++ should be reverted.
+ EXPECT_FALSE(LibDHCP::getRuntimeOptionDef("isc", 100));
+ def = LibDHCP::getRuntimeOptionDef("isc", 233);
+ ASSERT_TRUE(def);
+ EXPECT_EQ("bar", def->getName());
+ EXPECT_EQ(233, def->getCode());
+}
+
+// The goal of this test is to verify that the option definition
+// comprising an array of uint32 values can be created.
+TEST_F(Dhcp4ParserTest, optionDefArray) {
+
+ // Configuration string. Created option definition should
+ // comprise an array of uint32 values.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"uint32\","
+ " \"array\": true,"
+ " \"space\": \"isc\""
+ " } ]"
+ "}";
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseOPTION_DEFS(config));
+ extractConfig(config);
+
+ // Make sure that the particular option definition does not exist.
+ OptionDefinitionPtr def = CfgMgr::instance().getStagingCfg()->
+ getCfgOptionDef()->get("isc", 100);
+ ASSERT_FALSE(def);
+
+ // Use the configuration string to create new option definition.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ // The option definition should now be available in the CfgMgr.
+ def = CfgMgr::instance().getStagingCfg()->
+ getCfgOptionDef()->get("isc", 100);
+ ASSERT_TRUE(def);
+
+ // Check the option data.
+ EXPECT_EQ("foo", def->getName());
+ EXPECT_EQ(100, def->getCode());
+ EXPECT_EQ(OPT_UINT32_TYPE, def->getType());
+ EXPECT_TRUE(def->getArrayType());
+ EXPECT_TRUE(def->getEncapsulatedSpace().empty());
+}
+
+// The purpose of this test to verify that encapsulated option
+// space name may be specified.
+TEST_F(Dhcp4ParserTest, optionDefEncapsulate) {
+
+ // Configuration string. Included the encapsulated
+ // option space name.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"uint32\","
+ " \"space\": \"isc\","
+ " \"encapsulate\": \"sub-opts-space\""
+ " } ]"
+ "}";
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseOPTION_DEFS(config));
+ extractConfig(config);
+
+ // Make sure that the particular option definition does not exist.
+ OptionDefinitionPtr def = CfgMgr::instance().getStagingCfg()->
+ getCfgOptionDef()->get("isc", 100);
+ ASSERT_FALSE(def);
+
+ // Use the configuration string to create new option definition.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ // The option definition should now be available in the CfgMgr.
+ def = CfgMgr::instance().getStagingCfg()->
+ getCfgOptionDef()->get("isc", 100);
+ ASSERT_TRUE(def);
+
+ // Check the option data.
+ EXPECT_EQ("foo", def->getName());
+ EXPECT_EQ(100, def->getCode());
+ EXPECT_EQ(OPT_UINT32_TYPE, def->getType());
+ EXPECT_FALSE(def->getArrayType());
+ EXPECT_EQ("sub-opts-space", def->getEncapsulatedSpace());
+}
+
+/// The purpose of this test is to verify that the option definition
+/// with invalid name is not accepted.
+TEST_F(Dhcp4ParserTest, optionDefInvalidName) {
+ // Configuration string. The option name is invalid as it
+ // contains the % character.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"invalid%name\","
+ " \"code\": 100,"
+ " \"type\": \"string\","
+ " \"space\": \"isc\""
+ " } ]"
+ "}";
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseOPTION_DEFS(config));
+
+ // Use the configuration string to create new option definition.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ // Expecting parsing error (error code 1).
+ checkResult(status, 1);
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+}
+
+/// The purpose of this test is to verify that the option definition
+/// with invalid type is not accepted.
+TEST_F(Dhcp4ParserTest, optionDefInvalidType) {
+ // Configuration string. The option type is invalid. It is
+ // "sting" instead of "string".
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"sting\","
+ " \"space\": \"isc\""
+ " } ]"
+ "}";
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseOPTION_DEFS(config));
+
+ // Use the configuration string to create new option definition.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ // Expecting parsing error (error code 1).
+ checkResult(status, 1);
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+}
+
+/// The purpose of this test is to verify that the option definition
+/// with invalid type is not accepted.
+TEST_F(Dhcp4ParserTest, optionDefInvalidRecordType) {
+ // Configuration string. The third of the record fields
+ // is invalid. It is "sting" instead of "string".
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"record\","
+ " \"record-types\": \"uint32,uint8,sting\","
+ " \"space\": \"isc\""
+ " } ]"
+ "}";
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseOPTION_DEFS(config));
+
+ // Use the configuration string to create new option definition.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ // Expecting parsing error (error code 1).
+ checkResult(status, 1);
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+}
+
+/// The purpose of this test is to verify that various integer types
+/// are supported.
+TEST_F(Dhcp4ParserTest, optionIntegerTypes) {
+ // Configuration string. The third of the record fields
+ // is invalid. It is "sting" instead of "string".
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"record\","
+ " \"record-types\": \"uint8,uint16,uint32,int8,int16,int32\","
+ " \"space\": \"isc\""
+ " } ]"
+ "}";
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseOPTION_DEFS(config));
+
+ // Use the configuration string to create new option definition.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ // Expecting parsing error (error code 1).
+ checkResult(status, 0);
+}
+
+/// The goal of this test is to verify that the invalid encapsulated
+/// option space name is not accepted.
+TEST_F(Dhcp4ParserTest, optionDefInvalidEncapsulatedSpace) {
+ // Configuration string. The encapsulated option space
+ // name is invalid (% character is not allowed).
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"uint32\","
+ " \"space\": \"isc\","
+ " \"encapsulate\": \"invalid%space%name\""
+ " } ]"
+ "}";
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseOPTION_DEFS(config));
+
+ // Use the configuration string to create new option definition.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ // Expecting parsing error (error code 1).
+ checkResult(status, 1);
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+}
+
+/// The goal of this test is to verify that the encapsulated
+/// option space name can't be specified for the option that
+/// comprises an array of data fields.
+TEST_F(Dhcp4ParserTest, optionDefEncapsulatedSpaceAndArray) {
+ // Configuration string. The encapsulated option space
+ // name is set to non-empty value and the array flag
+ // is set.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"uint32\","
+ " \"array\": true,"
+ " \"space\": \"isc\","
+ " \"encapsulate\": \"valid-space-name\""
+ " } ]"
+ "}";
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseOPTION_DEFS(config));
+
+ // Use the configuration string to create new option definition.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ // Expecting parsing error (error code 1).
+ checkResult(status, 1);
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+}
+
+/// The goal of this test is to verify that the option may not
+/// encapsulate option space it belongs to.
+TEST_F(Dhcp4ParserTest, optionDefEncapsulateOwnSpace) {
+ // Configuration string. Option is set to encapsulate
+ // option space it belongs to.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"uint32\","
+ " \"space\": \"isc\","
+ " \"encapsulate\": \"isc\""
+ " } ]"
+ "}";
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseOPTION_DEFS(config));
+
+ // Use the configuration string to create new option definition.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ // Expecting parsing error (error code 1).
+ checkResult(status, 1);
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+}
+
+/// The purpose of this test is to verify that it is not allowed
+/// to override the standard option (that belongs to dhcp4 option
+/// space and has its definition) and that it is allowed to define
+/// option in the dhcp4 option space that has a code which is not
+/// used by any of the standard options.
+TEST_F(Dhcp4ParserTest, optionStandardDefOverride) {
+
+ // Configuration string. The option code 109 is unassigned so it
+ // can be used for a custom option definition in dhcp4 option space.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 109,"
+ " \"type\": \"string\","
+ " \"space\": \"dhcp4\""
+ " } ]"
+ "}";
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseOPTION_DEFS(config));
+ extractConfig(config);
+
+ OptionDefinitionPtr def = CfgMgr::instance().getStagingCfg()->
+ getCfgOptionDef()->get(DHCP4_OPTION_SPACE, 109);
+ ASSERT_FALSE(def);
+
+ // Use the configuration string to create new option definition.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ // The option definition should now be available in the CfgMgr.
+ def = CfgMgr::instance().getStagingCfg()->
+ getCfgOptionDef()->get(DHCP4_OPTION_SPACE, 109);
+ ASSERT_TRUE(def);
+
+ // Check the option data.
+ EXPECT_EQ("foo", def->getName());
+ EXPECT_EQ(109, def->getCode());
+ EXPECT_EQ(OPT_STRING_TYPE, def->getType());
+ EXPECT_FALSE(def->getArrayType());
+
+ // The combination of option space and code is invalid. The 'dhcp4' option
+ // space groups standard options and the code 3 is reserved for one of
+ // them.
+ config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"routers\","
+ " \"code\": 3,"
+ " \"type\": \"ipv4-address\","
+ " \"space\": \"dhcp4\""
+ " } ]"
+ "}";
+ ASSERT_NO_THROW(json = parseOPTION_DEFS(config));
+
+ // Use the configuration string to create new option definition.
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ // Expecting parsing error (error code 1).
+ checkResult(status, 1);
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+
+ /// There is no definition for unassigned option 170.
+ config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"unassigned-option-170\","
+ " \"code\": 170,"
+ " \"type\": \"string\","
+ " \"space\": \"dhcp4\""
+ " } ]"
+ "}";
+ ASSERT_NO_THROW(json = parseOPTION_DEFS(config));
+ extractConfig(config);
+
+ // Use the configuration string to create new option definition.
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ // Expecting success.
+ checkResult(status, 0);
+
+ def = CfgMgr::instance().getStagingCfg()->
+ getCfgOptionDef()->get(DHCP4_OPTION_SPACE, 170);
+ ASSERT_TRUE(def);
+
+ // Check the option data.
+ EXPECT_EQ("unassigned-option-170", def->getName());
+ EXPECT_EQ(170, def->getCode());
+ EXPECT_EQ(OPT_STRING_TYPE, def->getType());
+ EXPECT_FALSE(def->getArrayType());
+}
+
+// Goal of this test is to verify that global option data is configured
+TEST_F(Dhcp4ParserTest, optionDataDefaultsGlobal) {
+ ConstElementPtr x;
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000,"
+ "\"renew-timer\": 1000,"
+ "\"option-data\": [ {"
+ " \"name\": \"dhcp-message\","
+ " \"data\": \"ABCDEF0105\","
+ " \"csv-format\": false"
+ " },"
+ " {"
+ " \"name\": \"default-ip-ttl\","
+ " \"data\": \"01\","
+ " \"csv-format\": false"
+ " } ],"
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\""
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 0);
+
+ // These options are global
+ Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
+ getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.200"));
+ ASSERT_TRUE(subnet);
+ OptionContainerPtr options = subnet->getCfgOption()->getAll(DHCP4_OPTION_SPACE);
+ ASSERT_EQ(0, options->size());
+
+ options = CfgMgr::instance().getStagingCfg()->getCfgOption()->getAll(DHCP4_OPTION_SPACE);
+ ASSERT_EQ(2, options->size());
+
+ // Get the search index. Index #1 is to search using option code.
+ const OptionContainerTypeIndex& idx = options->get<1>();
+
+ // Get the options for specified index. Expecting one option to be
+ // returned but in theory we may have multiple options with the same
+ // code so we get the range.
+ std::pair<OptionContainerTypeIndex::const_iterator,
+ OptionContainerTypeIndex::const_iterator> range =
+ idx.equal_range(56);
+ // Expect single option with the code equal to 56.
+ ASSERT_EQ(1, std::distance(range.first, range.second));
+ const uint8_t foo_expected[] = {
+ 0xAB, 0xCD, 0xEF, 0x01, 0x05
+ };
+ // Check if option is valid in terms of code and carried data.
+ testOption(*range.first, 56, foo_expected, sizeof(foo_expected));
+
+ range = idx.equal_range(23);
+ ASSERT_EQ(1, std::distance(range.first, range.second));
+ // Do another round of testing with second option.
+ const uint8_t foo2_expected[] = {
+ 0x01
+ };
+ testOption(*range.first, 23, foo2_expected, sizeof(foo2_expected));
+
+ // Check that options with other option codes are not returned.
+ for (uint8_t code = 24; code < 35; ++code) {
+ range = idx.equal_range(code);
+ EXPECT_EQ(0, std::distance(range.first, range.second));
+ }
+}
+
+// Goal of this test is to verify that subnet option data is configured
+TEST_F(Dhcp4ParserTest, optionDataDefaultsSubnet) {
+ ConstElementPtr x;
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000,"
+ "\"renew-timer\": 1000,"
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\","
+ " \"option-data\": [ {"
+ " \"name\": \"dhcp-message\","
+ " \"data\": \"ABCDEF0105\","
+ " \"csv-format\": false"
+ " },"
+ " {"
+ " \"name\": \"default-ip-ttl\","
+ " \"data\": \"01\","
+ " \"csv-format\": false"
+ " } ]"
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 0);
+
+ // These options are subnet options
+ OptionContainerPtr options =
+ CfgMgr::instance().getStagingCfg()->getCfgOption()->getAll(DHCP4_OPTION_SPACE);
+ ASSERT_EQ(0, options->size());
+
+ Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
+ getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.200"));
+ ASSERT_TRUE(subnet);
+ options = subnet->getCfgOption()->getAll(DHCP4_OPTION_SPACE);
+ ASSERT_EQ(2, options->size());
+
+ // Get the search index. Index #1 is to search using option code.
+ const OptionContainerTypeIndex& idx = options->get<1>();
+
+ // Get the options for specified index. Expecting one option to be
+ // returned but in theory we may have multiple options with the same
+ // code so we get the range.
+ std::pair<OptionContainerTypeIndex::const_iterator,
+ OptionContainerTypeIndex::const_iterator> range =
+ idx.equal_range(56);
+ // Expect single option with the code equal to 56.
+ ASSERT_EQ(1, std::distance(range.first, range.second));
+ const uint8_t foo_expected[] = {
+ 0xAB, 0xCD, 0xEF, 0x01, 0x05
+ };
+ // Check if option is valid in terms of code and carried data.
+ testOption(*range.first, 56, foo_expected, sizeof(foo_expected));
+
+ range = idx.equal_range(23);
+ ASSERT_EQ(1, std::distance(range.first, range.second));
+ // Do another round of testing with second option.
+ const uint8_t foo2_expected[] = {
+ 0x01
+ };
+ testOption(*range.first, 23, foo2_expected, sizeof(foo2_expected));
+}
+
+/// The goal of this test is to verify that two options having the same
+/// option code can be added to different option spaces.
+TEST_F(Dhcp4ParserTest, optionDataTwoSpaces) {
+
+ // This configuration string is to configure two options
+ // sharing the code 56 and having different definitions
+ // and belonging to the different option spaces.
+ // The option definition must be provided for the
+ // option that belongs to the 'isc' option space.
+ // The definition is not required for the option that
+ // belongs to the 'dhcp4' option space as it is the
+ // standard option.
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"valid-lifetime\": 4000,"
+ "\"rebind-timer\": 2000,"
+ "\"renew-timer\": 1000,"
+ "\"option-data\": [ {"
+ " \"name\": \"dhcp-message\","
+ " \"data\": \"ABCDEF0105\","
+ " \"csv-format\": false"
+ " },"
+ " {"
+ " \"name\": \"foo\","
+ " \"space\": \"isc\","
+ " \"data\": \"1234\""
+ " } ],"
+ "\"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 56,"
+ " \"type\": \"uint32\","
+ " \"space\": \"isc\""
+ " } ],"
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\""
+ " } ]"
+ "}";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ // Options should be now available
+ // Try to get the option from the space dhcp4.
+ OptionDescriptor desc1 =
+ CfgMgr::instance().getStagingCfg()->getCfgOption()->get(DHCP4_OPTION_SPACE, 56);
+ ASSERT_TRUE(desc1.option_);
+ EXPECT_EQ(56, desc1.option_->getType());
+ // Try to get the option from the space isc.
+ OptionDescriptor desc2 =
+ CfgMgr::instance().getStagingCfg()->getCfgOption()->get("isc", 56);
+ ASSERT_TRUE(desc2.option_);
+ EXPECT_EQ(56, desc1.option_->getType());
+ // Try to get the non-existing option from the non-existing
+ // option space and expect that option is not returned.
+ OptionDescriptor desc3 =
+ CfgMgr::instance().getStagingCfg()->getCfgOption()->get("non-existing", 56);
+ ASSERT_FALSE(desc3.option_);
+}
+
+// The goal of this test is to verify that it is possible to
+// encapsulate option space containing some options with
+// another option. In this test we create base option that
+// encapsulates option space 'isc' that comprises two other
+// options. Also, for all options their definitions are
+// created.
+TEST_F(Dhcp4ParserTest, optionDataEncapsulate) {
+
+ // @todo DHCP configurations has many dependencies between
+ // parameters. First of all, configuration for subnet was
+ // inherited from the global values. Thus subnet had to be
+ // configured when all global values have been configured.
+ // Also, an option can encapsulate another option only
+ // if the latter has been configured. For this reason in this
+ // test we created two-stage configuration where first we
+ // created options that belong to encapsulated option space.
+ // In the second stage we add the base option. Also, the Subnet
+ // object is configured in the second stage so it is created
+ // at the very end (when all other parameters are configured).
+
+ // Starting stage 1. Configure sub-options and their definitions.
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"valid-lifetime\": 4000,"
+ "\"rebind-timer\": 2000,"
+ "\"renew-timer\": 1000,"
+ "\"option-data\": [ {"
+ " \"name\": \"foo\","
+ " \"space\": \"isc\","
+ " \"data\": \"1234\""
+ " },"
+ " {"
+ " \"name\": \"foo2\","
+ " \"space\": \"isc\","
+ " \"data\": \"192.168.2.1\""
+ " } ],"
+ "\"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 1,"
+ " \"type\": \"uint32\","
+ " \"space\": \"isc\""
+ " },"
+ " {"
+ " \"name\": \"foo2\","
+ " \"code\": 2,"
+ " \"type\": \"ipv4-address\","
+ " \"space\": \"isc\""
+ " } ]"
+ "}";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ CfgMgr::instance().clear();
+
+ // Stage 2. Configure base option and a subnet. Please note that
+ // the configuration from the stage 2 is repeated because Kea
+ // configuration manager sends whole configuration for the lists
+ // where at least one element is being modified or added.
+ config = "{ " + genIfaceConfig() + "," +
+ "\"valid-lifetime\": 3000,"
+ "\"rebind-timer\": 2000,"
+ "\"renew-timer\": 1000,"
+ "\"option-data\": [ {"
+ " \"name\": \"base-option\","
+ " \"data\": \"11\""
+ " },"
+ " {"
+ " \"name\": \"foo\","
+ " \"space\": \"isc\","
+ " \"data\": \"1234\""
+ " },"
+ " {"
+ " \"name\": \"foo2\","
+ " \"space\": \"isc\","
+ " \"data\": \"192.168.2.1\""
+ " } ],"
+ "\"option-def\": [ {"
+ " \"name\": \"base-option\","
+ " \"code\": 222,"
+ " \"type\": \"uint8\","
+ " \"space\": \"dhcp4\","
+ " \"encapsulate\": \"isc\""
+ "},"
+ "{"
+ " \"name\": \"foo\","
+ " \"code\": 1,"
+ " \"type\": \"uint32\","
+ " \"space\": \"isc\""
+ " },"
+ " {"
+ " \"name\": \"foo2\","
+ " \"code\": 2,"
+ " \"type\": \"ipv4-address\","
+ " \"space\": \"isc\""
+ " } ],"
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\""
+ " } ]"
+ "}";
+
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ // We should have one option available.
+ OptionContainerPtr options =
+ CfgMgr::instance().getStagingCfg()->getCfgOption()->getAll(DHCP4_OPTION_SPACE);
+ ASSERT_TRUE(options);
+ ASSERT_EQ(1, options->size());
+
+ // Get the option.
+ OptionDescriptor desc =
+ CfgMgr::instance().getStagingCfg()->getCfgOption()->get(DHCP4_OPTION_SPACE, 222);
+ EXPECT_TRUE(desc.option_);
+ EXPECT_EQ(222, desc.option_->getType());
+
+ // This option should comprise two sub-options.
+ // One of them is 'foo' with code 1.
+ OptionPtr option_foo = desc.option_->getOption(1);
+ ASSERT_TRUE(option_foo);
+ EXPECT_EQ(1, option_foo->getType());
+
+ // ...another one 'foo2' with code 2.
+ OptionPtr option_foo2 = desc.option_->getOption(2);
+ ASSERT_TRUE(option_foo2);
+ EXPECT_EQ(2, option_foo2->getType());
+}
+
+// Goal of this test is to verify options configuration
+// for a single subnet. In particular this test checks
+// that local options configuration overrides global
+// option setting.
+TEST_F(Dhcp4ParserTest, optionDataInSingleSubnet) {
+ ConstElementPtr x;
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"option-data\": [ {"
+ " \"name\": \"dhcp-message\","
+ " \"data\": \"AB\","
+ " \"csv-format\": false"
+ " } ],"
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"option-data\": [ {"
+ " \"name\": \"dhcp-message\","
+ " \"data\": \"ABCDEF0105\","
+ " \"csv-format\": false"
+ " },"
+ " {"
+ " \"name\": \"default-ip-ttl\","
+ " \"data\": \"01\","
+ " \"csv-format\": false"
+ " } ]"
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 0);
+
+ Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
+ getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.24"));
+ ASSERT_TRUE(subnet);
+ OptionContainerPtr options = subnet->getCfgOption()->getAll(DHCP4_OPTION_SPACE);
+ ASSERT_EQ(2, options->size());
+
+ // Get the search index. Index #1 is to search using option code.
+ const OptionContainerTypeIndex& idx = options->get<1>();
+
+ // Get the options for specified index. Expecting one option to be
+ // returned but in theory we may have multiple options with the same
+ // code so we get the range.
+ std::pair<OptionContainerTypeIndex::const_iterator,
+ OptionContainerTypeIndex::const_iterator> range =
+ idx.equal_range(56);
+ // Expect single option with the code equal to 100.
+ ASSERT_EQ(1, std::distance(range.first, range.second));
+ const uint8_t foo_expected[] = {
+ 0xAB, 0xCD, 0xEF, 0x01, 0x05
+ };
+ // Check if option is valid in terms of code and carried data.
+ testOption(*range.first, 56, foo_expected, sizeof(foo_expected));
+
+ range = idx.equal_range(23);
+ ASSERT_EQ(1, std::distance(range.first, range.second));
+ // Do another round of testing with second option.
+ const uint8_t foo2_expected[] = {
+ 0x01
+ };
+ testOption(*range.first, 23, foo2_expected, sizeof(foo2_expected));
+}
+
+// The goal of this test is to check that the option carrying a boolean
+// value can be configured using one of the values: "true", "false", "0"
+// or "1".
+TEST_F(Dhcp4ParserTest, optionDataBoolean) {
+ // Create configuration. Use standard option 19 (ip-forwarding).
+ std::map<std::string, std::string> params;
+ params["name"] = "ip-forwarding";
+ params["space"] = DHCP4_OPTION_SPACE;
+ params["code"] = "19";
+ params["data"] = "true";
+ params["csv-format"] = "true";
+
+ std::string config = createConfigWithOption(params);
+ ASSERT_TRUE(executeConfiguration(config, "parse configuration with a"
+ " boolean value"));
+
+ // The subnet should now hold one option with the code 19.
+ OptionDescriptor desc = getOptionFromSubnet(IOAddress("192.0.2.24"),
+ 19);
+ ASSERT_TRUE(desc.option_);
+
+ // This option should be set to "true", represented as 0x1 in the option
+ // buffer.
+ uint8_t expected_option_data[] = {
+ 0x1
+ };
+ testConfiguration(params, 19, expected_option_data,
+ sizeof(expected_option_data));
+
+ // Configure the option with the "1" value. This should have the same
+ // effect as if "true" was specified.
+ params["data"] = "1";
+ testConfiguration(params, 19, expected_option_data,
+ sizeof(expected_option_data));
+
+ // The value of "1" with a few leading zeros should work too.
+ params["data"] = "00001";
+ testConfiguration(params, 19, expected_option_data,
+ sizeof(expected_option_data));
+
+ // Configure the option with the "false" value.
+ params["data"] = "false";
+ // The option buffer should now hold the value of 0.
+ expected_option_data[0] = 0;
+ testConfiguration(params, 19, expected_option_data,
+ sizeof(expected_option_data));
+
+ // Specifying "0" should have the same effect as "false".
+ params["data"] = "0";
+ testConfiguration(params, 19, expected_option_data,
+ sizeof(expected_option_data));
+
+ // The same effect should be for multiple 0 chars.
+ params["data"] = "00000";
+ testConfiguration(params, 19, expected_option_data,
+ sizeof(expected_option_data));
+
+ // Bogus values should not be accepted.
+ params["data"] = "bogus";
+ testInvalidOptionParam(params);
+
+ params["data"] = "2";
+ testInvalidOptionParam(params);
+
+ // Now let's test that it is possible to use binary format.
+ params["data"] = "0";
+ params["csv-format"] = "false";
+ testConfiguration(params, 19, expected_option_data,
+ sizeof(expected_option_data));
+
+ // The binary 1 should work as well.
+ params["data"] = "1";
+ expected_option_data[0] = 1;
+ testConfiguration(params, 19, expected_option_data,
+ sizeof(expected_option_data));
+
+ // As well as an even number of digits.
+ params["data"] = "01";
+ testConfiguration(params, 19, expected_option_data,
+ sizeof(expected_option_data));
+
+}
+
+// Goal of this test is to verify options configuration
+// for multiple subnets.
+TEST_F(Dhcp4ParserTest, optionDataInMultipleSubnets) {
+ ConstElementPtr x;
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"option-data\": [ {"
+ " \"name\": \"dhcp-message\","
+ " \"data\": \"0102030405060708090A\","
+ " \"csv-format\": false"
+ " } ]"
+ " },"
+ " {"
+ " \"id\": 2,"
+ " \"pools\": [ { \"pool\": \"192.0.3.101 - 192.0.3.150\" } ],"
+ " \"subnet\": \"192.0.3.0/24\", "
+ " \"option-data\": [ {"
+ " \"name\": \"default-ip-ttl\","
+ " \"data\": \"FF\","
+ " \"csv-format\": false"
+ " } ]"
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 0);
+
+ Subnet4Ptr subnet1 = CfgMgr::instance().getStagingCfg()->
+ getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.100"));
+ ASSERT_TRUE(subnet1);
+ OptionContainerPtr options1 = subnet1->getCfgOption()->getAll(DHCP4_OPTION_SPACE);
+ ASSERT_EQ(1, options1->size());
+
+ // Get the search index. Index #1 is to search using option code.
+ const OptionContainerTypeIndex& idx1 = options1->get<1>();
+
+ // Get the options for specified index. Expecting one option to be
+ // returned but in theory we may have multiple options with the same
+ // code so we get the range.
+ std::pair<OptionContainerTypeIndex::const_iterator,
+ OptionContainerTypeIndex::const_iterator> range1 =
+ idx1.equal_range(56);
+ // Expect single option with the code equal to 56.
+ ASSERT_EQ(1, std::distance(range1.first, range1.second));
+ const uint8_t foo_expected[] = {
+ 0x01, 0x02, 0x03, 0x04, 0x05,
+ 0x06, 0x07, 0x08, 0x09, 0x0A
+ };
+ // Check if option is valid in terms of code and carried data.
+ testOption(*range1.first, 56, foo_expected, sizeof(foo_expected));
+
+ // Test another subnet in the same way.
+ Subnet4Ptr subnet2 = CfgMgr::instance().getStagingCfg()->
+ getCfgSubnets4()->selectSubnet(IOAddress("192.0.3.102"));
+ ASSERT_TRUE(subnet2);
+ OptionContainerPtr options2 = subnet2->getCfgOption()->getAll(DHCP4_OPTION_SPACE);
+ ASSERT_EQ(1, options2->size());
+
+ const OptionContainerTypeIndex& idx2 = options2->get<1>();
+ std::pair<OptionContainerTypeIndex::const_iterator,
+ OptionContainerTypeIndex::const_iterator> range2 =
+ idx2.equal_range(23);
+ ASSERT_EQ(1, std::distance(range2.first, range2.second));
+
+ const uint8_t foo2_expected[] = { 0xFF };
+ testOption(*range2.first, 23, foo2_expected, sizeof(foo2_expected));
+}
+
+// This test verifies that it is possible to specify options on
+// pool levels.
+TEST_F(Dhcp4ParserTest, optionDataSinglePool) {
+ ConstElementPtr x;
+ string config = "{ " + genIfaceConfig() + ","
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { "
+ " \"pool\": \"192.0.2.1 - 192.0.2.100\","
+ " \"option-data\": [ {"
+ " \"name\": \"dhcp-message\","
+ " \"data\": \"ABCDEF0105\","
+ " \"csv-format\": false"
+ " },"
+ " {"
+ " \"name\": \"default-ip-ttl\","
+ " \"data\": \"01\","
+ " \"csv-format\": false"
+ " } ]"
+ " } ],"
+ " \"subnet\": \"192.0.2.0/24\""
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 0);
+
+ Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->
+ selectSubnet(IOAddress("192.0.2.24"), classify_);
+ ASSERT_TRUE(subnet);
+
+ PoolPtr pool = subnet->getPool(Lease::TYPE_V4, IOAddress("192.0.2.24"), false);
+ ASSERT_TRUE(pool);
+ Pool4Ptr pool4 = boost::dynamic_pointer_cast<Pool4>(pool);
+ ASSERT_TRUE(pool4);
+
+ OptionContainerPtr options =
+ pool4->getCfgOption()->getAll(DHCP4_OPTION_SPACE);
+ ASSERT_EQ(2, options->size());
+
+ // Get the search index. Index #1 is to search using option code.
+ const OptionContainerTypeIndex& idx = options->get<1>();
+
+ // Get the options for specified index. Expecting one option to be
+ // returned but in theory we may have multiple options with the same
+ // code so we get the range.
+ std::pair<OptionContainerTypeIndex::const_iterator,
+ OptionContainerTypeIndex::const_iterator> range =
+ idx.equal_range(56);
+ // Expect a single option with the code equal to 100.
+ ASSERT_EQ(1, std::distance(range.first, range.second));
+ const uint8_t foo_expected[] = {
+ 0xAB, 0xCD, 0xEF, 0x01, 0x05
+ };
+ // Check if option is valid in terms of code and carried data.
+ testOption(*range.first, 56, foo_expected, sizeof(foo_expected));
+
+ range = idx.equal_range(23);
+ ASSERT_EQ(1, std::distance(range.first, range.second));
+ // Do another round of testing with second option.
+ const uint8_t foo2_expected[] = {
+ 0x01
+ };
+ testOption(*range.first, 23, foo2_expected, sizeof(foo2_expected));
+}
+
+// This test verifies that it's possible to define different options in
+// different pools and those options are not confused.
+TEST_F(Dhcp4ParserTest, optionDataMultiplePools) {
+ ConstElementPtr x;
+ string config = "{ " + genIfaceConfig() + ","
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { "
+ " \"pool\": \"192.0.2.1 - 192.0.2.100\","
+ " \"option-data\": [ {"
+ " \"name\": \"dhcp-message\","
+ " \"data\": \"ABCDEF0105\","
+ " \"csv-format\": false"
+ " } ]"
+ " },"
+ " {"
+ " \"pool\": \"192.0.2.200 - 192.0.2.250\","
+ " \"option-data\": [ {"
+ " \"name\": \"default-ip-ttl\","
+ " \"data\": \"01\","
+ " \"csv-format\": false"
+ " } ]"
+ " } ],"
+ " \"subnet\": \"192.0.2.0/24\""
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 0);
+
+ Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->
+ selectSubnet(IOAddress("192.0.2.24"), classify_);
+ ASSERT_TRUE(subnet);
+
+ PoolPtr pool1 = subnet->getPool(Lease::TYPE_V4, IOAddress("192.0.2.24"), false);
+ ASSERT_TRUE(pool1);
+ Pool4Ptr pool41 = boost::dynamic_pointer_cast<Pool4>(pool1);
+ ASSERT_TRUE(pool41);
+
+ OptionContainerPtr options1 =
+ pool41->getCfgOption()->getAll(DHCP4_OPTION_SPACE);
+ ASSERT_EQ(1, options1->size());
+
+ // Get the search index. Index #1 is to search using option code.
+ const OptionContainerTypeIndex& idx1 = options1->get<1>();
+
+ // Get the options for specified index. Expecting one option to be
+ // returned but in theory we may have multiple options with the same
+ // code so we get the range.
+ std::pair<OptionContainerTypeIndex::const_iterator,
+ OptionContainerTypeIndex::const_iterator> range1 =
+ idx1.equal_range(56);
+ // Expect a single option with the code equal to 100.
+ ASSERT_EQ(1, std::distance(range1.first, range1.second));
+ const uint8_t foo_expected[] = {
+ 0xAB, 0xCD, 0xEF, 0x01, 0x05
+ };
+ // Check if option is valid in terms of code and carried data.
+ testOption(*range1.first, 56, foo_expected, sizeof(foo_expected));
+
+ // Test another pool in the same way.
+ PoolPtr pool2 = subnet->getPool(Lease::TYPE_V4, IOAddress("192.0.2.240"), false);
+ ASSERT_TRUE(pool2);
+ Pool4Ptr pool42 = boost::dynamic_pointer_cast<Pool4>(pool2);
+ ASSERT_TRUE(pool42);
+
+ OptionContainerPtr options2 =
+ pool42->getCfgOption()->getAll(DHCP4_OPTION_SPACE);
+ ASSERT_EQ(1, options2->size());
+
+ const OptionContainerTypeIndex& idx2 = options2->get<1>();
+ std::pair<OptionContainerTypeIndex::const_iterator,
+ OptionContainerTypeIndex::const_iterator> range2 =
+ idx2.equal_range(23);
+ ASSERT_EQ(1, std::distance(range2.first, range2.second));
+ const uint8_t foo2_expected[] = {
+ 0x01
+ };
+ testOption(*range2.first, 23, foo2_expected, sizeof(foo2_expected));
+}
+
+// Verify that empty option name is rejected in the configuration.
+TEST_F(Dhcp4ParserTest, optionNameEmpty) {
+ // Empty option names not allowed.
+ testInvalidOptionParam("", "name");
+}
+
+// Verify that empty option name with spaces is rejected
+// in the configuration.
+TEST_F(Dhcp4ParserTest, optionNameSpaces) {
+ // Spaces in option names not allowed.
+ testInvalidOptionParam("option foo", "name");
+}
+
+// Verify that negative option code is rejected in the configuration.
+TEST_F(Dhcp4ParserTest, optionCodeNegative) {
+ // Check negative option code -4. This should fail too.
+ testInvalidOptionParam("-4", "code");
+}
+
+// Verify that out of bounds option code is rejected in the configuration.
+TEST_F(Dhcp4ParserTest, optionCodeNonUint8) {
+ // The valid option codes are uint16_t values so passing
+ // uint16_t maximum value incremented by 1 should result
+ // in failure.
+ testInvalidOptionParam("257", "code");
+}
+
+// Verify that zero option code is rejected in the configuration.
+TEST_F(Dhcp4ParserTest, optionCodeZero) {
+ // Option code 0 is reserved and should not be accepted
+ // by configuration parser.
+ testInvalidOptionParam("0", "code");
+}
+
+// Verify that invalid hex literals for option data are detected.
+TEST_F(Dhcp4ParserTest, optionDataInvalidHexLiterals) {
+ testInvalidOptionParam("01020R", "data"); // non hex digit
+ testInvalidOptionParam("0x01:02", "data"); // 0x prefix with colon separator
+ testInvalidOptionParam("0x01 02", "data"); // 0x prefix with space separator
+ testInvalidOptionParam("0X0102", "data"); // 0X upper case X in prefix
+ testInvalidOptionParam("01.02", "data"); // invalid separator
+}
+
+// Verify the valid forms hex literals in option data are supported.
+TEST_F(Dhcp4ParserTest, optionDataValidHexLiterals) {
+
+ std::vector<std::string> valid_hexes =
+ {
+ "0a0b0C0D", // upper and lower case
+ "0A:0B:0C:0D", // colon seperator
+ "0A 0B 0C 0D", // space seperator
+ "A0B0C0D", // odd number of digits
+ "0xA0B0C0D" // 0x prefix
+ };
+
+ for (auto valid_hex : valid_hexes) {
+ ConstElementPtr x;
+ std::string config = createConfigWithOption(valid_hex, "data");
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 0);
+
+ Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
+ getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.5"));
+ ASSERT_TRUE(subnet);
+ OptionContainerPtr options = subnet->getCfgOption()->getAll(DHCP4_OPTION_SPACE);
+ ASSERT_EQ(1, options->size());
+
+ // Get the search index. Index #1 is to search using option code.
+ const OptionContainerTypeIndex& idx = options->get<1>();
+
+ // Get the options for specified index. Expecting one option to be
+ // returned but in theory we may have multiple options with the same
+ // code so we get the range.
+ std::pair<OptionContainerTypeIndex::const_iterator,
+ OptionContainerTypeIndex::const_iterator> range = idx.equal_range(56);
+ // Expect single option with the code equal to 100.
+ ASSERT_EQ(1, std::distance(range.first, range.second));
+ const uint8_t foo_expected[] = { 0x0A, 0x0B, 0x0C, 0x0D };
+
+ // Check if option is valid in terms of code and carried data.
+ testOption(*range.first, 56, foo_expected, sizeof(foo_expected));
+
+ // Clear configuration for the next pass.
+ resetConfiguration();
+ }
+}
+
+// Verify that specific option object is returned for standard
+// option which has dedicated option class derived from Option.
+TEST_F(Dhcp4ParserTest, stdOptionData) {
+ ConstElementPtr x;
+ std::map<std::string, std::string> params;
+ params["name"] = "nis-servers";
+ params["space"] = DHCP4_OPTION_SPACE;
+ // Option code 41 means nis-servers.
+ params["code"] = "41";
+ // Specify option values in a CSV (user friendly) format.
+ params["data"] = "192.0.2.10, 192.0.2.1, 192.0.2.3";
+ params["csv-format"] = "true";
+
+ std::string config = createConfigWithOption(params);
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 0);
+
+ Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
+ getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.5"));
+ ASSERT_TRUE(subnet);
+ OptionContainerPtr options =
+ subnet->getCfgOption()->getAll(DHCP4_OPTION_SPACE);
+ ASSERT_TRUE(options);
+ ASSERT_EQ(1, options->size());
+
+ // Get the search index. Index #1 is to search using option code.
+ const OptionContainerTypeIndex& idx = options->get<1>();
+
+ // Get the options for specified index. Expecting one option to be
+ // returned but in theory we may have multiple options with the same
+ // code so we get the range.
+ std::pair<OptionContainerTypeIndex::const_iterator,
+ OptionContainerTypeIndex::const_iterator> range =
+ idx.equal_range(DHO_NIS_SERVERS);
+ // Expect single option with the code equal to NIS_SERVERS option code.
+ ASSERT_EQ(1, std::distance(range.first, range.second));
+ // The actual pointer to the option is held in the option field
+ // in the structure returned.
+ OptionPtr option = range.first->option_;
+ ASSERT_TRUE(option);
+ // Option object returned for here is expected to be Option4AddrLst
+ // which is derived from Option. This class is dedicated to
+ // represent standard option DHO_NIS_SERVERS.
+ boost::shared_ptr<Option4AddrLst> option_addrs =
+ boost::dynamic_pointer_cast<Option4AddrLst>(option);
+ // If cast is unsuccessful than option returned was of a
+ // different type than Option4AddrLst. This is wrong.
+ ASSERT_TRUE(option_addrs);
+
+ // Get addresses from the option.
+ Option4AddrLst::AddressContainer addrs = option_addrs->getAddresses();
+ // Verify that the addresses have been configured correctly.
+ ASSERT_EQ(3, addrs.size());
+ EXPECT_EQ("192.0.2.10", addrs[0].toText());
+ EXPECT_EQ("192.0.2.1", addrs[1].toText());
+ EXPECT_EQ("192.0.2.3", addrs[2].toText());
+}
+
+/// This test checks if Uint32Parser can really parse the whole range
+/// and properly err of out of range values. As we can't call Uint32Parser
+/// directly, we are exploiting the fact that it is used to parse global
+/// parameter renew-timer and the results are stored in uint32_defaults.
+/// We get the uint32_defaults using a getUint32Defaults functions which
+/// is defined only to access the values from this test.
+TEST_F(Dhcp4ParserTest, DISABLED_Uint32Parser) {
+
+ ConstElementPtr status;
+
+ // CASE 1: 0 - minimum value, should work
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_,
+ parseDHCP4("{\"renew-timer\": 0}")));
+
+ // returned value must be ok (0 is a proper value)
+ checkResult(status, 0);
+ /// @todo: check that the renew-timer is really 0
+
+ // CASE 2: 4294967295U (UINT_MAX) should work as well
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_,
+ parseDHCP4("{\"renew-timer\": 4294967295}")));
+
+ // returned value must be ok (0 is a proper value)
+ checkResult(status, 0);
+ /// @todo: check that the renew-timer is really 4294967295U
+
+ // CASE 3: 4294967296U (UINT_MAX + 1) should not work
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_,
+ parseJSON("{\"renew-timer\": 4294967296}")));
+
+ // returned value must be rejected (1 configuration error)
+ checkResult(status, 1);
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+
+ // CASE 4: -1 (UINT_MIN -1 ) should not work
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_,
+ parseJSON("{\"renew-timer\": -1}")));
+
+ // returned value must be rejected (1 configuration error)
+ checkResult(status, 1);
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+}
+
+// The goal of this test is to verify that the domain-search option
+// can be set using domain names
+TEST_F(Dhcp4ParserTest, domainSearchOption) {
+ // Create configuration.
+ std::map<std::string, std::string> params;
+ params["name"] = "domain-search";
+ params["space"] = DHCP4_OPTION_SPACE;
+ params["code"] = "119"; // DHO_DOMAIN_SEARCH
+ params["data"] = "mydomain.example.com, example.com";
+ params["csv-format"] = "true";
+
+ std::string config = createConfigWithOption(params);
+ EXPECT_TRUE(executeConfiguration(config, "parse configuration with a"
+ " domain-search option"));
+}
+
+// The goal of this test is to verify that the slp-directory-agent
+// option can be set using a trailing array of addresses and
+// slp-service-scope without option scope list
+TEST_F(Dhcp4ParserTest, slpOptions) {
+ ConstElementPtr x;
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000,"
+ "\"renew-timer\": 1000,"
+ "\"option-data\": [ {"
+ " \"name\": \"slp-directory-agent\","
+ " \"data\": \"true, 10.0.0.3, 127.0.0.1\""
+ " },"
+ " {"
+ " \"name\": \"slp-service-scope\","
+ " \"data\": \"false, \""
+ " } ],"
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\""
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config, true));
+ extractConfig(config);
+
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 0);
+
+ // Get options
+ OptionContainerPtr options = CfgMgr::instance().getStagingCfg()->
+ getCfgOption()->getAll(DHCP4_OPTION_SPACE);
+ ASSERT_EQ(2, options->size());
+
+ // Get the search index. Index #1 is to search using option code.
+ const OptionContainerTypeIndex& idx = options->get<1>();
+
+ // Get the options for specified index. Expecting one option to be
+ // returned but in theory we may have multiple options with the same
+ // code so we get the range.
+ std::pair<OptionContainerTypeIndex::const_iterator,
+ OptionContainerTypeIndex::const_iterator> range =
+ idx.equal_range(DHO_DIRECTORY_AGENT);
+ // Expect a single option with the code equal to 78.
+ ASSERT_EQ(1, std::distance(range.first, range.second));
+ const uint8_t sda_expected[] = {
+ 0x01, 0x0a, 0x00, 0x00, 0x03, 0x7f, 0x00, 0x00, 0x01
+ };
+ // Check if option is valid in terms of code and carried data.
+ testOption(*range.first, 78, sda_expected, sizeof(sda_expected));
+
+ range = idx.equal_range(DHO_SERVICE_SCOPE);
+ ASSERT_EQ(1, std::distance(range.first, range.second));
+ // Do another round of testing with second option.
+ const uint8_t sss_expected[] = {
+ 0x00
+ };
+ testOption(*range.first, 79, sss_expected, sizeof(sss_expected));
+}
+
+// The goal of this test is to verify that the standard option can
+// be configured to encapsulate multiple other options.
+TEST_F(Dhcp4ParserTest, stdOptionDataEncapsulate) {
+
+ // The configuration is two stage process in this test.
+ // In the first stage we create definitions of suboptions
+ // that we will add to the base option.
+ // Let's create some dummy options: foo and foo2.
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"valid-lifetime\": 4000,"
+ "\"rebind-timer\": 2000,"
+ "\"renew-timer\": 1000,"
+ "\"option-data\": [ {"
+ " \"name\": \"foo\","
+ " \"space\": \"vendor-encapsulated-options-space\","
+ " \"data\": \"1234\""
+ " },"
+ " {"
+ " \"name\": \"foo2\","
+ " \"space\": \"vendor-encapsulated-options-space\","
+ " \"data\": \"192.168.2.1\""
+ " } ],"
+ "\"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 1,"
+ " \"type\": \"uint32\","
+ " \"space\": \"vendor-encapsulated-options-space\""
+ " },"
+ " {"
+ " \"name\": \"foo2\","
+ " \"code\": 2,"
+ " \"type\": \"ipv4-address\","
+ " \"space\": \"vendor-encapsulated-options-space\""
+ " } ]"
+ "}";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ CfgMgr::instance().clear();
+
+ // Once the definitions have been added we can configure the
+ // standard option #43. This option comprises an enterprise
+ // number and sub options. By convention (introduced in
+ // std_option_defs.h) option named 'vendor-encapsulated-options'
+ // encapsulates the option space named 'vendor-encapsulated-options-space'.
+ // We add our dummy options to this option space and thus
+ // they should be included as sub-options in the
+ // 'vendor-encapsulated-options' option.
+ config = "{ " + genIfaceConfig() + "," +
+ "\"valid-lifetime\": 3000,"
+ "\"rebind-timer\": 2000,"
+ "\"renew-timer\": 1000,"
+ "\"option-data\": [ {"
+ " \"name\": \"vendor-encapsulated-options\","
+ " \"csv-format\": false"
+ " },"
+ " {"
+ " \"name\": \"foo\","
+ " \"space\": \"vendor-encapsulated-options-space\","
+ " \"data\": \"1234\""
+ " },"
+ " {"
+ " \"name\": \"foo2\","
+ " \"space\": \"vendor-encapsulated-options-space\","
+ " \"code\": 2,"
+ " \"data\": \"192.168.2.1\","
+ " \"csv-format\": true"
+ " } ],"
+ "\"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 1,"
+ " \"type\": \"uint32\","
+ " \"space\": \"vendor-encapsulated-options-space\""
+ " },"
+ " {"
+ " \"name\": \"foo2\","
+ " \"code\": 2,"
+ " \"type\": \"ipv4-address\","
+ " \"space\": \"vendor-encapsulated-options-space\""
+ " } ],"
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\""
+ " } ]"
+ "}";
+
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ // We should have one option available.
+ OptionContainerPtr options =
+ CfgMgr::instance().getStagingCfg()->getCfgOption()->getAll(DHCP4_OPTION_SPACE);
+ ASSERT_TRUE(options);
+ ASSERT_EQ(1, options->size());
+
+ // Get the option.
+ OptionDescriptor desc = CfgMgr::instance().getStagingCfg()->
+ getCfgOption()->get(DHCP4_OPTION_SPACE, DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ EXPECT_TRUE(desc.option_);
+ EXPECT_EQ(DHO_VENDOR_ENCAPSULATED_OPTIONS, desc.option_->getType());
+
+ // Option with the code 1 should be added as a sub-option.
+ OptionPtr option_foo = desc.option_->getOption(1);
+ ASSERT_TRUE(option_foo);
+ EXPECT_EQ(1, option_foo->getType());
+ // This option comprises a single uint32_t value thus it is
+ // represented by OptionInt<uint32_t> class. Let's get the
+ // object of this type.
+ boost::shared_ptr<OptionInt<uint32_t> > option_foo_uint32 =
+ boost::dynamic_pointer_cast<OptionInt<uint32_t> >(option_foo);
+ ASSERT_TRUE(option_foo_uint32);
+ // Validate the value according to the configuration.
+ EXPECT_EQ(1234, option_foo_uint32->getValue());
+
+ // Option with the code 2 should be added as a sub-option.
+ OptionPtr option_foo2 = desc.option_->getOption(2);
+ ASSERT_TRUE(option_foo2);
+ EXPECT_EQ(2, option_foo2->getType());
+ // This option comprises the IPV4 address. Such option is
+ // represented by OptionCustom object.
+ OptionCustomPtr option_foo2_v4 =
+ boost::dynamic_pointer_cast<OptionCustom>(option_foo2);
+ ASSERT_TRUE(option_foo2_v4);
+ // Get the IP address carried by this option and validate it.
+ EXPECT_EQ("192.168.2.1", option_foo2_v4->readAddress().toText());
+
+ // Option with the code 3 should not be added.
+ EXPECT_FALSE(desc.option_->getOption(3));
+}
+
+// This test checks if vendor options can be specified in the config file
+// (in hex format), and later retrieved
+TEST_F(Dhcp4ParserTest, vendorOptionsHex) {
+
+ // This configuration string is to configure two options
+ // sharing the code 1 and belonging to the different vendor spaces.
+ // (different vendor-id values).
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"valid-lifetime\": 4000,"
+ "\"rebind-timer\": 2000,"
+ "\"renew-timer\": 1000,"
+ "\"option-data\": [ {"
+ " \"name\": \"option-one\","
+ " \"space\": \"vendor-4491\"," // VENDOR_ID_CABLE_LABS = 4491
+ " \"code\": 100," // just a random code
+ " \"data\": \"ABCDEF0105\","
+ " \"csv-format\": false"
+ " },"
+ " {"
+ " \"name\": \"option-two\","
+ " \"space\": \"vendor-1234\","
+ " \"code\": 100,"
+ " \"data\": \"1234\","
+ " \"csv-format\": false"
+ " } ],"
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1-192.0.2.10\" } ],"
+ " \"subnet\": \"192.0.2.0/24\""
+ " } ]"
+ "}";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ // Try to get the option from the vendor space 4491
+ OptionDescriptor desc1 = CfgMgr::instance().getStagingCfg()->
+ getCfgOption()->get(VENDOR_ID_CABLE_LABS, 100);
+ ASSERT_TRUE(desc1.option_);
+ EXPECT_EQ(100, desc1.option_->getType());
+ // Try to get the option from the vendor space 1234
+ OptionDescriptor desc2 =
+ CfgMgr::instance().getStagingCfg()->getCfgOption()->get(1234, 100);
+ ASSERT_TRUE(desc2.option_);
+ EXPECT_EQ(100, desc1.option_->getType());
+
+ // Try to get the non-existing option from the non-existing
+ // option space and expect that option is not returned.
+ OptionDescriptor desc3 =
+ CfgMgr::instance().getStagingCfg()->getCfgOption()->get(5678, 100);
+ ASSERT_FALSE(desc3.option_);
+}
+
+// This test checks if vendor options can be specified in the config file,
+// (in csv format), and later retrieved
+TEST_F(Dhcp4ParserTest, vendorOptionsCsv) {
+
+ // This configuration string is to configure two options
+ // sharing the code 1 and belonging to the different vendor spaces.
+ // (different vendor-id values).
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"valid-lifetime\": 4000,"
+ "\"rebind-timer\": 2000,"
+ "\"renew-timer\": 1000,"
+ "\"option-data\": [ {"
+ " \"name\": \"foo\","
+ " \"space\": \"vendor-4491\","
+ " \"code\": 100,"
+ " \"data\": \"this is a string vendor-opt\""
+ " } ],"
+ "\"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"string\","
+ " \"space\": \"vendor-4491\""
+ " } ],"
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" "
+ " } ]"
+ "}";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ // Try to get the option from the vendor space 4491
+ OptionDescriptor desc1 = CfgMgr::instance().getStagingCfg()->
+ getCfgOption()->get(VENDOR_ID_CABLE_LABS, 100);
+ ASSERT_TRUE(desc1.option_);
+ EXPECT_EQ(100, desc1.option_->getType());
+
+ // Try to get the non-existing option from the non-existing
+ // option space and expect that option is not returned.
+ OptionDescriptor desc2 =
+ CfgMgr::instance().getStagingCfg()->getCfgOption()->get(5678, 100);
+ ASSERT_FALSE(desc2.option_);
+}
+
+// Tests of the hooks libraries configuration. All tests have the pre-
+// condition (checked in the test fixture's SetUp() method) that no hooks
+// libraries are loaded at the start of the tests.
+
+// Helper function to return a configuration containing an arbitrary number
+// of hooks libraries.
+std::string
+buildHooksLibrariesConfig(const std::vector<std::string>& libraries = {},
+ bool multi_threading = true) {
+
+ // Create the first part of the configuration string.
+ string config =
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"hooks-libraries\": [";
+
+ // Append the libraries (separated by commas if needed)
+ for (unsigned int i = 0; i < libraries.size(); ++i) {
+ if (i > 0) {
+ config += string(", ");
+ }
+ config += (string("{ \"library\": \"") + libraries[i] + string("\" }"));
+ }
+
+ // Append the remainder of the configuration.
+ config += string(
+ "],"
+ "\"valid-lifetime\": 4000,"
+ "\"rebind-timer\": 2000,"
+ "\"renew-timer\": 1000,"
+ "\"option-data\": [ {"
+ " \"name\": \"dhcp-message\","
+ " \"data\": \"ABCDEF0105\","
+ " \"csv-format\": false"
+ " },"
+ " {"
+ " \"name\": \"foo\","
+ " \"space\": \"isc\","
+ " \"data\": \"1234\""
+ " } ],"
+ "\"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 56,"
+ " \"type\": \"uint32\","
+ " \"space\": \"isc\""
+ " } ],"
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\""
+ " } ]");
+
+ config += R"(,
+ "multi-threading": {
+ "enable-multi-threading": )" +
+ string(multi_threading ? "true" : "false") + R"(
+ })";
+
+ config += string("}");
+
+ return (config);
+}
+
+// The goal of this test is to verify the configuration of hooks libraries if
+// none are specified.
+TEST_F(Dhcp4ParserTest, NoHooksLibraries) {
+ // Parse a configuration containing no names.
+ string config = buildHooksLibrariesConfig();
+ if (!executeConfiguration(config,
+ "set configuration with no hooks libraries")) {
+ FAIL() << "Unable to execute configuration";
+
+ } else {
+ // No libraries should be loaded at the end of the test.
+ std::vector<std::string> libraries = HooksManager::getLibraryNames();
+ EXPECT_TRUE(libraries.empty());
+ }
+}
+
+// Verify parsing fails with one library that will fail validation.
+TEST_F(Dhcp4ParserTest, InvalidLibrary) {
+ // Parse a configuration containing a failing library.
+ string config = buildHooksLibrariesConfig({NOT_PRESENT_LIBRARY});
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ ConstElementPtr status;
+ ASSERT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // The status object must not be NULL
+ ASSERT_TRUE(status);
+
+ // Returned value should not be 0
+ comment_ = parseAnswer(rcode_, status);
+ EXPECT_NE(0, rcode_);
+}
+
+// Verify the configuration of hooks libraries with two being specified.
+TEST_F(Dhcp4ParserTest, LibrariesSpecified) {
+ // Marker files should not be present.
+ EXPECT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
+ EXPECT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+
+ // Set up the configuration with two libraries and load them.
+ // Disable multi-threading since one of the libraries is single-threaded.
+ string config = buildHooksLibrariesConfig({CALLOUT_LIBRARY_1, CALLOUT_LIBRARY_2},
+ /* multi_threading = */ false);
+ ASSERT_TRUE(executeConfiguration(config,
+ "load two valid libraries"));
+
+ // Expect two libraries to be loaded in the correct order (load marker file
+ // is present, no unload marker file).
+ std::vector<std::string> libraries = HooksManager::getLibraryNames();
+ ASSERT_EQ(2, libraries.size());
+ EXPECT_TRUE(checkMarkerFile(LOAD_MARKER_FILE, "12"));
+ EXPECT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+
+ // Commit the changes so as we get the fresh configuration for the
+ // second part of this test.
+ CfgMgr::instance().commit();
+
+ // Unload the libraries. The load file should not have changed, but
+ // the unload one should indicate the unload() functions have been run.
+ config = buildHooksLibrariesConfig();
+ ASSERT_TRUE(executeConfiguration(config, "unloading libraries"));
+ EXPECT_TRUE(checkMarkerFile(LOAD_MARKER_FILE, "12"));
+ EXPECT_TRUE(checkMarkerFile(UNLOAD_MARKER_FILE, "21"));
+
+ // Expect the hooks system to say that none are loaded.
+ libraries = HooksManager::getLibraryNames();
+ EXPECT_TRUE(libraries.empty());
+}
+
+// Verify the configuration of hooks libraries which are not compatible with
+// multi threading is rejected.
+TEST_F(Dhcp4ParserTest, IncompatibleLibrary2Specified) {
+ // Marker files should not be present.
+ EXPECT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
+ EXPECT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+
+ std::vector<std::string> libraries;
+ libraries.push_back(string(CALLOUT_LIBRARY_2));
+
+ // Set up the configuration with two libraries and load them.
+ string config = buildHooksLibrariesConfig(libraries, true);
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ ConstElementPtr status;
+ ASSERT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // The status object must not be NULL
+ ASSERT_TRUE(status);
+
+ // Returned value should not be 0
+ comment_ = parseAnswer(rcode_, status);
+ EXPECT_NE(0, rcode_);
+
+ // Expect the library to be rejected by the server (no load marker file, no
+ // unload marker file).
+ EXPECT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
+ EXPECT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+
+ // Expect the hooks system to say that none are loaded.
+ libraries = HooksManager::getLibraryNames();
+ EXPECT_TRUE(libraries.empty());
+}
+
+// Verify the configuration of hooks libraries which are not compatible with
+// multi threading is rejected.
+TEST_F(Dhcp4ParserTest, IncompatibleLibrary3Specified) {
+ // Marker files should not be present.
+ EXPECT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
+ EXPECT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+
+ std::vector<std::string> libraries;
+ libraries.push_back(string(CALLOUT_LIBRARY_3));
+
+ // Set up the configuration with two libraries and load them.
+ string config = buildHooksLibrariesConfig(libraries, true);
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ ConstElementPtr status;
+ ASSERT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // The status object must not be NULL
+ ASSERT_TRUE(status);
+
+ // Returned value should not be 0
+ comment_ = parseAnswer(rcode_, status);
+ EXPECT_NE(0, rcode_);
+
+ // Expect the library to be rejected by the server (no load marker file, no
+ // unload marker file).
+ EXPECT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
+ EXPECT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+
+ // Expect the hooks system to say that none are loaded.
+ libraries = HooksManager::getLibraryNames();
+ EXPECT_TRUE(libraries.empty());
+}
+
+// This test verifies that it is possible to select subset of interfaces
+// on which server should listen.
+TEST_F(Dhcp4ParserTest, selectedInterfaces) {
+ IfaceMgrTestConfig test_config(true);
+
+ ConstElementPtr x;
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"eth0\", \"eth1\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+
+ // Make sure the config manager is clean and there is no hanging
+ // interface configuration.
+ EXPECT_FALSE(test_config.socketOpen("eth0", AF_INET));
+ EXPECT_FALSE(test_config.socketOpen("eth1", AF_INET));
+
+ // Apply configuration.
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ CfgMgr::instance().getStagingCfg()->getCfgIface()->openSockets(AF_INET, 10000);
+
+ // eth0 and eth1 were explicitly selected. eth2 was not.
+ EXPECT_TRUE(test_config.socketOpen("eth0", AF_INET));
+ EXPECT_TRUE(test_config.socketOpen("eth1", AF_INET));
+}
+
+// This test verifies that it is possible to configure the server in such a way
+// that it listens on all interfaces.
+TEST_F(Dhcp4ParserTest, allInterfaces) {
+ IfaceMgrTestConfig test_config(true);
+
+ ConstElementPtr x;
+ // This configuration specifies two interfaces on which server should listen
+ // but it also includes asterisk. The asterisk switches server into the
+ // mode when it listens on all interfaces regardless of what interface names
+ // were specified in the "interfaces" parameter.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"eth0\", \"*\", \"eth1\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+
+ // Make sure there is no old configuration.
+ ASSERT_FALSE(test_config.socketOpen("eth0", AF_INET));
+ ASSERT_FALSE(test_config.socketOpen("eth1", AF_INET));
+
+ // Apply configuration.
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ CfgMgr::instance().getStagingCfg()->getCfgIface()->openSockets(AF_INET, 10000);
+
+ // All interfaces should be now active.
+ ASSERT_TRUE(test_config.socketOpen("eth0", AF_INET));
+ ASSERT_TRUE(test_config.socketOpen("eth1", AF_INET));
+}
+
+// This test verifies that it is possible to select subset of interfaces
+// and addresses.
+TEST_F(Dhcp4ParserTest, selectedInterfacesAndAddresses) {
+ IfaceMgrTestConfig test_config(true);
+
+ ConstElementPtr x;
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"eth0/10.0.0.1\", \"eth1/192.0.2.3\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+
+ ConstElementPtr status;
+
+ // Make sure the config manager is clean and there is no hanging
+ // interface configuration.
+ ASSERT_FALSE(test_config.socketOpen("eth0", "10.0.0.1"));
+ ASSERT_FALSE(test_config.socketOpen("eth1", "192.0.2.3"));
+ ASSERT_FALSE(test_config.socketOpen("eth1", "192.0.2.5"));
+
+ // Apply configuration.
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ CfgMgr::instance().getStagingCfg()->getCfgIface()->openSockets(AF_INET, 10000);
+
+ // An address on eth0 was selected
+ EXPECT_TRUE(test_config.socketOpen("eth0", "10.0.0.1"));
+ // The 192.0.2.3 address on eth1 was selected.
+ EXPECT_TRUE(test_config.socketOpen("eth1", "192.0.2.3"));
+ // The 192.0.2.5 was not selected, thus the socket should not
+ // be bound to this address.
+ EXPECT_FALSE(test_config.socketOpen("eth1", "192.0.2.5"));
+}
+
+// This test verifies that valid d2CliengConfig works correctly.
+TEST_F(Dhcp4ParserTest, d2ClientConfigValid) {
+ ConstElementPtr status;
+
+ // Verify that the D2 configuration 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 = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ " \"dhcp-ddns\" : {"
+ " \"enable-updates\" : true, "
+ " \"server-ip\" : \"192.168.2.1\", "
+ " \"server-port\" : 777, "
+ " \"sender-ip\" : \"192.168.2.2\", "
+ " \"sender-port\" : 778, "
+ " \"max-queue-size\" : 2048, "
+ " \"ncr-protocol\" : \"UDP\", "
+ " \"ncr-format\" : \"JSON\"}, "
+ "\"valid-lifetime\": 4000 }";
+
+ // Convert the JSON string to configuration elements.
+ ConstElementPtr config;
+ ASSERT_NO_THROW(config = parseDHCP4(config_str, true));
+ extractConfig(config_str);
+
+ // Pass the configuration in for parsing.
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*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("192.168.2.2", d2_client_config->getSenderIp().toText());
+ EXPECT_EQ(778, d2_client_config->getSenderPort());
+ EXPECT_EQ(2048, d2_client_config->getMaxQueueSize());
+ EXPECT_EQ(dhcp_ddns::NCR_UDP, d2_client_config->getNcrProtocol());
+ EXPECT_EQ(dhcp_ddns::FMT_JSON, d2_client_config->getNcrFormat());
+
+ // ddns-send-updates should be global default
+ checkGlobal("ddns-send-updates", true);
+
+ // The following, deprecated dhcp-ddns parameters,
+ // should all have global default values.
+ checkGlobal("ddns-override-no-update", false);
+ checkGlobal("ddns-override-client-update", false);
+ checkGlobal("ddns-replace-client-name", "never");
+ checkGlobal("ddns-generated-prefix", "myhost");
+ checkGlobal("ddns-qualifying-suffix", "");
+}
+
+// This test verifies that valid but deprecated dhcp-ddns parameters
+// get moved to the global scope when they do not already exist there.
+TEST_F(Dhcp4ParserTest, d2ClientConfigMoveToGlobal) {
+ ConstElementPtr status;
+
+ // Verify that the D2 configuration 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 = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ " \"dhcp-ddns\" : {"
+ " \"enable-updates\" : true, "
+ " \"server-ip\" : \"192.168.2.1\", "
+ " \"server-port\" : 777, "
+ " \"sender-ip\" : \"192.168.2.2\", "
+ " \"sender-port\" : 778, "
+ " \"max-queue-size\" : 2048, "
+ " \"ncr-protocol\" : \"UDP\", "
+ " \"ncr-format\" : \"JSON\", "
+ " \"override-no-update\" : true, "
+ " \"override-client-update\" : true, "
+ " \"replace-client-name\" : \"when-present\", "
+ " \"generated-prefix\" : \"test.prefix\", "
+ " \"qualifying-suffix\" : \"test.suffix.\", "
+ " \"hostname-char-set\" : \"[^A-Z]\", "
+ " \"hostname-char-replacement\" : \"x\" }, "
+ "\"valid-lifetime\": 4000 }";
+
+ // Convert the JSON string to configuration elements.
+ ConstElementPtr config;
+ ASSERT_NO_THROW(config = parseDHCP4(config_str, true));
+ extractConfig(config_str);
+
+ // Pass the configuration in for parsing.
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*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("192.168.2.2", d2_client_config->getSenderIp().toText());
+ EXPECT_EQ(778, d2_client_config->getSenderPort());
+ EXPECT_EQ(2048, d2_client_config->getMaxQueueSize());
+ EXPECT_EQ(dhcp_ddns::NCR_UDP, d2_client_config->getNcrProtocol());
+ EXPECT_EQ(dhcp_ddns::FMT_JSON, d2_client_config->getNcrFormat());
+
+ // ddns-send-updates should be global default
+ checkGlobal("ddns-send-updates", true);
+
+ // The following should all have been moved from dhcp-ddns.
+ checkGlobal("ddns-override-no-update", true);
+ checkGlobal("ddns-override-client-update", true);
+ checkGlobal("ddns-replace-client-name", "when-present");
+ checkGlobal("ddns-generated-prefix", "test.prefix");
+ checkGlobal("ddns-qualifying-suffix", "test.suffix.");
+ checkGlobal("hostname-char-set", "[^A-Z]");
+ checkGlobal("hostname-char-replacement", "x");
+}
+
+// This test verifies that explicit global values override deprecated
+// dhcp-ddns parameters (i.e. global scope wins)
+TEST_F(Dhcp4ParserTest, d2ClientConfigBoth) {
+ ConstElementPtr status;
+
+ // Verify that the D2 configuration 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 = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ " \"dhcp-ddns\" : {"
+ " \"enable-updates\" : true, "
+ " \"server-ip\" : \"192.168.2.1\", "
+ " \"server-port\" : 777, "
+ " \"sender-ip\" : \"192.168.2.2\", "
+ " \"sender-port\" : 778, "
+ " \"max-queue-size\" : 2048, "
+ " \"ncr-protocol\" : \"UDP\", "
+ " \"ncr-format\" : \"JSON\", "
+ " \"override-no-update\" : false, "
+ " \"override-client-update\" : false, "
+ " \"replace-client-name\" : \"when-present\", "
+ " \"generated-prefix\" : \"d2.prefix\", "
+ " \"qualifying-suffix\" : \"d2.suffix.\", "
+ " \"hostname-char-set\" : \"[^0-9]\", "
+ " \"hostname-char-replacement\" : \"z\" }, "
+ " \"ddns-send-updates\" : false, "
+ " \"ddns-override-no-update\" : true, "
+ " \"ddns-override-client-update\" : true, "
+ " \"ddns-replace-client-name\" : \"always\", "
+ " \"ddns-generated-prefix\" : \"global.prefix\", "
+ " \"ddns-qualifying-suffix\" : \"global.suffix.\", "
+ " \"hostname-char-set\" : \"[^A-Z]\", "
+ " \"hostname-char-replacement\" : \"x\", "
+ "\"valid-lifetime\": 4000 }";
+
+ // Convert the JSON string to configuration elements.
+ ConstElementPtr config;
+ ASSERT_NO_THROW(config = parseDHCP4(config_str, true));
+ extractConfig(config_str);
+
+ // Pass the configuration in for parsing.
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*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("192.168.2.2", d2_client_config->getSenderIp().toText());
+ EXPECT_EQ(778, d2_client_config->getSenderPort());
+ EXPECT_EQ(2048, d2_client_config->getMaxQueueSize());
+ EXPECT_EQ(dhcp_ddns::NCR_UDP, d2_client_config->getNcrProtocol());
+ EXPECT_EQ(dhcp_ddns::FMT_JSON, d2_client_config->getNcrFormat());
+
+ // Verify all global values won.
+ checkGlobal("ddns-send-updates", false);
+ checkGlobal("ddns-override-no-update", true);
+ checkGlobal("ddns-override-client-update", true);
+ checkGlobal("ddns-replace-client-name", "always");
+ checkGlobal("ddns-generated-prefix", "global.prefix");
+ checkGlobal("ddns-qualifying-suffix", "global.suffix.");
+ checkGlobal("hostname-char-set", "[^A-Z]");
+ checkGlobal("hostname-char-replacement", "x");
+}
+
+// This test checks the ability of the server to handle a configuration
+// containing an invalid dhcp-ddns (D2ClientConfig) entry.
+TEST_F(Dhcp4ParserTest, invalidD2ClientConfig) {
+ ConstElementPtr status;
+
+ // Configuration string with an invalid D2 client config,
+ // "server-ip" is invalid.
+ string config_str = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ " \"dhcp-ddns\" : {"
+ " \"enable-updates\" : true, "
+ " \"server-ip\" : \"bogus-value\", "
+ " \"server-port\" : 5301, "
+ " \"ncr-protocol\" : \"UDP\", "
+ " \"ncr-format\" : \"JSON\", "
+ " \"override-no-update\" : true, "
+ " \"override-client-update\" : true, "
+ " \"replace-client-name\" : \"when-present\", "
+ " \"generated-prefix\" : \"test.prefix\", "
+ " \"qualifying-suffix\" : \"test.suffix.\" },"
+ "\"valid-lifetime\": 4000 }";
+
+ // Convert the JSON string to configuration elements.
+ ConstElementPtr config;
+ ASSERT_NO_THROW(config = parseDHCP4(config_str));
+
+ // Configuration should not throw, but should fail.
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, config));
+
+ // check if returned status is failed.
+ checkResult(status, 1);
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+
+ // Verify that the D2 configuration 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());
+}
+
+// This test checks if it is possible to specify relay information
+TEST_F(Dhcp4ParserTest, subnetRelayInfo) {
+
+ ConstElementPtr status;
+
+ // A config with relay information.
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"renew-timer\": 1, "
+ " \"rebind-timer\": 2, "
+ " \"valid-lifetime\": 4,"
+ " \"relay\": { "
+ " \"ip-address\": \"192.0.2.123\""
+ " },"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // returned value should be 0 (configuration success)
+ checkResult(status, 0);
+
+ Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
+ getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.200"));
+ ASSERT_TRUE(subnet);
+
+ EXPECT_TRUE(subnet->hasRelays());
+ EXPECT_TRUE(subnet->hasRelayAddress(IOAddress("192.0.2.123")));
+}
+
+// This test checks if it is possible to specify a list of relays
+TEST_F(Dhcp4ParserTest, subnetRelayInfoList) {
+
+ ConstElementPtr status;
+
+ // A config with relay information.
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"renew-timer\": 1, "
+ " \"rebind-timer\": 2, "
+ " \"valid-lifetime\": 4,"
+ " \"relay\": { "
+ " \"ip-addresses\": [ \"192.0.3.123\", \"192.0.3.124\" ]"
+ " },"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // returned value should be 0 (configuration success)
+ checkResult(status, 0);
+
+ SubnetSelector selector;
+ selector.giaddr_ = IOAddress("192.0.2.200");
+
+ Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
+ getCfgSubnets4()->selectSubnet(selector);
+ ASSERT_TRUE(subnet);
+
+ EXPECT_TRUE(subnet->hasRelays());
+ EXPECT_TRUE(subnet->hasRelayAddress(IOAddress("192.0.3.123")));
+ EXPECT_TRUE(subnet->hasRelayAddress(IOAddress("192.0.3.124")));
+}
+
+// Goal of this test is to verify that multiple subnets can be configured
+// with defined client classes.
+TEST_F(Dhcp4ParserTest, classifySubnets) {
+ ConstElementPtr x;
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"client-class\": \"alpha\" "
+ " },"
+ " {"
+ " \"id\": 2,"
+ " \"pools\": [ { \"pool\": \"192.0.3.101 - 192.0.3.150\" } ],"
+ " \"subnet\": \"192.0.3.0/24\", "
+ " \"client-class\": \"beta\" "
+ " },"
+ " {"
+ " \"id\": 3,"
+ " \"pools\": [ { \"pool\": \"192.0.4.101 - 192.0.4.150\" } ],"
+ " \"subnet\": \"192.0.4.0/24\", "
+ " \"client-class\": \"gamma\" "
+ " },"
+ " {"
+ " \"id\": 4,"
+ " \"pools\": [ { \"pool\": \"192.0.5.101 - 192.0.5.150\" } ],"
+ " \"subnet\": \"192.0.5.0/24\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 0);
+
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->getAll();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(4, subnets->size()); // We expect 4 subnets
+
+ // Let's check if client belonging to alpha class is supported in subnet[0]
+ // and not supported in any other subnet (except subnet[3], which allows
+ // everyone).
+ ClientClasses classes;
+ classes.insert("alpha");
+ auto subnet0 = subnets->begin();
+ auto subnet1 = std::next(subnet0);
+ auto subnet2 = std::next(subnet1);
+ auto subnet3 = std::next(subnet2);
+ EXPECT_TRUE ((*subnet0)->clientSupported(classes));
+ EXPECT_FALSE((*subnet1)->clientSupported(classes));
+ EXPECT_FALSE((*subnet2)->clientSupported(classes));
+ EXPECT_TRUE ((*subnet3)->clientSupported(classes));
+
+ // Let's check if client belonging to beta class is supported in subnet[1]
+ // and not supported in any other subnet (except subnet[3], which allows
+ // everyone).
+ classes.clear();
+ classes.insert("beta");
+ EXPECT_FALSE((*subnet0)->clientSupported(classes));
+ EXPECT_TRUE ((*subnet1)->clientSupported(classes));
+ EXPECT_FALSE((*subnet2)->clientSupported(classes));
+ EXPECT_TRUE ((*subnet3)->clientSupported(classes));
+
+ // Let's check if client belonging to gamma class is supported in subnet[2]
+ // and not supported in any other subnet (except subnet[3], which allows
+ // everyone).
+ classes.clear();
+ classes.insert("gamma");
+ EXPECT_FALSE((*subnet0)->clientSupported(classes));
+ EXPECT_FALSE((*subnet1)->clientSupported(classes));
+ EXPECT_TRUE ((*subnet2)->clientSupported(classes));
+ EXPECT_TRUE ((*subnet3)->clientSupported(classes));
+
+ // Let's check if client belonging to some other class (not mentioned in
+ // the config) is supported only in subnet[3], which allows everyone.
+ classes.clear();
+ classes.insert("delta");
+ EXPECT_FALSE((*subnet0)->clientSupported(classes));
+ EXPECT_FALSE((*subnet1)->clientSupported(classes));
+ EXPECT_FALSE((*subnet2)->clientSupported(classes));
+ EXPECT_TRUE ((*subnet3)->clientSupported(classes));
+
+ // Finally, let's check class-less client. He should be allowed only in
+ // the last subnet, which does not have any class restrictions.
+ classes.clear();
+ EXPECT_FALSE((*subnet0)->clientSupported(classes));
+ EXPECT_FALSE((*subnet1)->clientSupported(classes));
+ EXPECT_FALSE((*subnet2)->clientSupported(classes));
+ EXPECT_TRUE ((*subnet3)->clientSupported(classes));
+}
+
+// Goal of this test is to verify that multiple pools can be configured
+// with defined client classes.
+TEST_F(Dhcp4ParserTest, classifyPools) {
+ ConstElementPtr x;
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { "
+ " \"pool\": \"192.0.2.1 - 192.0.2.100\", "
+ " \"client-class\": \"alpha\" "
+ " },"
+ " {"
+ " \"pool\": \"192.0.3.101 - 192.0.3.150\", "
+ " \"client-class\": \"beta\" "
+ " },"
+ " {"
+ " \"pool\": \"192.0.4.101 - 192.0.4.150\", "
+ " \"client-class\": \"gamma\" "
+ " },"
+ " {"
+ " \"pool\": \"192.0.5.101 - 192.0.5.150\" "
+ " } ],"
+ " \"subnet\": \"192.0.0.0/16\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config, true));
+ extractConfig(config);
+
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 0);
+
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->getAll();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(1, subnets->size());
+ const PoolCollection& pools = (*subnets->begin())->getPools(Lease::TYPE_V4);
+ ASSERT_EQ(4, pools.size()); // We expect 4 pools
+
+ // Let's check if client belonging to alpha class is supported in pool[0]
+ // and not supported in any other pool (except pool[3], which allows
+ // everyone).
+ ClientClasses classes;
+ classes.insert("alpha");
+ EXPECT_TRUE(pools.at(0)->clientSupported(classes));
+ EXPECT_FALSE(pools.at(1)->clientSupported(classes));
+ EXPECT_FALSE(pools.at(2)->clientSupported(classes));
+ EXPECT_TRUE(pools.at(3)->clientSupported(classes));
+
+ // Let's check if client belonging to beta class is supported in pool[1]
+ // and not supported in any other pool (except pools[3], which allows
+ // everyone).
+ classes.clear();
+ classes.insert("beta");
+ EXPECT_FALSE(pools.at(0)->clientSupported(classes));
+ EXPECT_TRUE(pools.at(1)->clientSupported(classes));
+ EXPECT_FALSE(pools.at(2)->clientSupported(classes));
+ EXPECT_TRUE(pools.at(3)->clientSupported(classes));
+
+ // Let's check if client belonging to gamma class is supported in pool[2]
+ // and not supported in any other pool (except pool[3], which allows
+ // everyone).
+ classes.clear();
+ classes.insert("gamma");
+ EXPECT_FALSE(pools.at(0)->clientSupported(classes));
+ EXPECT_FALSE(pools.at(1)->clientSupported(classes));
+ EXPECT_TRUE(pools.at(2)->clientSupported(classes));
+ EXPECT_TRUE(pools.at(3)->clientSupported(classes));
+
+ // Let's check if client belonging to some other class (not mentioned in
+ // the config) is supported only in pool[3], which allows everyone.
+ classes.clear();
+ classes.insert("delta");
+ EXPECT_FALSE(pools.at(0)->clientSupported(classes));
+ EXPECT_FALSE(pools.at(1)->clientSupported(classes));
+ EXPECT_FALSE(pools.at(2)->clientSupported(classes));
+ EXPECT_TRUE(pools.at(3)->clientSupported(classes));
+
+ // Finally, let's check class-less client. He should be allowed only in
+ // the last pool, which does not have any class restrictions.
+ classes.clear();
+ EXPECT_FALSE(pools.at(0)->clientSupported(classes));
+ EXPECT_FALSE(pools.at(1)->clientSupported(classes));
+ EXPECT_FALSE(pools.at(2)->clientSupported(classes));
+ EXPECT_TRUE(pools.at(3)->clientSupported(classes));
+}
+
+// This test verifies that the host reservations can be specified for
+// respective IPv4 subnets.
+TEST_F(Dhcp4ParserTest, reservations) {
+ ConstElementPtr x;
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ "
+ " { "
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"id\": 123,"
+ " \"reservations\": ["
+ " ]"
+ " },"
+ " {"
+ " \"reservations\": ["
+ " {"
+ " \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+ " \"ip-address\": \"192.0.3.112\","
+ " \"hostname\": \"\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"name-servers\","
+ " \"data\": \"192.0.3.15\""
+ " },"
+ " {"
+ " \"name\": \"default-ip-ttl\","
+ " \"data\": \"32\""
+ " }"
+ " ]"
+ " },"
+ " {"
+ " \"hw-address\": \"01:02:03:04:05:06\","
+ " \"ip-address\": \"192.0.3.120\","
+ " \"hostname\": \"\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"name-servers\","
+ " \"data\": \"192.0.3.95\""
+ " },"
+ " {"
+ " \"name\": \"default-ip-ttl\","
+ " \"data\": \"11\""
+ " }"
+ " ]"
+ " }"
+ " ],"
+ " \"pools\": [ { \"pool\": \"192.0.3.101 - 192.0.3.150\" } ],"
+ " \"subnet\": \"192.0.3.0/24\", "
+ " \"id\": 234"
+ " },"
+ " {"
+ " \"pools\": [ { \"pool\": \"192.0.4.101 - 192.0.4.150\" } ],"
+ " \"subnet\": \"192.0.4.0/24\","
+ " \"id\": 542,"
+ " \"reservations\": ["
+ " {"
+ " \"duid\": \"0A:09:08:07:06:05:04:03:02:01\","
+ " \"ip-address\": \"192.0.4.101\","
+ " \"hostname\": \"\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"name-servers\","
+ " \"data\": \"192.0.4.11\""
+ " },"
+ " {"
+ " \"name\": \"default-ip-ttl\","
+ " \"data\": \"95\""
+ " }"
+ " ]"
+ " },"
+ " {"
+ " \"circuit-id\": \"060504030201\","
+ " \"ip-address\": \"192.0.4.102\","
+ " \"hostname\": \"\""
+ " },"
+ " {"
+ " \"client-id\": \"05:01:02:03:04:05:06\","
+ " \"ip-address\": \"192.0.4.103\","
+ " \"hostname\": \"\""
+ " }"
+ " ]"
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 0);
+
+ // Make sure all subnets have been successfully configured. There is no
+ // need to sanity check the subnet properties because it should have
+ // been already tested by other tests.
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->getAll();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(3, subnets->size());
+
+ // Hosts configuration must be available.
+ CfgHostsPtr hosts_cfg = CfgMgr::instance().getStagingCfg()->getCfgHosts();
+ ASSERT_TRUE(hosts_cfg);
+
+ // Let's create an object holding hardware address of the host having
+ // a reservation in the subnet having id of 234. For simplicity the
+ // address is a collection of numbers from 1 to 6.
+ std::vector<uint8_t> hwaddr;
+ for (unsigned int i = 1; i < 7; ++i) {
+ hwaddr.push_back(static_cast<uint8_t>(i));
+ }
+ // Retrieve the reservation and sanity check the address reserved.
+ ConstHostPtr host = hosts_cfg->get4(234, Host::IDENT_HWADDR,
+ &hwaddr[0], hwaddr.size());
+ ASSERT_TRUE(host);
+ EXPECT_EQ("192.0.3.120", host->getIPv4Reservation().toText());
+ // This reservation should be solely assigned to the subnet 234,
+ // and not to other two.
+ EXPECT_FALSE(hosts_cfg->get4(123, Host::IDENT_HWADDR,
+ &hwaddr[0], hwaddr.size()));
+ EXPECT_FALSE(hosts_cfg->get4(542, Host::IDENT_HWADDR,
+ &hwaddr[0], hwaddr.size()));
+ // Check that options are assigned correctly.
+ Option4AddrLstPtr opt_dns =
+ retrieveOption<Option4AddrLstPtr>(*host, DHO_NAME_SERVERS);
+ ASSERT_TRUE(opt_dns);
+ Option4AddrLst::AddressContainer dns_addrs = opt_dns->getAddresses();
+ ASSERT_EQ(1, dns_addrs.size());
+ EXPECT_EQ("192.0.3.95", dns_addrs[0].toText());
+ OptionUint8Ptr opt_ttl =
+ retrieveOption<OptionUint8Ptr>(*host, DHO_DEFAULT_IP_TTL);
+ ASSERT_TRUE(opt_ttl);
+ EXPECT_EQ(11, static_cast<int>(opt_ttl->getValue()));
+
+ // Do the same test for the DUID based reservation.
+ std::vector<uint8_t> duid;
+ for (unsigned int i = 1; i < 0xb; ++i) {
+ duid.push_back(static_cast<uint8_t>(i));
+ }
+ host = hosts_cfg->get4(234, Host::IDENT_DUID, &duid[0], duid.size());
+ ASSERT_TRUE(host);
+ EXPECT_EQ("192.0.3.112", host->getIPv4Reservation().toText());
+ EXPECT_FALSE(hosts_cfg->get4(123, Host::IDENT_DUID, &duid[0], duid.size()));
+ EXPECT_FALSE(hosts_cfg->get4(542, Host::IDENT_DUID, &duid[0], duid.size()));
+ // Check that options are assigned correctly.
+ opt_dns = retrieveOption<Option4AddrLstPtr>(*host, DHO_NAME_SERVERS);
+ ASSERT_TRUE(opt_dns);
+ dns_addrs = opt_dns->getAddresses();
+ ASSERT_EQ(1, dns_addrs.size());
+ EXPECT_EQ("192.0.3.15", dns_addrs[0].toText());
+ opt_ttl = retrieveOption<OptionUint8Ptr>(*host, DHO_DEFAULT_IP_TTL);
+ ASSERT_TRUE(opt_ttl);
+ EXPECT_EQ(32, static_cast<int>(opt_ttl->getValue()));
+
+ // The circuit-id used for one of the reservations in the subnet 542
+ // consists of numbers from 6 to 1. So, let's just reverse the order
+ // of the address from the previous test.
+ std::vector<uint8_t> circuit_id(hwaddr.rbegin(), hwaddr.rend());
+ host = hosts_cfg->get4(542, Host::IDENT_CIRCUIT_ID, &circuit_id[0],
+ circuit_id.size());
+ EXPECT_TRUE(host);
+ EXPECT_EQ("192.0.4.102", host->getIPv4Reservation().toText());
+ // This reservation must not belong to other subnets.
+ EXPECT_FALSE(hosts_cfg->get4(123, Host::IDENT_CIRCUIT_ID,
+ &circuit_id[0], circuit_id.size()));
+ EXPECT_FALSE(hosts_cfg->get4(234, Host::IDENT_CIRCUIT_ID,
+ &circuit_id[0], circuit_id.size()));
+
+ // Repeat the test for the DUID based reservation in this subnet.
+ std::vector<uint8_t> duid_r(duid.rbegin(), duid.rend());
+ host = hosts_cfg->get4(542, Host::IDENT_DUID, &duid_r[0], duid_r.size());
+ ASSERT_TRUE(host);
+ EXPECT_EQ("192.0.4.101", host->getIPv4Reservation().toText());
+ EXPECT_FALSE(hosts_cfg->get4(123, Host::IDENT_DUID,
+ &duid_r[0], duid_r.size()));
+ EXPECT_FALSE(hosts_cfg->get4(234, Host::IDENT_DUID,
+ &duid_r[0], duid_r.size()));
+ // Check that options are assigned correctly.
+ opt_dns = retrieveOption<Option4AddrLstPtr>(*host, DHO_NAME_SERVERS);
+ ASSERT_TRUE(opt_dns);
+ dns_addrs = opt_dns->getAddresses();
+ ASSERT_EQ(1, dns_addrs.size());
+ EXPECT_EQ("192.0.4.11", dns_addrs[0].toText());
+ opt_ttl = retrieveOption<OptionUint8Ptr>(*host, DHO_DEFAULT_IP_TTL);
+ ASSERT_TRUE(opt_ttl);
+ EXPECT_EQ(95, static_cast<int>(opt_ttl->getValue()));
+
+ // Check that we can find reservation using client identifier.
+ ClientIdPtr client_id = ClientId::fromText("05:01:02:03:04:05:06");
+ host = hosts_cfg->get4(542, Host::IDENT_CLIENT_ID, &client_id->getClientId()[0],
+ client_id->getClientId().size());
+ ASSERT_TRUE(host);
+ EXPECT_EQ("192.0.4.103", host->getIPv4Reservation().toText());
+
+ // But this reservation should not be returned for other subnet.
+ host = hosts_cfg->get4(234, Host::IDENT_CLIENT_ID, &client_id->getClientId()[0],
+ client_id->getClientId().size());
+ EXPECT_FALSE(host);
+}
+
+// This test checks that it is possible to configure option data for a
+// host using a user defined option format.
+TEST_F(Dhcp4ParserTest, reservationWithOptionDefinition) {
+ ConstElementPtr x;
+ // The following configuration contains host declaration in which
+ // a non-standard option is used. This option has option definition
+ // specified in the configuration.
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"uint32\","
+ " \"space\": \"isc\""
+ "} ],"
+ "\"subnet4\": [ "
+ " {"
+ " \"reservations\": ["
+ " {"
+ " \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+ " \"ip-address\": \"192.0.3.112\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"foo\","
+ " \"data\": \"123\","
+ " \"space\": \"isc\""
+ " }"
+ " ]"
+ " }"
+ " ],"
+ " \"pools\": [ { \"pool\": \"192.0.3.101 - 192.0.3.150\" } ],"
+ " \"subnet\": \"192.0.3.0/24\", "
+ " \"id\": 234"
+ " } ],"
+ "\"valid-lifetime\": 4000"
+ "}";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config, true));
+ extractConfig(config);
+
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 0);
+
+ // Hosts configuration must be available.
+ CfgHostsPtr hosts_cfg = CfgMgr::instance().getStagingCfg()->getCfgHosts();
+ ASSERT_TRUE(hosts_cfg);
+
+ // Let's create an object holding DUID of the host. For simplicity the
+ // address is a collection of numbers from 1 to A.
+ std::vector<uint8_t> duid;
+ for (unsigned int i = 1; i < 0xB; ++i) {
+ duid.push_back(static_cast<uint8_t>(i));
+ }
+ // Retrieve the reservation and sanity check the address reserved.
+ ConstHostPtr host =
+ hosts_cfg->get4(234, Host::IDENT_DUID, &duid[0], duid.size());
+ ASSERT_TRUE(host);
+ EXPECT_EQ("192.0.3.112", host->getIPv4Reservation().toText());
+
+ // Check if the option has been parsed.
+ OptionUint32Ptr opt_foo = retrieveOption<OptionUint32Ptr>(*host, "isc",
+ 100);
+ ASSERT_TRUE(opt_foo);
+ EXPECT_EQ(100, opt_foo->getType());
+ EXPECT_EQ(123, opt_foo->getValue());
+}
+
+// This test verifies that the bogus host reservation would trigger a
+// server configuration error.
+TEST_F(Dhcp4ParserTest, reservationBogus) {
+ // Case 1: misspelled hw-address parameter.
+ ConstElementPtr x;
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ "
+ " { "
+ " \"pools\": [ { \"pool\": \"192.0.4.101 - 192.0.4.150\" } ],"
+ " \"subnet\": \"192.0.4.0/24\","
+ " \"id\": 542,"
+ " \"reservations\": ["
+ " {"
+ " \"hw-addre\": \"06:05:04:03:02:01\","
+ " \"ip-address\": \"192.0.4.102\","
+ " \"hostname\": \"\""
+ " }"
+ " ]"
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseJSON(config));
+
+ CfgMgr::instance().clear();
+
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 1);
+
+ EXPECT_THROW(parseDHCP4(config), Dhcp4ParseError);
+
+ // Case 2: DUID and HW Address both specified.
+ config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ "
+ " { "
+ " \"pools\": [ { \"pool\": \"192.0.4.101 - 192.0.4.150\" } ],"
+ " \"subnet\": \"192.0.4.0/24\","
+ " \"id\": 542,"
+ " \"reservations\": ["
+ " {"
+ " \"duid\": \"01:02:03:04:05:06\","
+ " \"hw-address\": \"06:05:04:03:02:01\","
+ " \"ip-address\": \"192.0.4.102\","
+ " \"hostname\": \"\""
+ " }"
+ " ]"
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+
+ // Remove existing configuration, if any.
+ CfgMgr::instance().clear();
+
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 1);
+
+ // Case 3: Broken specification of option data.
+ config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ "
+ " { "
+ " \"pools\": [ { \"pool\": \"192.0.4.101 - 192.0.4.150\" } ],"
+ " \"subnet\": \"192.0.4.0/24\","
+ " \"id\": 542,"
+ " \"reservations\": ["
+ " {"
+ " \"hw-address\": \"06:05:04:03:02:01\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"name-servers\","
+ " \"data\": \"bogus-ip-address\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+
+ // Remove existing configuration, if any.
+ CfgMgr::instance().clear();
+
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 1);
+}
+
+/// The goal of this test is to verify that Host Reservation flags can be
+/// specified on a per-subnet basis.
+TEST_F(Dhcp4ParserTest, hostReservationPerSubnet) {
+
+ /// - Configuration:
+ /// - only addresses (no prefixes)
+ /// - 7 subnets with:
+ /// - 192.0.1.0/24 (all reservations enabled)
+ /// - 192.0.2.0/24 (out-of-pool reservations)
+ /// - 192.0.3.0/24 (reservations disabled)
+ /// - 192.0.4.0/24 (global reservations)
+ /// - 192.0.5.0/24 (reservations not specified)
+ /// - 192.0.6.0/24 (global + all enabled)
+ /// - 192.0.7.0/24 (global + out-of-pool enabled)
+ const char* hr_config =
+ "{ "
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.1.0/24\" } ],"
+ " \"subnet\": \"192.0.1.0/24\", "
+ " \"reservations-global\": false,"
+ " \"reservations-in-subnet\": true,"
+ " \"reservations-out-of-pool\": false"
+ " },"
+ " {"
+ " \"id\": 2,"
+ " \"pools\": [ { \"pool\": \"192.0.2.0/24\" } ],"
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"reservations-global\": false,"
+ " \"reservations-in-subnet\": true,"
+ " \"reservations-out-of-pool\": true"
+ " },"
+ " {"
+ " \"id\": 3,"
+ " \"pools\": [ { \"pool\": \"192.0.3.0/24\" } ],"
+ " \"subnet\": \"192.0.3.0/24\", "
+ " \"reservations-global\": false,"
+ " \"reservations-in-subnet\": false"
+ " },"
+ " {"
+ " \"id\": 4,"
+ " \"pools\": [ { \"pool\": \"192.0.4.0/24\" } ],"
+ " \"subnet\": \"192.0.4.0/24\", "
+ " \"reservations-global\": true,"
+ " \"reservations-in-subnet\": false"
+ " },"
+ " {"
+ " \"id\": 5,"
+ " \"pools\": [ { \"pool\": \"192.0.5.0/24\" } ],"
+ " \"subnet\": \"192.0.5.0/24\""
+ " },"
+ " {"
+ " \"id\": 6,"
+ " \"pools\": [ { \"pool\": \"192.0.6.0/24\" } ],"
+ " \"subnet\": \"192.0.6.0/24\", "
+ " \"reservations-global\": true,"
+ " \"reservations-in-subnet\": true,"
+ " \"reservations-out-of-pool\": false"
+ " },"
+ " {"
+ " \"id\": 7,"
+ " \"pools\": [ { \"pool\": \"192.0.7.0/24\" } ],"
+ " \"subnet\": \"192.0.7.0/24\", "
+ " \"reservations-global\": true,"
+ " \"reservations-in-subnet\": true,"
+ " \"reservations-out-of-pool\": true"
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(hr_config));
+ extractConfig(hr_config);
+ ConstElementPtr result;
+ EXPECT_NO_THROW(result = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // returned value should be 0 (success)
+ checkResult(result, 0);
+
+ // Let's get all subnets and check that there are 7 of them.
+ ConstCfgSubnets4Ptr subnets = CfgMgr::instance().getStagingCfg()->getCfgSubnets4();
+ ASSERT_TRUE(subnets);
+ const Subnet4Collection* subnet_col = subnets->getAll();
+ ASSERT_EQ(7, subnet_col->size()); // We expect 7 subnets
+
+ // Let's check if the parsed subnets have correct HR modes.
+
+ // Subnet 1
+ Subnet4Ptr subnet;
+ subnet = subnets->selectSubnet(IOAddress("192.0.1.1"));
+ ASSERT_TRUE(subnet);
+ EXPECT_FALSE(subnet->getReservationsGlobal());
+ EXPECT_TRUE(subnet->getReservationsInSubnet());
+ EXPECT_FALSE(subnet->getReservationsOutOfPool());
+
+ // Subnet 2
+ subnet = subnets->selectSubnet(IOAddress("192.0.2.1"));
+ ASSERT_TRUE(subnet);
+ EXPECT_FALSE(subnet->getReservationsGlobal());
+ EXPECT_TRUE(subnet->getReservationsInSubnet());
+ EXPECT_TRUE(subnet->getReservationsOutOfPool());
+
+ // Subnet 3
+ subnet = subnets->selectSubnet(IOAddress("192.0.3.1"));
+ ASSERT_TRUE(subnet);
+ EXPECT_FALSE(subnet->getReservationsGlobal());
+ EXPECT_FALSE(subnet->getReservationsInSubnet());
+ EXPECT_FALSE(subnet->getReservationsOutOfPool());
+
+ // Subnet 4
+ subnet = subnets->selectSubnet(IOAddress("192.0.4.1"));
+ ASSERT_TRUE(subnet);
+ EXPECT_TRUE(subnet->getReservationsGlobal());
+ EXPECT_FALSE(subnet->getReservationsInSubnet());
+ EXPECT_FALSE(subnet->getReservationsOutOfPool());
+
+ // Subnet 5
+ subnet = subnets->selectSubnet(IOAddress("192.0.5.1"));
+ ASSERT_TRUE(subnet);
+ EXPECT_FALSE(subnet->getReservationsGlobal());
+ EXPECT_TRUE(subnet->getReservationsInSubnet());
+ EXPECT_FALSE(subnet->getReservationsOutOfPool());
+
+ // Subnet 6
+ subnet = subnets->selectSubnet(IOAddress("192.0.6.1"));
+ ASSERT_TRUE(subnet);
+ EXPECT_TRUE(subnet->getReservationsGlobal());
+ EXPECT_TRUE(subnet->getReservationsInSubnet());
+ EXPECT_FALSE(subnet->getReservationsOutOfPool());
+
+ // Subnet 7
+ subnet = subnets->selectSubnet(IOAddress("192.0.7.1"));
+ ASSERT_TRUE(subnet);
+ EXPECT_TRUE(subnet->getReservationsGlobal());
+ EXPECT_TRUE(subnet->getReservationsInSubnet());
+ EXPECT_TRUE(subnet->getReservationsOutOfPool());
+}
+
+/// The goal of this test is to verify that Host Reservation flags can be
+/// specified globally.
+TEST_F(Dhcp4ParserTest, hostReservationGlobal) {
+
+ /// - Configuration:
+ /// - only addresses (no prefixes)
+ /// - 2 subnets with :
+ /// - 192.0.2.0/24 (all reservations enabled)
+ /// - 192.0.3.0/24 (reservations not specified)
+ const char* hr_config =
+ "{ "
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"reservations-global\": false,"
+ "\"reservations-in-subnet\": true,"
+ "\"reservations-out-of-pool\": true,"
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.0/24\" } ],"
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"reservations-global\": false,"
+ " \"reservations-in-subnet\": true,"
+ " \"reservations-out-of-pool\": false"
+ " },"
+ " {"
+ " \"id\": 2,"
+ " \"pools\": [ { \"pool\": \"192.0.3.0/24\" } ],"
+ " \"subnet\": \"192.0.3.0/24\""
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(hr_config));
+ extractConfig(hr_config);
+ ConstElementPtr result;
+ EXPECT_NO_THROW(result = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // returned value should be 0 (success)
+ checkResult(result, 0);
+
+ // Let's get all subnets and check that there are 4 of them.
+ ConstCfgSubnets4Ptr subnets = CfgMgr::instance().getStagingCfg()->getCfgSubnets4();
+ ASSERT_TRUE(subnets);
+ const Subnet4Collection* subnet_col = subnets->getAll();
+ ASSERT_EQ(2, subnet_col->size()); // We expect 2 subnets
+
+ // Let's check if the parsed subnets have correct HR modes.
+
+ // Subnet 1
+ Subnet4Ptr subnet;
+ subnet = subnets->selectSubnet(IOAddress("192.0.2.1"));
+ ASSERT_TRUE(subnet);
+ // Reset the fetch global function to staging (vs current) config.
+ subnet->setFetchGlobalsFn([]() -> ConstCfgGlobalsPtr {
+ return (CfgMgr::instance().getStagingCfg()->getConfiguredGlobals());
+ });
+ EXPECT_FALSE(subnet->getReservationsGlobal());
+ EXPECT_TRUE(subnet->getReservationsInSubnet());
+ EXPECT_FALSE(subnet->getReservationsOutOfPool());
+
+ // Subnet 2
+ subnet = subnets->selectSubnet(IOAddress("192.0.3.1"));
+ ASSERT_TRUE(subnet);
+ // Reset the fetch global function to staging (vs current) config.
+ subnet->setFetchGlobalsFn([]() -> ConstCfgGlobalsPtr {
+ return (CfgMgr::instance().getStagingCfg()->getConfiguredGlobals());
+ });
+ EXPECT_FALSE(subnet->getReservationsGlobal());
+ EXPECT_TRUE(subnet->getReservationsInSubnet());
+ EXPECT_TRUE(subnet->getReservationsOutOfPool());
+}
+
+/// Check that the decline-probation-period has a default value when not
+/// specified.
+TEST_F(Dhcp4ParserTest, declineTimerDefault) {
+ ConstElementPtr status;
+
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"subnet4\": [ ]"
+ "}";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // returned value should be 0 (success)
+ checkResult(status, 0);
+
+ // The value of decline-probation-period must be equal to the
+ // default value (86400). The default value is defined in GLOBAL4_DEFAULTS in
+ // simple_parser4.cc.
+ EXPECT_EQ(86400, CfgMgr::instance().getStagingCfg()->getDeclinePeriod());
+}
+
+/// Check that the dhcp4o6-port default value has a default value if not
+/// specified explicitly.
+TEST_F(Dhcp4ParserTest, dhcp4o6portDefault) {
+
+ string config_txt = "{ " + genIfaceConfig() + ","
+ "\"subnet4\": [ ] "
+ "}";
+ ConstElementPtr config;
+ ASSERT_NO_THROW(config = parseDHCP4(config_txt));
+ extractConfig(config_txt);
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, config));
+
+ // returned value should be 0 (success)
+ checkResult(status, 0);
+
+ // The value of decline-probation-period must be equal to the
+ // default value (0). The default value is defined in GLOBAL4_DEFAULTS in
+ // simple_parser4.cc.
+ EXPECT_EQ(0, CfgMgr::instance().getStagingCfg()->getDhcp4o6Port());
+}
+
+/// Check that the decline-probation-period value can be set properly.
+TEST_F(Dhcp4ParserTest, declineTimer) {
+ ConstElementPtr status;
+
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"decline-probation-period\": 12345,"
+ "\"subnet4\": [ ]"
+ "}";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // returned value should be 0 (success)
+ checkResult(status, 0);
+
+ // The value of decline-probation-period must be equal to the
+ // value specified.
+ EXPECT_EQ(12345,
+ CfgMgr::instance().getStagingCfg()->getDeclinePeriod());
+}
+
+/// Check that an incorrect decline-probation-period value will be caught.
+TEST_F(Dhcp4ParserTest, declineTimerError) {
+ ConstElementPtr status;
+
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"decline-probation-period\": \"soon\","
+ "\"subnet4\": [ ]"
+ "}";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseJSON(config));
+
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // returned value should be 1 (error)
+ checkResult(status, 1);
+
+ // Check that the error contains error position.
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+
+ // Check that the Dhcp4 parser catches the type error
+ EXPECT_THROW(parseDHCP4(config), Dhcp4ParseError);
+}
+
+// Check that configuration for the expired leases processing may be
+// specified.
+TEST_F(Dhcp4ParserTest, expiredLeasesProcessing) {
+ // Create basic configuration with the expiration specific parameters.
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"expired-leases-processing\": "
+ "{"
+ " \"reclaim-timer-wait-time\": 20,"
+ " \"flush-reclaimed-timer-wait-time\": 35,"
+ " \"hold-reclaimed-time\": 1800,"
+ " \"max-reclaim-leases\": 50,"
+ " \"max-reclaim-time\": 100,"
+ " \"unwarned-reclaim-cycles\": 10"
+ "},"
+ "\"subnet4\": [ ]"
+ "}";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // Returned value should be 0 (success)
+ checkResult(status, 0);
+
+ // The value of decline-probation-period must be equal to the
+ // value specified.
+ CfgExpirationPtr cfg = CfgMgr::instance().getStagingCfg()->getCfgExpiration();
+ ASSERT_TRUE(cfg);
+
+ // Verify that parameters are correct.
+ EXPECT_EQ(20, cfg->getReclaimTimerWaitTime());
+ EXPECT_EQ(35, cfg->getFlushReclaimedTimerWaitTime());
+ EXPECT_EQ(1800, cfg->getHoldReclaimedTime());
+ EXPECT_EQ(50, cfg->getMaxReclaimLeases());
+ EXPECT_EQ(100, cfg->getMaxReclaimTime());
+ EXPECT_EQ(10, cfg->getUnwarnedReclaimCycles());
+}
+
+// Check that invalid configuration for the expired leases processing is
+// causing an error.
+TEST_F(Dhcp4ParserTest, expiredLeasesProcessingError) {
+ // Create basic configuration with the expiration specific parameters.
+ // One of the parameters holds invalid value.
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"expired-leases-processing\": "
+ "{"
+ " \"reclaim-timer-wait-time\": -5,"
+ " \"flush-reclaimed-timer-wait-time\": 35,"
+ " \"hold-reclaimed-time\": 1800,"
+ " \"max-reclaim-leases\": 50,"
+ " \"max-reclaim-time\": 100,"
+ " \"unwarned-reclaim-cycles\": 10"
+ "},"
+ "\"subnet4\": [ ]"
+ "}";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // Returned value should be 0 (error)
+ checkResult(status, 1);
+
+ // Check that the error contains error position.
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+}
+
+// Checks if the DHCPv4 is able to parse the configuration without 4o6 parameters
+// and does not set 4o6 fields at all.
+TEST_F(Dhcp4ParserTest, 4o6default) {
+
+ ConstElementPtr status;
+
+ // Just a plain v4 config (no 4o6 parameters)
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // check if returned status is OK
+ checkResult(status, 0);
+
+ // Now check if the configuration was indeed handled and we have
+ // expected pool configured.
+ Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
+ getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.200"));
+ ASSERT_TRUE(subnet);
+
+ Cfg4o6& dhcp4o6 = subnet->get4o6();
+ EXPECT_FALSE(dhcp4o6.enabled());
+}
+
+// Checks if the DHCPv4 is able to parse the configuration with 4o6 subnet
+// defined.
+TEST_F(Dhcp4ParserTest, 4o6subnet) {
+
+ ConstElementPtr status;
+
+ // Just a plain v4 config (no 4o6 parameters)
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\","
+ " \"4o6-subnet\": \"2001:db8::123/45\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // check if returned status is OK
+ checkResult(status, 0);
+
+ // Now check if the configuration was indeed handled and we have
+ // expected pool configured.
+ Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
+ getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.200"));
+ ASSERT_TRUE(subnet);
+
+ Cfg4o6& dhcp4o6 = subnet->get4o6();
+ EXPECT_TRUE(dhcp4o6.enabled());
+ EXPECT_EQ(IOAddress("2001:db8::123"), dhcp4o6.getSubnet4o6().get().first);
+ EXPECT_EQ(45, dhcp4o6.getSubnet4o6().get().second);
+}
+
+// Checks if the DHCPv4 is able to parse the configuration with 4o6 subnet
+// defined.
+TEST_F(Dhcp4ParserTest, 4o6subnetBogus) {
+
+ ConstElementPtr status;
+
+ // Just a plain v4 config (no 4o6 parameters)
+ string config[] = {
+ // Bogus configuration 1: missing / in subnet
+ "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\","
+ " \"4o6-subnet\": \"2001:db8::123\" } ],"
+ "\"valid-lifetime\": 4000 }",
+
+ // Bogus configuration 2: incorrect address
+ "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\","
+ " \"4o6-subnet\": \"2001:db8:bogus/45\" } ],"
+ "\"valid-lifetime\": 4000 }",
+
+ // Bogus configuration 3: incorrect prefix length
+ "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\","
+ " \"4o6-subnet\": \"2001:db8::123/200\" } ],"
+ "\"valid-lifetime\": 4000 }"
+ };
+
+ ConstElementPtr json1;
+ ASSERT_NO_THROW(json1 = parseDHCP4(config[0]));
+ ConstElementPtr json2;
+ ASSERT_NO_THROW(json2 = parseDHCP4(config[0]));
+ ConstElementPtr json3;
+ ASSERT_NO_THROW(json3 = parseDHCP4(config[0]));
+
+ // Check that the first config is rejected.
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json1));
+ checkResult(status, 1);
+
+ // Check that the second config is rejected.
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json2));
+ checkResult(status, 1);
+
+ // Check that the third config is rejected.
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json3));
+ checkResult(status, 1);
+}
+
+// Checks if the DHCPv4 is able to parse the configuration with 4o6 network
+// interface defined.
+TEST_F(Dhcp4ParserTest, 4o6iface) {
+
+ ConstElementPtr status;
+
+ // Just a plain v4 config (no 4o6 parameters)
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\","
+ " \"4o6-interface\": \"ethX\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // check if returned status is OK
+ checkResult(status, 0);
+
+ // Now check if the configuration was indeed handled and we have
+ // expected pool configured.
+ Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
+ getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.200"));
+ ASSERT_TRUE(subnet);
+
+ Cfg4o6& dhcp4o6 = subnet->get4o6();
+ EXPECT_TRUE(dhcp4o6.enabled());
+ EXPECT_EQ("ethX", dhcp4o6.getIface4o6().get());
+}
+
+// Checks if the DHCPv4 is able to parse the configuration with both 4o6 network
+// interface and v6 subnet defined.
+TEST_F(Dhcp4ParserTest, 4o6subnetIface) {
+
+ ConstElementPtr status;
+
+ // Just a plain v4 config (no 4o6 parameters)
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\","
+ " \"4o6-subnet\": \"2001:db8::543/21\","
+ " \"4o6-interface\": \"ethX\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // check if returned status is OK
+ checkResult(status, 0);
+
+ // Now check if the configuration was indeed handled and we have
+ // expected subnet configured...
+ Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
+ getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.200"));
+ ASSERT_TRUE(subnet);
+
+ // ... and that subnet has 4o6 network interface specified.
+ Cfg4o6& dhcp4o6 = subnet->get4o6();
+ EXPECT_TRUE(dhcp4o6.enabled());
+ EXPECT_EQ(IOAddress("2001:db8::543"), dhcp4o6.getSubnet4o6().get().first);
+ EXPECT_EQ(21, dhcp4o6.getSubnet4o6().get().second);
+ EXPECT_EQ("ethX", dhcp4o6.getIface4o6().get());
+}
+
+// Checks if the DHCPv4 is able to parse the configuration with 4o6 network
+// interface-id.
+TEST_F(Dhcp4ParserTest, 4o6subnetInterfaceId) {
+
+ ConstElementPtr status;
+
+ // Just a plain v4 config (no 4o6 parameters)
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\","
+ " \"4o6-interface-id\": \"vlan123\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+
+ // check if returned status is OK
+ checkResult(status, 0);
+
+ // Now check if the configuration was indeed handled and we have
+ // expected 4o6-interface-id configured.
+ Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
+ getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.200"));
+ ASSERT_TRUE(subnet);
+
+ Cfg4o6& dhcp4o6 = subnet->get4o6();
+ EXPECT_TRUE(dhcp4o6.enabled());
+ OptionPtr ifaceid = dhcp4o6.getInterfaceId();
+ ASSERT_TRUE(ifaceid);
+
+ vector<uint8_t> data = ifaceid->getData();
+ EXPECT_EQ(7, data.size());
+ const char *exp = "vlan123";
+ EXPECT_EQ(0, memcmp(&data[0], exp, data.size()));
+}
+
+// Verifies that simple list of valid classes parses and
+// is staged for commit.
+TEST_F(Dhcp4ParserTest, validClientClassDictionary) {
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"valid-lifetime\": 4000, \n"
+ "\"rebind-timer\": 2000, \n"
+ "\"renew-timer\": 1000, \n"
+ "\"client-classes\" : [ \n"
+ " { \n"
+ " \"name\": \"one\" \n"
+ " }, \n"
+ " { \n"
+ " \"name\": \"two\" \n"
+ " }, \n"
+ " { \n"
+ " \"name\": \"three\" \n"
+ " } \n"
+ "], \n"
+ "\"subnet4\": [ { \n"
+ " \"id\": 1, \n"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], \n"
+ " \"subnet\": \"192.0.2.0/24\" \n"
+ " } ] \n"
+ "} \n";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ // We check staging config because CfgMgr::commit hasn't been executed.
+ ClientClassDictionaryPtr dictionary;
+ dictionary = CfgMgr::instance().getStagingCfg()->getClientClassDictionary();
+ ASSERT_TRUE(dictionary);
+ EXPECT_EQ(3, dictionary->getClasses()->size());
+
+ // Execute the commit
+ ASSERT_NO_THROW(CfgMgr::instance().commit());
+
+ // Verify that after commit, the current config has the correct dictionary
+ dictionary = CfgMgr::instance().getCurrentCfg()->getClientClassDictionary();
+ ASSERT_TRUE(dictionary);
+ EXPECT_EQ(3, dictionary->getClasses()->size());
+}
+
+// Verifies that a class list containing an invalid
+// class definition causes a configuration error.
+TEST_F(Dhcp4ParserTest, invalidClientClassDictionary) {
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"valid-lifetime\": 4000, \n"
+ "\"rebind-timer\": 2000, \n"
+ "\"renew-timer\": 1000, \n"
+ "\"client-classes\" : [ \n"
+ " { \n"
+ " \"name\": \"one\", \n"
+ " \"bogus\": \"bad\" \n"
+ " } \n"
+ "], \n"
+ "\"subnet4\": [ { \n"
+ " \"id\": 1, \n"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], \n"
+ " \"subnet\": \"192.0.2.0/24\" \n"
+ " } ] \n"
+ "} \n";
+
+ EXPECT_THROW(parseDHCP4(config), Dhcp4ParseError);
+}
+
+// Verifies that simple list of valid classes parses and
+// is staged for commit.
+TEST_F(Dhcp4ParserTest, clientClassValidLifetime) {
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"client-classes\" : [ \n"
+ " { \n"
+ " \"name\": \"one\", \n"
+ " \"min-valid-lifetime\": 1000, \n"
+ " \"valid-lifetime\": 2000, \n"
+ " \"max-valid-lifetime\": 3000 \n"
+ " }, \n"
+ " { \n"
+ " \"name\": \"two\" \n"
+ " } \n"
+ "], \n"
+ "\"subnet4\": [ { \n"
+ " \"id\": 1, \n"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], \n"
+ " \"subnet\": \"192.0.2.0/24\" \n"
+ " } ] \n"
+ "} \n";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW_LOG(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ ASSERT_NO_THROW_LOG(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ // We check staging config because CfgMgr::commit hasn't been executed.
+ ClientClassDictionaryPtr dictionary;
+ dictionary = CfgMgr::instance().getStagingCfg()->getClientClassDictionary();
+ ASSERT_TRUE(dictionary);
+ EXPECT_EQ(2, dictionary->getClasses()->size());
+
+ // Execute the commit
+ ASSERT_NO_THROW(CfgMgr::instance().commit());
+
+ // Verify that after commit, the current config has the correct dictionary
+ dictionary = CfgMgr::instance().getCurrentCfg()->getClientClassDictionary();
+ ASSERT_TRUE(dictionary);
+ EXPECT_EQ(2, dictionary->getClasses()->size());
+
+ ClientClassDefPtr class_def = dictionary->findClass("one");
+ ASSERT_TRUE(class_def);
+ EXPECT_EQ(class_def->getValid().getMin(), 1000);
+ EXPECT_EQ(class_def->getValid().get(), 2000);
+ EXPECT_EQ(class_def->getValid().getMax(), 3000);
+
+ class_def = dictionary->findClass("two");
+ ASSERT_TRUE(class_def);
+ EXPECT_TRUE(class_def->getValid().unspecified());
+}
+
+// Verifies that simple list of valid template classes parses and
+// is staged for commit.
+TEST_F(Dhcp4ParserTest, templateClientClassValidLifetime) {
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"client-classes\" : [ \n"
+ " { \n"
+ " \"name\": \"one\", \n"
+ " \"min-valid-lifetime\": 1000, \n"
+ " \"valid-lifetime\": 2000, \n"
+ " \"max-valid-lifetime\": 3000, \n"
+ " \"template-test\": \"''\" \n"
+ " }, \n"
+ " { \n"
+ " \"name\": \"two\", \n"
+ " \"template-test\": \"''\" \n"
+ " } \n"
+ "], \n"
+ "\"subnet4\": [ { \n"
+ " \"id\": 1, \n"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], \n"
+ " \"subnet\": \"192.0.2.0/24\" \n"
+ " } ] \n"
+ "} \n";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW_LOG(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ ASSERT_NO_THROW_LOG(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ // We check staging config because CfgMgr::commit hasn't been executed.
+ ClientClassDictionaryPtr dictionary;
+ dictionary = CfgMgr::instance().getStagingCfg()->getClientClassDictionary();
+ ASSERT_TRUE(dictionary);
+ EXPECT_EQ(2, dictionary->getClasses()->size());
+
+ // Execute the commit
+ ASSERT_NO_THROW(CfgMgr::instance().commit());
+
+ // Verify that after commit, the current config has the correct dictionary
+ dictionary = CfgMgr::instance().getCurrentCfg()->getClientClassDictionary();
+ ASSERT_TRUE(dictionary);
+ EXPECT_EQ(2, dictionary->getClasses()->size());
+
+ ClientClassDefPtr class_def = dictionary->findClass("one");
+ ASSERT_TRUE(class_def);
+ ASSERT_TRUE(dynamic_cast<TemplateClientClassDef*>(class_def.get()));
+ EXPECT_EQ(class_def->getValid().getMin(), 1000);
+ EXPECT_EQ(class_def->getValid().get(), 2000);
+ EXPECT_EQ(class_def->getValid().getMax(), 3000);
+
+ class_def = dictionary->findClass("two");
+ ASSERT_TRUE(class_def);
+ ASSERT_TRUE(dynamic_cast<TemplateClientClassDef*>(class_def.get()));
+ EXPECT_TRUE(class_def->getValid().unspecified());
+}
+
+// Test verifies that regular configuration does not provide any user context
+// in the address pool.
+TEST_F(Dhcp4ParserTest, poolUserContextMissing) {
+ extractConfig(PARSER_CONFIGS[0]);
+ PoolPtr pool;
+ getPool(string(PARSER_CONFIGS[0]), 0, 0, pool);
+ ASSERT_TRUE(pool);
+ EXPECT_FALSE(pool->getContext());
+}
+
+// Test verifies that it's possible to specify empty user context in the
+// address pool.
+TEST_F(Dhcp4ParserTest, poolUserContextEmpty) {
+ extractConfig(PARSER_CONFIGS[1]);
+ PoolPtr pool;
+ getPool(string(PARSER_CONFIGS[1]), 0, 0, pool);
+ ASSERT_TRUE(pool);
+ ConstElementPtr ctx = pool->getContext();
+ ASSERT_TRUE(ctx);
+
+ // The context should be of type map and not contain any parameters.
+ EXPECT_EQ(Element::map, ctx->getType());
+ EXPECT_EQ(0, ctx->size());
+}
+
+// Test verifies that it's possible to specify parameters in the user context
+// in the address pool.
+TEST_F(Dhcp4ParserTest, poolUserContextData) {
+ extractConfig(PARSER_CONFIGS[2]);
+ PoolPtr pool;
+ getPool(string(PARSER_CONFIGS[2]), 0, 0, pool);
+ ASSERT_TRUE(pool);
+ ConstElementPtr ctx = pool->getContext();
+ ASSERT_TRUE(ctx);
+
+ // The context should be of type map and contain 4 parameters.
+ EXPECT_EQ(Element::map, ctx->getType());
+ EXPECT_EQ(3, ctx->size());
+ ConstElementPtr int_param = ctx->get("integer-param");
+ ConstElementPtr str_param = ctx->get("string-param");
+ ConstElementPtr bool_param = ctx->get("bool-param");
+
+ ASSERT_TRUE(int_param);
+ ASSERT_EQ(Element::integer, int_param->getType());
+ int64_t int_value;
+ EXPECT_NO_THROW(int_param->getValue(int_value));
+ EXPECT_EQ(42L, int_value);
+
+ ASSERT_TRUE(str_param);
+ ASSERT_EQ(Element::string, str_param->getType());
+ EXPECT_EQ("Sagittarius", str_param->stringValue());
+
+ ASSERT_TRUE(bool_param);
+ bool bool_value;
+ ASSERT_EQ(Element::boolean, bool_param->getType());
+ EXPECT_NO_THROW(bool_param->getValue(bool_value));
+ EXPECT_TRUE(bool_value);
+}
+
+// Test verifies that it's possible to specify parameters in the user context
+// in the min-max address pool.
+TEST_F(Dhcp4ParserTest, pooMinMaxlUserContext) {
+ extractConfig(PARSER_CONFIGS[3]);
+ PoolPtr pool;
+ getPool(string(PARSER_CONFIGS[3]), 0, 0, pool);
+ ASSERT_TRUE(pool);
+ ConstElementPtr ctx = pool->getContext();
+ ASSERT_TRUE(ctx);
+
+ // The context should be of type map and contain 4 parameters.
+ EXPECT_EQ(Element::map, ctx->getType());
+ EXPECT_EQ(3, ctx->size());
+ ConstElementPtr int_param = ctx->get("integer-param");
+ ConstElementPtr str_param = ctx->get("string-param");
+ ConstElementPtr bool_param = ctx->get("bool-param");
+
+ ASSERT_TRUE(int_param);
+ ASSERT_EQ(Element::integer, int_param->getType());
+ int64_t int_value;
+ EXPECT_NO_THROW(int_param->getValue(int_value));
+ EXPECT_EQ(42L, int_value);
+
+ ASSERT_TRUE(str_param);
+ ASSERT_EQ(Element::string, str_param->getType());
+ EXPECT_EQ("Sagittarius", str_param->stringValue());
+
+ ASSERT_TRUE(bool_param);
+ bool bool_value;
+ ASSERT_EQ(Element::boolean, bool_param->getType());
+ EXPECT_NO_THROW(bool_param->getValue(bool_value));
+ EXPECT_TRUE(bool_value);
+}
+
+// Test verifies the error message for an incorrect pool range
+// is what we expect.
+TEST_F(Dhcp4ParserTest, invalidPoolRange) {
+ string config = "{ " + genIfaceConfig() + ", \n" +
+ "\"valid-lifetime\": 4000, \n"
+ "\"rebind-timer\": 2000, \n"
+ "\"renew-timer\": 1000, \n"
+ "\"subnet4\": [ { \n"
+ " \"id\": 1, \n"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 19.2.0.200\" } ], \n"
+ " \"subnet\": \"192.0.2.0/24\" \n"
+ " } ] \n"
+ "} \n";
+
+ string expected = "Failed to create pool defined by: "
+ "192.0.2.1-19.2.0.200 (<string>:7:26)";
+
+ configure(config, CONTROL_RESULT_ERROR, expected);
+}
+
+// Test verifies the error message for an outside subnet pool range
+// is what we expect.
+TEST_F(Dhcp4ParserTest, outsideSubnetPool) {
+ string config = "{ " + genIfaceConfig() + ", \n" +
+ "\"valid-lifetime\": 4000, \n"
+ "\"rebind-timer\": 2000, \n"
+ "\"renew-timer\": 1000, \n"
+ "\"subnet4\": [ { \n"
+ " \"id\": 1, \n"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], \n"
+ " \"subnet\": \"10.0.2.0/24\" \n"
+ " } ] \n"
+ "} \n";
+
+ string expected = "subnet configuration failed: "
+ "a pool of type V4, with the following address range: "
+ "192.0.2.1-192.0.2.100 does not match the prefix of a subnet: "
+ "10.0.2.0/24 to which it is being added (<string>:5:14)";
+
+ configure(config, CONTROL_RESULT_ERROR, expected);
+}
+
+// Test verifies that empty shared networks are accepted.
+TEST_F(Dhcp4ParserTest, sharedNetworksEmpty) {
+ string config = "{\n"
+ "\"valid-lifetime\": 4000, \n"
+ "\"rebind-timer\": 2000, \n"
+ "\"renew-timer\": 1000, \n"
+ "\"subnet4\": [ { \n"
+ " \"id\": 1, \n"
+ " \"pools\": [ { \"pool\": \"10.0.2.1 - 10.0.2.100\" } ], \n"
+ " \"subnet\": \"10.0.2.0/24\" \n"
+ " } ],\n"
+ "\"shared-networks\": [ ]\n"
+ "} \n";
+
+ configure(config, CONTROL_RESULT_SUCCESS, "");
+}
+
+// Test verifies that if a shared network is defined, it at least has to have
+// a name.
+TEST_F(Dhcp4ParserTest, sharedNetworksNoName) {
+ string config = "{\n"
+ "\"valid-lifetime\": 4000, \n"
+ "\"rebind-timer\": 2000, \n"
+ "\"renew-timer\": 1000, \n"
+ "\"subnet4\": [ { \n"
+ " \"id\": 1, \n"
+ " \"pools\": [ { \"pool\": \"10.0.2.1 - 10.0.2.100\" } ], \n"
+ " \"subnet\": \"10.0.2.0/24\" \n"
+ " } ],\n"
+ "\"shared-networks\": [ { } ]\n"
+ "} \n";
+
+ EXPECT_THROW(parseDHCP4(config, true), Dhcp4ParseError);
+}
+
+// Test verifies that empty shared networks are accepted.
+TEST_F(Dhcp4ParserTest, sharedNetworksEmptyName) {
+ string config = "{\n"
+ "\"valid-lifetime\": 4000, \n"
+ "\"rebind-timer\": 2000, \n"
+ "\"renew-timer\": 1000, \n"
+ "\"subnet4\": [ { \n"
+ " \"id\": 1, \n"
+ " \"pools\": [ { \"pool\": \"10.0.2.1 - 10.0.2.100\" } ], \n"
+ " \"subnet\": \"10.0.2.0/24\" \n"
+ " } ],\n"
+ "\"shared-networks\": [ { \"name\": \"\" } ]\n"
+ "} \n";
+
+ configure(config, CONTROL_RESULT_ERROR,
+ "Shared-network with subnets is missing mandatory 'name' parameter");
+}
+
+// Test verifies that a degenerated shared-network (no subnets) is
+// accepted.
+TEST_F(Dhcp4ParserTest, sharedNetworksName) {
+ string config = "{\n"
+ "\"subnet4\": [ { \n"
+ " \"id\": 1, \n"
+ " \"pools\": [ { \"pool\": \"10.0.2.1 - 10.0.2.100\" } ], \n"
+ " \"subnet\": \"10.0.2.0/24\" \n"
+ " } ],\n"
+ "\"shared-networks\": [ { \"name\": \"foo\" } ]\n"
+ "} \n";
+
+ configure(config, CONTROL_RESULT_SUCCESS, "");
+
+ // Now verify that the shared network was indeed configured.
+ CfgSharedNetworks4Ptr cfg_net = CfgMgr::instance().getStagingCfg()
+ ->getCfgSharedNetworks4();
+ ASSERT_TRUE(cfg_net);
+ const SharedNetwork4Collection* nets = cfg_net->getAll();
+ ASSERT_TRUE(nets);
+ ASSERT_EQ(1, nets->size());
+ SharedNetwork4Ptr net = *(nets->begin());
+ ASSERT_TRUE(net);
+ EXPECT_EQ("foo", net->getName());
+
+ // Verify that there are no subnets in this shared-network
+ const Subnet4SimpleCollection* subs = net->getAllSubnets();
+ ASSERT_TRUE(subs);
+ EXPECT_EQ(0, subs->size());
+}
+
+// Test verifies that a degenerated shared-network (just one subnet) is
+// accepted.
+TEST_F(Dhcp4ParserTest, sharedNetworks1subnet) {
+ string config = "{\n"
+ "\"valid-lifetime\": 4000, \n"
+ "\"rebind-timer\": 2000, \n"
+ "\"renew-timer\": 1000, \n"
+ "\"shared-networks\": [ {\n"
+ " \"name\": \"foo\"\n,"
+ " \"subnet4\": [ { \n"
+ " \"id\": 1,\n"
+ " \"subnet\": \"192.0.2.0/24\",\n"
+ " \"pools\": [ { \"pool\": \"192.0.2.1-192.0.2.10\" } ]\n"
+ " } ]\n"
+ " } ]\n"
+ "} \n";
+
+ configure(config, CONTROL_RESULT_SUCCESS, "");
+
+ // Now verify that the shared network was indeed configured.
+ CfgSharedNetworks4Ptr cfg_net = CfgMgr::instance().getStagingCfg()
+ ->getCfgSharedNetworks4();
+ ASSERT_TRUE(cfg_net);
+
+ // There should be exactly one shared subnet.
+ const SharedNetwork4Collection* nets = cfg_net->getAll();
+ ASSERT_TRUE(nets);
+ ASSERT_EQ(1, nets->size());
+
+ SharedNetwork4Ptr net = *(nets->begin());
+ ASSERT_TRUE(net);
+ EXPECT_EQ("foo", net->getName());
+
+ // It should have one subnet.
+ const Subnet4SimpleCollection* subs = net->getAllSubnets();
+ ASSERT_TRUE(subs);
+ EXPECT_EQ(1, subs->size());
+ checkSubnet(*subs, "192.0.2.0/24", 1000, 2000, 4000);
+
+ // Now make sure the subnet was added to global list of subnets.
+ CfgSubnets4Ptr subnets4 = CfgMgr::instance().getStagingCfg()->getCfgSubnets4();
+ ASSERT_TRUE(subnets4);
+
+ const Subnet4Collection* gsubs = subnets4->getAll();
+ ASSERT_TRUE(gsubs);
+ checkSubnet(*gsubs, "192.0.2.0/24", 1000, 2000, 4000);
+}
+
+// Test verifies that a proper shared-network (three subnets) is
+// accepted. It verifies several things:
+// - that more than one subnet can be added to shared subnets
+// - that each subnet being part of the shared subnets is also stored in
+// global subnets collection
+// - that a subnet can inherit global values
+// - that subnet can override global parameters
+// - that overridden parameters only affect one subnet and not others
+TEST_F(Dhcp4ParserTest, sharedNetworks3subnets) {
+ string config = "{\n"
+ "\"valid-lifetime\": 4000, \n"
+ "\"min-valid-lifetime\": 3000, \n"
+ "\"max-valid-lifetime\": 5000, \n"
+ "\"rebind-timer\": 2000, \n"
+ "\"renew-timer\": 1000, \n"
+ "\"shared-networks\": [ {\n"
+ " \"name\": \"foo\"\n,"
+ " \"subnet4\": [\n"
+ " { \n"
+ " \"id\": 1,\n"
+ " \"subnet\": \"192.0.1.0/24\",\n"
+ " \"pools\": [ { \"pool\": \"192.0.1.1-192.0.1.10\" } ]\n"
+ " },\n"
+ " { \n"
+ " \"id\": 2,\n"
+ " \"subnet\": \"192.0.2.0/24\",\n"
+ " \"pools\": [ { \"pool\": \"192.0.2.1-192.0.2.10\" } ],\n"
+ " \"renew-timer\": 2,\n"
+ " \"rebind-timer\": 22,\n"
+ " \"valid-lifetime\": 222,\n"
+ " \"min-valid-lifetime\": 111,\n"
+ " \"max-valid-lifetime\": 333\n"
+ " },\n"
+ " { \n"
+ " \"id\": 3,\n"
+ " \"subnet\": \"192.0.3.0/24\",\n"
+ " \"pools\": [ { \"pool\": \"192.0.3.1-192.0.3.10\" } ]\n"
+ " }\n"
+ " ]\n"
+ " } ]\n"
+ "} \n";
+
+ configure(config, CONTROL_RESULT_SUCCESS, "");
+
+ // Now verify that the shared network was indeed configured.
+ CfgSharedNetworks4Ptr cfg_net = CfgMgr::instance().getStagingCfg()
+ ->getCfgSharedNetworks4();
+
+ // There is expected one shared subnet.
+ ASSERT_TRUE(cfg_net);
+ const SharedNetwork4Collection* nets = cfg_net->getAll();
+ ASSERT_TRUE(nets);
+ ASSERT_EQ(1, nets->size());
+
+ SharedNetwork4Ptr net = *(nets->begin());
+ ASSERT_TRUE(net);
+
+ EXPECT_EQ("foo", net->getName());
+
+ const Subnet4SimpleCollection* subs = net->getAllSubnets();
+ ASSERT_TRUE(subs);
+ EXPECT_EQ(3, subs->size());
+ checkSubnet(*subs, "192.0.1.0/24", 1000, 2000, 4000, 3000, 5000);
+ checkSubnet(*subs, "192.0.2.0/24", 2, 22, 222, 111, 333);
+ checkSubnet(*subs, "192.0.3.0/24", 1000, 2000, 4000, 3000, 5000);
+
+ // Now make sure the subnet was added to global list of subnets.
+ CfgSubnets4Ptr subnets4 = CfgMgr::instance().getStagingCfg()->getCfgSubnets4();
+ ASSERT_TRUE(subnets4);
+
+ const Subnet4Collection* gsubs = subnets4->getAll();
+ ASSERT_TRUE(gsubs);
+ checkSubnet(*gsubs, "192.0.1.0/24", 1000, 2000, 4000, 3000, 5000);
+ checkSubnet(*gsubs, "192.0.2.0/24", 2, 22, 222, 111, 333);
+ checkSubnet(*gsubs, "192.0.3.0/24", 1000, 2000, 4000, 3000, 5000);
+}
+
+// This test checks if parameters are derived properly:
+// - global to shared network
+// - shared network to subnet
+// Also, it tests that more than one shared network can be defined.
+TEST_F(Dhcp4ParserTest, sharedNetworksDerive) {
+
+ // We need to fake the interfaces present, because we want to test
+ // interface names inheritance. However, there are sanity checks
+ // on subnet level that would refuse the value if the interface
+ // is not present.
+ IfaceMgrTestConfig iface_config(true);
+
+ // This config is structured in a way that the first shared
+ // subnet have many parameters defined. The first subnet
+ // should inherit them. The second subnet overrides all
+ // values and those values should be used, not those from
+ // shared network scope.
+ string config = "{\n"
+ "\"renew-timer\": 1, \n" // global values here
+ "\"rebind-timer\": 2, \n"
+ "\"valid-lifetime\": 4, \n"
+ "\"min-valid-lifetime\": 3, \n"
+ "\"max-valid-lifetime\": 5, \n"
+ "\"shared-networks\": [ {\n"
+ " \"name\": \"foo\"\n," // shared network values here
+ " \"interface\": \"eth0\",\n"
+ " \"match-client-id\": false,\n"
+ " \"authoritative\": true,\n"
+ " \"next-server\": \"1.2.3.4\",\n"
+ " \"server-hostname\": \"foo\",\n"
+ " \"boot-file-name\": \"bar\",\n"
+ " \"store-extended-info\": true,\n"
+ " \"relay\": {\n"
+ " \"ip-address\": \"5.6.7.8\"\n"
+ " },\n"
+ " \"reservations-global\": false,\n"
+ " \"reservations-in-subnet\": false,\n"
+ " \"renew-timer\": 10,\n"
+ " \"rebind-timer\": 20,\n"
+ " \"valid-lifetime\": 40,\n"
+ " \"min-valid-lifetime\": 30,\n"
+ " \"max-valid-lifetime\": 50,\n"
+ " \"subnet4\": [\n"
+ " { \n"
+ " \"id\": 1,\n"
+ " \"subnet\": \"192.0.1.0/24\",\n"
+ " \"pools\": [ { \"pool\": \"192.0.1.1-192.0.1.10\" } ]\n"
+ " },\n"
+ " { \n"
+ " \"id\": 2,\n"
+ " \"subnet\": \"192.0.2.0/24\",\n"
+ " \"pools\": [ { \"pool\": \"192.0.2.1-192.0.2.10\" } ],\n"
+ " \"renew-timer\": 100,\n"
+ " \"rebind-timer\": 200,\n"
+ " \"valid-lifetime\": 400,\n"
+ " \"min-valid-lifetime\": 300,\n"
+ " \"max-valid-lifetime\": 500,\n"
+ " \"match-client-id\": true,\n"
+ " \"next-server\": \"11.22.33.44\",\n"
+ " \"server-hostname\": \"some-name.example.org\",\n"
+ " \"boot-file-name\": \"bootfile.efi\",\n"
+ " \"relay\": {\n"
+ " \"ip-address\": \"55.66.77.88\"\n"
+ " },\n"
+ " \"reservations-global\": false,\n"
+ " \"reservations-in-subnet\": true,\n"
+ " \"reservations-out-of-pool\": true\n"
+ " }\n"
+ " ]\n"
+ " },\n"
+ "{ // second shared-network starts here\n"
+ " \"name\": \"bar\",\n"
+ " \"subnet4\": [\n"
+ " {\n"
+ " \"id\": 3,\n"
+ " \"subnet\": \"192.0.3.0/24\",\n"
+ " \"pools\": [ { \"pool\": \"192.0.3.1-192.0.3.10\" } ]\n"
+ " }\n"
+ " ]\n"
+ " } ]\n"
+ "} \n";
+
+ configure(config, CONTROL_RESULT_SUCCESS, "");
+
+ // Now verify that the shared network was indeed configured.
+ CfgSharedNetworks4Ptr cfg_net = CfgMgr::instance().getStagingCfg()
+ ->getCfgSharedNetworks4();
+
+ // Two shared networks are expected.
+ ASSERT_TRUE(cfg_net);
+ const SharedNetwork4Collection* nets = cfg_net->getAll();
+ ASSERT_TRUE(nets);
+ ASSERT_EQ(2, nets->size());
+
+ SharedNetwork4Ptr net = nets->at(0);
+ ASSERT_TRUE(net);
+
+ // The first shared network has two subnets.
+ const Subnet4SimpleCollection* subs = net->getAllSubnets();
+ ASSERT_TRUE(subs);
+ EXPECT_EQ(2, subs->size());
+
+ // For the first subnet, the renew-timer should be 10, because it was
+ // derived from shared-network level. Other parameters a derived
+ // from global scope to shared-network level and later again to
+ // subnet4 level.
+ Subnet4Ptr s = checkSubnet(*subs, "192.0.1.0/24", 10, 20, 40, 30, 50);
+ ASSERT_TRUE(s);
+
+ // These are values derived from shared network scope:
+ EXPECT_EQ("eth0", s->getIface().get());
+ EXPECT_FALSE(s->getMatchClientId());
+ EXPECT_TRUE(s->getAuthoritative());
+ EXPECT_TRUE(s->getStoreExtendedInfo());
+ EXPECT_EQ(IOAddress("1.2.3.4"), s->getSiaddr());
+ EXPECT_EQ("foo", s->getSname().get());
+ EXPECT_EQ("bar", s->getFilename().get());
+ EXPECT_TRUE(s->hasRelayAddress(IOAddress("5.6.7.8")));
+ EXPECT_FALSE(s->getReservationsGlobal());
+ EXPECT_FALSE(s->getReservationsInSubnet());
+ EXPECT_FALSE(s->getReservationsOutOfPool());
+
+ // For the second subnet, the renew-timer should be 100, because it
+ // was specified explicitly. Other parameters a derived
+ // from global scope to shared-network level and later again to
+ // subnet4 level.
+ s = checkSubnet(*subs, "192.0.2.0/24", 100, 200, 400, 300, 500);
+
+ // These are values derived from shared network scope:
+ EXPECT_EQ("eth0", s->getIface().get());
+ EXPECT_TRUE(s->getMatchClientId());
+ EXPECT_TRUE(s->getAuthoritative());
+ EXPECT_TRUE(s->getStoreExtendedInfo());
+ EXPECT_EQ(IOAddress("11.22.33.44"), s->getSiaddr().get());
+ EXPECT_EQ("some-name.example.org", s->getSname().get());
+ EXPECT_EQ("bootfile.efi", s->getFilename().get());
+ EXPECT_TRUE(s->hasRelayAddress(IOAddress("55.66.77.88")));
+ EXPECT_FALSE(s->getReservationsGlobal());
+ EXPECT_TRUE(s->getReservationsInSubnet());
+ EXPECT_TRUE(s->getReservationsOutOfPool());
+
+ // Ok, now check the second shared subnet.
+ net = nets->at(1);
+ ASSERT_TRUE(net);
+
+ subs = net->getAllSubnets();
+ ASSERT_TRUE(subs);
+ EXPECT_EQ(1, subs->size());
+
+ // This subnet should derive its renew-timer from global scope.
+ // All other parameters should have default values.
+ s = checkSubnet(*subs, "192.0.3.0/24", 1, 2, 4, 3, 5);
+ EXPECT_EQ("", s->getIface().get());
+ EXPECT_TRUE(s->getMatchClientId());
+ EXPECT_FALSE(s->getAuthoritative());
+ EXPECT_FALSE(s->getStoreExtendedInfo());
+ EXPECT_EQ(IOAddress("0.0.0.0"), s->getSiaddr());
+ EXPECT_TRUE(s->getSname().empty());
+ EXPECT_TRUE(s->getFilename().empty());
+ EXPECT_FALSE(s->hasRelays());
+ EXPECT_FALSE(s->getReservationsGlobal());
+ EXPECT_TRUE(s->getReservationsInSubnet());
+ EXPECT_FALSE(s->getReservationsOutOfPool());
+}
+
+// This test checks if client-class is derived properly.
+TEST_F(Dhcp4ParserTest, sharedNetworksDeriveClientClass) {
+
+ // This config is structured in a way that the first shared network has
+ // client-class defined. This should in general be inherited by subnets, but
+ // it's also possible to override the values on subnet level.
+ string config = "{\n"
+ "\"renew-timer\": 1, \n" // global values here
+ "\"rebind-timer\": 2, \n"
+ "\"valid-lifetime\": 4, \n"
+ "\"shared-networks\": [ {\n"
+ " \"name\": \"foo\"\n," // shared network values here
+ " \"client-class\": \"alpha\",\n"
+ " \"subnet4\": [\n"
+ " { \n"
+ " \"id\": 1,\n"
+ " \"subnet\": \"192.0.1.0/24\",\n"
+ " \"pools\": [ { \"pool\": \"192.0.1.1-192.0.1.10\" } ]\n"
+ " },\n"
+ " { \n"
+ " \"id\": 2,\n"
+ " \"subnet\": \"192.0.2.0/24\",\n"
+ " \"pools\": [ { \"pool\": \"192.0.2.1-192.0.2.10\" } ],\n"
+ " \"client-class\": \"beta\"\n"
+ " }\n"
+ " ]\n"
+ " },\n"
+ "{ // second shared-network starts here\n"
+ " \"name\": \"bar\",\n"
+ " \"subnet4\": [\n"
+ " {\n"
+ " \"id\": 3,\n"
+ " \"subnet\": \"192.0.3.0/24\",\n"
+ " \"pools\": [ { \"pool\": \"192.0.3.1-192.0.3.10\" } ]\n"
+ " }\n"
+ " ]\n"
+
+ " } ]\n"
+ "} \n";
+
+ configure(config, CONTROL_RESULT_SUCCESS, "");
+
+ // Now verify that the shared network was indeed configured.
+ CfgSharedNetworks4Ptr cfg_net = CfgMgr::instance().getStagingCfg()
+ ->getCfgSharedNetworks4();
+
+ // Two shared networks are expected.
+ ASSERT_TRUE(cfg_net);
+ const SharedNetwork4Collection* nets = cfg_net->getAll();
+ ASSERT_TRUE(nets);
+ ASSERT_EQ(2, nets->size());
+
+ SharedNetwork4Ptr net = nets->at(0);
+ ASSERT_TRUE(net);
+
+ EXPECT_EQ("alpha", net->getClientClass().get());
+
+ // The first shared network has two subnets.
+ const Subnet4SimpleCollection* subs = net->getAllSubnets();
+ ASSERT_TRUE(subs);
+ EXPECT_EQ(2, subs->size());
+
+ // For the first subnet, the client-class should be inherited from
+ // shared-network level.
+ Subnet4Ptr s = checkSubnet(*subs, "192.0.1.0/24", 1, 2, 4);
+ ASSERT_TRUE(s);
+ EXPECT_EQ("alpha", s->getClientClass().get());
+
+ // For the second subnet, the values are overridden on subnet level.
+ // The value should not be inherited.
+ s = checkSubnet(*subs, "192.0.2.0/24", 1, 2, 4);
+ EXPECT_EQ("beta", s->getClientClass().get()); // beta defined on subnet level
+
+ // Ok, now check the second shared network. It doesn't have anything defined
+ // on shared-network or subnet level, so everything should have default
+ // values.
+ net = nets->at(1);
+ ASSERT_TRUE(net);
+
+ subs = net->getAllSubnets();
+ ASSERT_TRUE(subs);
+ EXPECT_EQ(1, subs->size());
+
+ s = checkSubnet(*subs, "192.0.3.0/24", 1, 2, 4);
+ EXPECT_TRUE(s->getClientClass().empty());
+}
+
+// This test checks multiple host data sources.
+TEST_F(Dhcp4ParserTest, hostsDatabases) {
+
+ string config = PARSER_CONFIGS[4];
+ extractConfig(config);
+ configure(config, CONTROL_RESULT_SUCCESS, "");
+
+ // Check database config
+ ConstCfgDbAccessPtr cfgdb =
+ CfgMgr::instance().getStagingCfg()->getCfgDbAccess();
+ ASSERT_TRUE(cfgdb);
+ const std::list<std::string>& hal = cfgdb->getHostDbAccessStringList();
+ ASSERT_EQ(2, hal.size());
+ // Keywords are in alphabetical order
+ EXPECT_EQ("name=keatest1 password=keatest type=mysql user=keatest", hal.front());
+ EXPECT_EQ("name=keatest2 password=keatest type=mysql user=keatest", hal.back());
+}
+
+// This test checks comments. Please keep it last.
+TEST_F(Dhcp4ParserTest, comments) {
+
+ string config = PARSER_CONFIGS[5];
+ extractConfig(config);
+ configure(config, CONTROL_RESULT_SUCCESS, "");
+
+ // Check global user context.
+ ConstElementPtr ctx = CfgMgr::instance().getStagingCfg()->getContext();
+ ASSERT_TRUE(ctx);
+ ASSERT_EQ(1, ctx->size());
+ ASSERT_TRUE(ctx->get("comment"));
+ EXPECT_EQ("\"A DHCPv4 server\"", ctx->get("comment")->str());
+
+ // There is a network interface configuration.
+ ConstCfgIfacePtr iface = CfgMgr::instance().getStagingCfg()->getCfgIface();
+ ASSERT_TRUE(iface);
+
+ // Check network interface configuration user context.
+ ConstElementPtr ctx_iface = iface->getContext();
+ ASSERT_TRUE(ctx_iface);
+ ASSERT_EQ(1, ctx_iface->size());
+ ASSERT_TRUE(ctx_iface->get("comment"));
+ EXPECT_EQ("\"Use wildcard\"", ctx_iface->get("comment")->str());
+
+ // There is a global option definition.
+ const OptionDefinitionPtr& opt_def =
+ LibDHCP::getRuntimeOptionDef("isc", 100);
+ ASSERT_TRUE(opt_def);
+ EXPECT_EQ("foo", opt_def->getName());
+ EXPECT_EQ(100, opt_def->getCode());
+ EXPECT_FALSE(opt_def->getArrayType());
+ EXPECT_EQ(OPT_IPV4_ADDRESS_TYPE, opt_def->getType());
+ EXPECT_TRUE(opt_def->getEncapsulatedSpace().empty());
+
+ // Check option definition user context.
+ ConstElementPtr ctx_opt_def = opt_def->getContext();
+ ASSERT_TRUE(ctx_opt_def);
+ ASSERT_EQ(1, ctx_opt_def->size());
+ ASSERT_TRUE(ctx_opt_def->get("comment"));
+ EXPECT_EQ("\"An option definition\"", ctx_opt_def->get("comment")->str());
+
+ // There is an option descriptor aka option data.
+ const OptionDescriptor& opt_desc =
+ CfgMgr::instance().getStagingCfg()->getCfgOption()->
+ get(DHCP4_OPTION_SPACE, DHO_DHCP_MESSAGE);
+ ASSERT_TRUE(opt_desc.option_);
+ EXPECT_EQ(DHO_DHCP_MESSAGE, opt_desc.option_->getType());
+
+ // Check option descriptor user context.
+ ConstElementPtr ctx_opt_desc = opt_desc.getContext();
+ ASSERT_TRUE(ctx_opt_desc);
+ ASSERT_EQ(1, ctx_opt_desc->size());
+ ASSERT_TRUE(ctx_opt_desc->get("comment"));
+ EXPECT_EQ("\"Set option value\"", ctx_opt_desc->get("comment")->str());
+
+ // And there are some client classes.
+ const ClientClassDictionaryPtr& dict =
+ CfgMgr::instance().getStagingCfg()->getClientClassDictionary();
+ ASSERT_TRUE(dict);
+ EXPECT_EQ(3, dict->getClasses()->size());
+ ClientClassDefPtr cclass = dict->findClass("all");
+ ASSERT_TRUE(cclass);
+ EXPECT_EQ("all", cclass->getName());
+ EXPECT_EQ("'' == ''", cclass->getTest());
+
+ // Check client class user context.
+ ConstElementPtr ctx_class = cclass->getContext();
+ ASSERT_TRUE(ctx_class);
+ ASSERT_EQ(1, ctx_class->size());
+ ASSERT_TRUE(ctx_class->get("comment"));
+ EXPECT_EQ("\"match all\"", ctx_class->get("comment")->str());
+
+ // The 'none' class has no user-context/comment.
+ cclass = dict->findClass("none");
+ ASSERT_TRUE(cclass);
+ EXPECT_EQ("none", cclass->getName());
+ EXPECT_EQ("", cclass->getTest());
+ EXPECT_FALSE(cclass->getContext());
+
+ // The 'both' class has a user context and a comment.
+ cclass = dict->findClass("both");
+ EXPECT_EQ("both", cclass->getName());
+ EXPECT_EQ("", cclass->getTest());
+ ctx_class = cclass->getContext();
+ ASSERT_TRUE(ctx_class);
+ ASSERT_EQ(2, ctx_class->size());
+ ASSERT_TRUE(ctx_class->get("comment"));
+ EXPECT_EQ("\"a comment\"", ctx_class->get("comment")->str());
+ ASSERT_TRUE(ctx_class->get("version"));
+ EXPECT_EQ("1", ctx_class->get("version")->str());
+
+ // There is a control socket.
+ ConstElementPtr socket =
+ CfgMgr::instance().getStagingCfg()->getControlSocketInfo();
+ ASSERT_TRUE(socket);
+ ASSERT_TRUE(socket->get("socket-type"));
+ EXPECT_EQ("\"unix\"", socket->get("socket-type")->str());
+ ASSERT_TRUE(socket->get("socket-name"));
+ EXPECT_EQ("\"/tmp/kea4-ctrl-socket\"", socket->get("socket-name")->str());
+
+ // Check control socket comment and user context.
+ ConstElementPtr ctx_socket = socket->get("user-context");
+ ASSERT_EQ(1, ctx_socket->size());
+ ASSERT_TRUE(ctx_socket->get("comment"));
+ EXPECT_EQ("\"Indirect comment\"", ctx_socket->get("comment")->str());
+
+ // Now verify that the shared network was indeed configured.
+ const CfgSharedNetworks4Ptr& cfg_net =
+ CfgMgr::instance().getStagingCfg()->getCfgSharedNetworks4();
+ ASSERT_TRUE(cfg_net);
+ const SharedNetwork4Collection* nets = cfg_net->getAll();
+ ASSERT_TRUE(nets);
+ ASSERT_EQ(1, nets->size());
+ SharedNetwork4Ptr net = nets->at(0);
+ ASSERT_TRUE(net);
+ EXPECT_EQ("foo", net->getName());
+
+ // Check shared network user context.
+ ConstElementPtr ctx_net = net->getContext();
+ ASSERT_TRUE(ctx_net);
+ ASSERT_EQ(1, ctx_net->size());
+ ASSERT_TRUE(ctx_net->get("comment"));
+ EXPECT_EQ("\"A shared network\"", ctx_net->get("comment")->str());
+
+ // The shared network has a subnet.
+ const Subnet4SimpleCollection* subs = net->getAllSubnets();
+ ASSERT_TRUE(subs);
+ ASSERT_EQ(1, subs->size());
+ Subnet4Ptr sub = *subs->begin();
+ ASSERT_TRUE(sub);
+ EXPECT_EQ(100, sub->getID());
+ EXPECT_EQ("192.0.1.0/24", sub->toText());
+
+ // Check subnet user context.
+ ConstElementPtr ctx_sub = sub->getContext();
+ ASSERT_TRUE(ctx_sub);
+ ASSERT_EQ(1, ctx_sub->size());
+ ASSERT_TRUE(ctx_sub->get("comment"));
+ EXPECT_EQ("\"A subnet\"", ctx_sub->get("comment")->str());
+
+ // The subnet has a pool.
+ const PoolCollection& pools = sub->getPools(Lease::TYPE_V4);
+ ASSERT_EQ(1, pools.size());
+ PoolPtr pool = pools.at(0);
+ ASSERT_TRUE(pool);
+
+ // Check pool user context.
+ ConstElementPtr ctx_pool = pool->getContext();
+ ASSERT_TRUE(ctx_pool);
+ ASSERT_EQ(1, ctx_pool->size());
+ ASSERT_TRUE(ctx_pool->get("comment"));
+ EXPECT_EQ("\"A pool\"", ctx_pool->get("comment")->str());
+
+ // The subnet has a host reservation.
+ uint8_t hw[] = { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF };
+ ConstHostPtr host =
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->
+ get4(100, Host::IDENT_HWADDR, &hw[0], sizeof(hw));
+ ASSERT_TRUE(host);
+ EXPECT_EQ(Host::IDENT_HWADDR, host->getIdentifierType());
+ EXPECT_EQ("aa:bb:cc:dd:ee:ff", host->getHWAddress()->toText(false));
+ EXPECT_FALSE(host->getDuid());
+ EXPECT_EQ(100, host->getIPv4SubnetID());
+ EXPECT_EQ(SUBNET_ID_UNUSED, host->getIPv6SubnetID());
+ EXPECT_EQ("foo.example.com", host->getHostname());
+
+ // Check host user context.
+ ConstElementPtr ctx_host = host->getContext();
+ ASSERT_TRUE(ctx_host);
+ ASSERT_EQ(1, ctx_host->size());
+ ASSERT_TRUE(ctx_host->get("comment"));
+ EXPECT_EQ("\"A host reservation\"", ctx_host->get("comment")->str());
+
+ // The host reservation has an option data.
+ ConstCfgOptionPtr opts = host->getCfgOption4();
+ ASSERT_TRUE(opts);
+ EXPECT_FALSE(opts->empty());
+ const OptionDescriptor& host_desc =
+ opts->get(DHCP4_OPTION_SPACE, DHO_DOMAIN_NAME);
+ ASSERT_TRUE(host_desc.option_);
+ EXPECT_EQ(DHO_DOMAIN_NAME, host_desc.option_->getType());
+
+ // Check embedded option data user context.
+ ConstElementPtr ctx_host_desc = host_desc.getContext();
+ ASSERT_TRUE(ctx_host_desc);
+ ASSERT_EQ(1, ctx_host_desc->size());
+ ASSERT_TRUE(ctx_host_desc->get("comment"));
+ EXPECT_EQ("\"An option in a reservation\"",
+ ctx_host_desc->get("comment")->str());
+
+ // Finally dynamic DNS update configuration.
+ const D2ClientConfigPtr& d2 =
+ CfgMgr::instance().getStagingCfg()->getD2ClientConfig();
+ ASSERT_TRUE(d2);
+ EXPECT_FALSE(d2->getEnableUpdates());
+
+ // Check dynamic DNS update configuration user context.
+ ConstElementPtr ctx_d2 = d2->getContext();
+ ASSERT_TRUE(ctx_d2);
+ ASSERT_EQ(1, ctx_d2->size());
+ ASSERT_TRUE(ctx_d2->get("comment"));
+ EXPECT_EQ("\"No dynamic DNS\"", ctx_d2->get("comment")->str());
+
+#if 0
+ // Loggers section supports comments too.
+
+ string logging = "{\n"
+ "\"loggers\": [ {\n"
+ " \"comment\": \"A logger\",\n"
+ " \"name\": \"kea-dhcp4\"\n"
+ "} ]\n";
+#endif
+}
+
+// This test verifies that the global host reservations can be specified
+TEST_F(Dhcp4ParserTest, globalReservations) {
+ ConstElementPtr x;
+ string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, \n"
+ "\"renew-timer\": 1000,\n"
+ "\"reservations\": [\n"
+ " {\n"
+ " \"duid\": \"01:02:03:04:05:06:07:08:09:0A\",\n"
+ " \"ip-address\": \"192.0.200.1\",\n"
+ " \"hostname\": \"global1\",\n"
+ " \"option-data\": [\n"
+ " {\n"
+ " \"name\": \"name-servers\",\n"
+ " \"data\": \"192.0.3.15\"\n"
+ " },\n"
+ " {\n"
+ " \"name\": \"default-ip-ttl\",\n"
+ " \"data\": \"32\"\n"
+ " }\n"
+ " ]\n"
+ " },\n"
+ " {\n"
+ " \"hw-address\": \"01:02:03:04:05:06\",\n"
+ " \"hostname\": \"global2\",\n"
+ " \"option-data\": [\n"
+ " {\n"
+ " \"name\": \"name-servers\",\n"
+ " \"data\": \"192.0.3.95\"\n"
+ " },\n"
+ " {\n"
+ " \"name\": \"default-ip-ttl\",\n"
+ " \"data\": \"11\"\n"
+ " }\n"
+ " ]\n"
+ " }],\n"
+ "\"subnet4\": [ \n"
+ " { \n"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],\n"
+ " \"subnet\": \"192.0.2.0/24\", \n"
+ " \"id\": 123,\n"
+ " \"reservations\": [\n"
+ " ]\n"
+ " },\n"
+ " {\n"
+ " \"pools\": [ { \"pool\": \"192.0.4.101 - 192.0.4.150\" } ],\n"
+ " \"subnet\": \"192.0.4.0/24\",\n"
+ " \"id\": 542\n"
+ " } ],\n"
+ "\"valid-lifetime\": 4000"
+ "}\n";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(x, 0);
+
+ // Make sure all subnets have been successfully configured. There is no
+ // need to sanity check the subnet properties because it should have
+ // been already tested by other tests.
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->getAll();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(2, subnets->size());
+
+ // Hosts configuration must be available.
+ CfgHostsPtr hosts_cfg = CfgMgr::instance().getStagingCfg()->getCfgHosts();
+ ASSERT_TRUE(hosts_cfg);
+
+ // Let's create a hardware address of the host named "global2"
+ std::vector<uint8_t> hwaddr;
+ for (unsigned int i = 1; i < 7; ++i) {
+ hwaddr.push_back(static_cast<uint8_t>(i));
+ }
+
+ // Retrieve the global reservation and sanity check the hostname reserved.
+ ConstHostPtr host = hosts_cfg->get4(SUBNET_ID_GLOBAL, Host::IDENT_HWADDR,
+ &hwaddr[0], hwaddr.size());
+ ASSERT_TRUE(host);
+ EXPECT_EQ("global2", host->getHostname());
+
+ // Check that options are stored correctly.
+ Option4AddrLstPtr opt_dns =
+ retrieveOption<Option4AddrLstPtr>(*host, DHO_NAME_SERVERS);
+ ASSERT_TRUE(opt_dns);
+ Option4AddrLst::AddressContainer dns_addrs = opt_dns->getAddresses();
+ ASSERT_EQ(1, dns_addrs.size());
+ EXPECT_EQ("192.0.3.95", dns_addrs[0].toText());
+ OptionUint8Ptr opt_ttl =
+ retrieveOption<OptionUint8Ptr>(*host, DHO_DEFAULT_IP_TTL);
+ ASSERT_TRUE(opt_ttl);
+ EXPECT_EQ(11, static_cast<int>(opt_ttl->getValue()));
+
+ // This reservation should be global solely and not assigned to
+ // either subnet
+ EXPECT_FALSE(hosts_cfg->get4(123, Host::IDENT_HWADDR,
+ &hwaddr[0], hwaddr.size()));
+
+ EXPECT_FALSE(hosts_cfg->get4(542, Host::IDENT_HWADDR,
+ &hwaddr[0], hwaddr.size()));
+
+ // Do the same test for the DUID based reservation.
+ std::vector<uint8_t> duid;
+ for (unsigned int i = 1; i < 0xb; ++i) {
+ duid.push_back(static_cast<uint8_t>(i));
+ }
+
+ // Retrieve the global reservation and sanity check the hostname reserved.
+ host = hosts_cfg->get4(SUBNET_ID_GLOBAL, Host::IDENT_DUID, &duid[0], duid.size());
+ ASSERT_TRUE(host);
+ EXPECT_EQ("global1", host->getHostname());
+
+ // Check that options are assigned correctly.
+ opt_dns = retrieveOption<Option4AddrLstPtr>(*host, DHO_NAME_SERVERS);
+ ASSERT_TRUE(opt_dns);
+ dns_addrs = opt_dns->getAddresses();
+ ASSERT_EQ(1, dns_addrs.size());
+ EXPECT_EQ("192.0.3.15", dns_addrs[0].toText());
+ opt_ttl = retrieveOption<OptionUint8Ptr>(*host, DHO_DEFAULT_IP_TTL);
+ ASSERT_TRUE(opt_ttl);
+ EXPECT_EQ(32, static_cast<int>(opt_ttl->getValue()));
+
+ // This reservation should be global solely and not assigned to
+ // either subnet
+ EXPECT_FALSE(hosts_cfg->get4(123, Host::IDENT_DUID, &duid[0], duid.size()));
+ EXPECT_FALSE(hosts_cfg->get4(542, Host::IDENT_DUID, &duid[0], duid.size()));
+}
+
+// Rather than disable these tests they are compiled out. This avoids them
+// reporting as disabled and thereby drawing attention to them.
+// This test verifies that configuration control with unsupported type fails
+TEST_F(Dhcp4ParserTest, configControlInfoNoFactory) {
+ string config = PARSER_CONFIGS[6];
+
+ // Unregister "mysql" and ignore the return value.
+ static_cast<void>(TestConfigBackendDHCPv4::
+ unregisterBackendType(ConfigBackendDHCPv4Mgr::instance(),
+ "mysql"));
+
+ // Should fail because "type=mysql" has no factories.
+ configure(config, CONTROL_RESULT_ERROR,
+ "during update from config backend database: "
+ "The type of the configuration backend: "
+ "'mysql' is not supported");
+}
+
+// This test verifies that configuration control info gets populated.
+TEST_F(Dhcp4ParserTest, configControlInfo) {
+ string config = PARSER_CONFIGS[6];
+
+ // Should be able to register a backend factory for "mysql".
+ ASSERT_TRUE(TestConfigBackendDHCPv4::
+ registerBackendType(ConfigBackendDHCPv4Mgr::instance(),
+ "mysql"));
+
+ // Should parse ok, now that the factory has been registered.
+ configure(config, CONTROL_RESULT_SUCCESS, "");
+
+ // Make sure the config control info is there.
+ process::ConstConfigControlInfoPtr info =
+ CfgMgr::instance().getStagingCfg()->getConfigControlInfo();
+ ASSERT_TRUE(info);
+
+ // Fetch the list of config dbs. It should have two entries.
+ const process::ConfigDbInfoList& dblist = info->getConfigDatabases();
+ ASSERT_EQ(2, dblist.size());
+
+ // Make sure the entries are what we expect and in the right order.
+ // (DbAccessParser creates access strings with the keywords in
+ // alphabetical order).
+ EXPECT_EQ("name=keatest1 password=keatest type=mysql user=keatest",
+ dblist.front().getAccessString());
+ EXPECT_EQ("name=keatest2 password=keatest type=mysql user=keatest",
+ dblist.back().getAccessString());
+
+ // Verify that the config-fetch-wait-time is correct.
+ EXPECT_FALSE(info->getConfigFetchWaitTime().unspecified());
+ EXPECT_EQ(10, info->getConfigFetchWaitTime().get());
+}
+
+// Check whether it is possible to configure server-tag
+TEST_F(Dhcp4ParserTest, serverTag) {
+ // Config without server-tag
+ string config_no_tag = "{ " + genIfaceConfig() + "," +
+ "\"subnet4\": [ ] "
+ "}";
+
+ // Config with server-tag
+ string config_tag = "{ " + genIfaceConfig() + "," +
+ "\"server-tag\": \"boo\", "
+ "\"subnet4\": [ ] "
+ "}";
+
+ // Config with an invalid server-tag
+ string bad_tag = "{ " + genIfaceConfig() + "," +
+ "\"server-tag\": 777, "
+ "\"subnet4\": [ ] "
+ "}";
+
+ // Let's check the default. It should be empty.
+ ASSERT_TRUE(CfgMgr::instance().getStagingCfg()->getServerTag().empty());
+
+ // Configuration with no tag should default to an emtpy tag value.
+ configure(config_no_tag, CONTROL_RESULT_SUCCESS, "");
+ EXPECT_TRUE(CfgMgr::instance().getStagingCfg()->getServerTag().empty());
+
+ // Clear the config
+ CfgMgr::instance().clear();
+
+ // Configuration with the tag should have the tag value.
+ configure(config_tag, CONTROL_RESULT_SUCCESS, "");
+ EXPECT_EQ("boo", CfgMgr::instance().getStagingCfg()->getServerTag().get());
+
+ // Make sure a invalid server-tag fails to parse.
+ ASSERT_THROW(parseDHCP4(bad_tag), std::exception);
+}
+
+// Check whether it is possible to configure packet queue
+TEST_F(Dhcp4ParserTest, dhcpQueueControl) {
+ struct Scenario {
+ std::string description_;
+ std::string json_;
+ std::string mt_json_;
+ };
+
+ std::vector<Scenario> scenarios = {
+ {
+ "no entry",
+ "",
+ "",
+ },
+ {
+ "queue disabled",
+ "{ \n"
+ " \"enable-queue\": false \n"
+ "} \n",
+ R"("multi-threading": {
+ "enable-multi-threading": false
+ })",
+ },
+ {
+ "queue enabled at first, but gets forcefully disabled by MT",
+ "{ \n"
+ " \"enable-queue\": true \n"
+ "} \n",
+ R"("multi-threading": {
+ "enable-multi-threading": true
+ })",
+ },
+ {
+ "queue disabled, arbitrary content allowed",
+ "{ \n"
+ " \"enable-queue\": false, \n"
+ " \"foo\": \"bogus\", \n"
+ " \"random-int\" : 1234 \n"
+ "} \n",
+ R"("multi-threading": {
+ "enable-multi-threading": false
+ })",
+ },
+ {
+ "queue enabled, with queue-type",
+ "{ \n"
+ " \"enable-queue\": true, \n"
+ " \"queue-type\": \"some-type\" \n"
+ "} \n",
+ R"("multi-threading": {
+ "enable-multi-threading": false
+ })",
+ },
+ {
+ "queue enabled with queue-type and arbitrary content",
+ "{ \n"
+ " \"enable-queue\": true, \n"
+ " \"queue-type\": \"some-type\", \n"
+ " \"foo\": \"bogus\", \n"
+ " \"random-int\" : 1234 \n"
+ "} \n",
+ R"("multi-threading": {
+ "enable-multi-threading": false
+ })",
+ }
+ };
+
+ // Let's check the default. It should be empty.
+ data::ConstElementPtr staged_control;
+ staged_control = CfgMgr::instance().getStagingCfg()->getDHCPQueueControl();
+ ASSERT_FALSE(staged_control);
+
+ // Iterate over the valid scenarios and verify they succeed.
+ data::ElementPtr exp_control;
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE(scenario.description_);
+ {
+ // Clear the config
+ CfgMgr::instance().clear();
+
+ // Construct the config JSON
+ std::stringstream os;
+ os << "{ " + genIfaceConfig();
+ if (!scenario.json_.empty()) {
+ os << ",\n \"dhcp-queue-control\": " << scenario.json_;
+ }
+ if (!scenario.mt_json_.empty()) {
+ os << ",\n" << scenario.mt_json_;
+ }
+ os << "\n}\n";
+
+ // Configure the server. This should succeed.
+ configure(os.str(), CONTROL_RESULT_SUCCESS, "");
+
+ // Fetch the queue control info.
+ staged_control = CfgMgr::instance().getStagingCfg()->getDHCPQueueControl();
+
+ // Make sure the staged queue config exists.
+ ASSERT_TRUE(staged_control);
+
+ // Now build the expected queue control content.
+ if (scenario.json_.empty()) {
+ exp_control = Element::createMap();
+ } else {
+ try {
+ exp_control = boost::const_pointer_cast<Element>(Element::fromJSON(scenario.json_));
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << " cannot convert expected JSON, test is broken:"
+ << ex.what();
+ }
+ }
+
+ // Add the defaults to expected queue control.
+ SimpleParser4::setDefaults(exp_control, SimpleParser4::DHCP_QUEUE_CONTROL4_DEFAULTS);
+
+ // This specific scenario is the only one where we expect enable-queue
+ // to be changed from what the user set it to.
+ if (scenario.description_ == "queue enabled at first, but gets forcefully disabled by MT") {
+ exp_control->set("enable-queue", Element::create(false));
+ }
+
+ // Verify that the staged queue control equals the expected queue control.
+ EXPECT_TRUE(staged_control->equals(*exp_control))
+#ifdef HAVE_CREATE_UNIFIED_DIFF
+ << "\nDiff:\n" << isc::test::generateDiff(prettyPrint(staged_control), prettyPrint(exp_control)) << "\n"
+#endif
+ ;
+ }
+ }
+}
+
+// Check that we catch invalid dhcp-queue-control content
+TEST_F(Dhcp4ParserTest, dhcpQueueControlInvalid) {
+ struct Scenario {
+ std::string description_;
+ std::string json_;
+ std::string exp_error_;
+ };
+
+ std::vector<Scenario> scenarios = {
+ {
+ "not a map",
+ "75 \n",
+ "<string>:2.24-25: syntax error, unexpected integer, expecting {"
+ },
+ {
+ "enable-queue missing",
+ "{ \n"
+ " \"enable-type\": \"some-type\" \n"
+ "} \n",
+ "missing parameter 'enable-queue' (<string>:2:2) "
+ "[dhcp-queue-control map between <string>:2:24 and <string>:4:1]"
+ },
+ {
+ "enable-queue not boolean",
+ "{ \n"
+ " \"enable-queue\": \"always\" \n"
+ "} \n",
+ "<string>:3.20-27: syntax error, unexpected constant string, "
+ "expecting boolean"
+ },
+ {
+ "queue enabled, type not a string",
+ "{ \n"
+ " \"enable-queue\": true, \n"
+ " \"queue-type\": 7777 \n"
+ "} \n",
+ "<string>:4.18-21: syntax error, unexpected integer, "
+ "expecting constant string"
+ }
+ };
+
+ // Iterate over the incorrect scenarios and verify they
+ // fail as expected. Note, we use parseDHCP4() directly
+ // as all of the errors above are enforced by the grammar.
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE(scenario.description_);
+ {
+ // Construct the config JSON
+ std::stringstream os;
+ os << "{ " + genIfaceConfig();
+ os << ",\n \"dhcp-queue-control\": " << scenario.json_;
+ os << "} \n";
+
+ std::string error_msg = "";
+ try {
+ ASSERT_TRUE(parseDHCP4(os.str(), false)) << "parser returned empty element";
+ } catch(const std::exception& ex) {
+ error_msg = ex.what();
+ }
+
+ ASSERT_FALSE(error_msg.empty()) << "parseDHCP4 should have thrown";
+ EXPECT_EQ(scenario.exp_error_, error_msg);
+ }
+ }
+}
+
+// Checks inheritence of calculate-tee-times, t1-percent, t2-percent
+TEST_F(Dhcp4ParserTest, calculateTeeTimesInheritence) {
+ // Configure the server. This should succeed.
+ string config =
+ "{ \n"
+ " \"interfaces-config\": { \n"
+ " \"interfaces\": [\"*\" ] \n"
+ " }, \n"
+ " \"valid-lifetime\": 4000, \n"
+ " \"shared-networks\": [ { \n"
+ " \"name\": \"foo\", \n"
+ " \"calculate-tee-times\": true, \n"
+ " \"t1-percent\": .4, \n"
+ " \"t2-percent\": .75,\n"
+ " \"subnet4\": ["
+ " { "
+ " \"id\": 100,"
+ " \"subnet\": \"192.0.1.0/24\", \n"
+ " \"pools\": [ { \"pool\": \"192.0.1.1-192.0.1.10\" } ], \n"
+ " \"calculate-tee-times\": false,\n"
+ " \"t1-percent\": .45, \n"
+ " \"t2-percent\": .65 \n"
+ " }, \n"
+ " { \n"
+ " \"id\": 200, \n"
+ " \"subnet\": \"192.0.2.0/24\", \n"
+ " \"pools\": [ { \"pool\": \"192.0.2.1-192.0.2.10\"} ] \n"
+ " } \n"
+ " ] \n"
+ " } ], \n"
+ " \"subnet4\": [ { \n"
+ " \"id\": 300, \n"
+ " \"subnet\": \"192.0.3.0/24\", \n"
+ " \"pools\": [ { \"pool\": \"192.0.3.0 - 192.0.3.15\" } ]\n"
+ " } ] \n"
+ "} \n";
+
+ extractConfig(config);
+ configure(config, CONTROL_RESULT_SUCCESS, "");
+
+ CfgSubnets4Ptr subnets4 = CfgMgr::instance().getStagingCfg()->getCfgSubnets4();
+
+ // Subnet 100 should use its own explicit values.
+ ConstSubnet4Ptr subnet4 = subnets4->getBySubnetId(100);
+ ASSERT_TRUE(subnet4);
+ EXPECT_FALSE(subnet4->getCalculateTeeTimes());
+ EXPECT_TRUE(util::areDoublesEquivalent(0.45, subnet4->getT1Percent()));
+ EXPECT_TRUE(util::areDoublesEquivalent(0.65, subnet4->getT2Percent()));
+
+ // Subnet 200 should use the shared-network values.
+ subnet4 = subnets4->getBySubnetId(200);
+ ASSERT_TRUE(subnet4);
+ EXPECT_EQ(true, subnet4->getCalculateTeeTimes());
+ EXPECT_TRUE(util::areDoublesEquivalent(0.4, subnet4->getT1Percent()));
+ EXPECT_TRUE(util::areDoublesEquivalent(0.75, subnet4->getT2Percent()));
+
+ // Subnet 300 should use the global values.
+ subnet4 = subnets4->getBySubnetId(300);
+ ASSERT_TRUE(subnet4);
+ EXPECT_FALSE(subnet4->getCalculateTeeTimes());
+ EXPECT_TRUE(util::areDoublesEquivalent(0.5, subnet4->getT1Percent()));
+ EXPECT_TRUE(util::areDoublesEquivalent(0.875, subnet4->getT2Percent()));
+}
+
+// This test checks that the global store-extended-info parameter is optional
+// and that values under the subnet are used.
+TEST_F(Dhcp4ParserTest, storeExtendedInfoNoGlobal) {
+ std::string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ "
+ "{"
+ " \"id\": 1,"
+ " \"store-extended-info\": true,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\""
+ "},"
+ "{"
+ " \"id\": 2,"
+ " \"store-extended-info\": false,"
+ " \"pools\": [ { \"pool\": \"192.0.3.1 - 192.0.3.100\" } ],"
+ " \"subnet\": \"192.0.3.0/24\""
+ "} ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ ASSERT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(status, 0);
+
+ CfgSubnets4Ptr cfg = CfgMgr::instance().getStagingCfg()->getCfgSubnets4();
+ Subnet4Ptr subnet1 = cfg->selectSubnet(IOAddress("192.0.2.1"));
+ ASSERT_TRUE(subnet1);
+ // Reset the fetch global function to staging (vs current) config.
+ subnet1->setFetchGlobalsFn([]() -> ConstCfgGlobalsPtr {
+ return (CfgMgr::instance().getStagingCfg()->getConfiguredGlobals());
+ });
+ EXPECT_TRUE(subnet1->getStoreExtendedInfo());
+
+ Subnet4Ptr subnet2 = cfg->selectSubnet(IOAddress("192.0.3.1"));
+ ASSERT_TRUE(subnet2);
+ // Reset the fetch global function to staging (vs current) config.
+ subnet2->setFetchGlobalsFn([]() -> ConstCfgGlobalsPtr {
+ return (CfgMgr::instance().getStagingCfg()->getConfiguredGlobals());
+ });
+ EXPECT_FALSE(subnet2->getStoreExtendedInfo());
+}
+
+// This test checks that the global store-extended-info parameter is used
+// when there is no such parameter under subnet and that the parameter
+// specified for a subnet overrides the global setting.
+TEST_F(Dhcp4ParserTest, storeExtendedInfoGlobal) {
+ std::string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"store-extended-info\": true,"
+ "\"subnet4\": [ "
+ "{"
+ " \"id\": 1,"
+ " \"store-extended-info\": false,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\""
+ "},"
+ "{"
+ " \"id\": 2,"
+ " \"pools\": [ { \"pool\": \"192.0.3.1 - 192.0.3.100\" } ],"
+ " \"subnet\": \"192.0.3.0/24\""
+ "} ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ ASSERT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(status, 0);
+
+ CfgSubnets4Ptr cfg = CfgMgr::instance().getStagingCfg()->getCfgSubnets4();
+ Subnet4Ptr subnet1 = cfg->selectSubnet(IOAddress("192.0.2.1"));
+ ASSERT_TRUE(subnet1);
+ // Reset the fetch global function to staging (vs current) config.
+ subnet1->setFetchGlobalsFn([]() -> ConstCfgGlobalsPtr {
+ return (CfgMgr::instance().getStagingCfg()->getConfiguredGlobals());
+ });
+ EXPECT_FALSE(subnet1->getStoreExtendedInfo());
+
+ Subnet4Ptr subnet2 = cfg->selectSubnet(IOAddress("192.0.3.1"));
+ ASSERT_TRUE(subnet2);
+ // Reset the fetch global function to staging (vs current) config.
+ subnet2->setFetchGlobalsFn([]() -> ConstCfgGlobalsPtr {
+ return (CfgMgr::instance().getStagingCfg()->getConfiguredGlobals());
+ });
+ EXPECT_TRUE(subnet2->getStoreExtendedInfo());
+}
+
+/// This test checks that the statistic-default-sample-count and age
+/// global parameters are committed to the stats manager as expected.
+TEST_F(Dhcp4ParserTest, statsDefaultLimits) {
+ std::string config = "{ " + genIfaceConfig() + "," +
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"statistic-default-sample-count\": 10, "
+ "\"statistic-default-sample-age\": 5, "
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ ASSERT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(status, 0);
+
+ CfgMgr::instance().commit();
+
+ stats::StatsMgr& stats_mgr = stats::StatsMgr::instance();
+ EXPECT_EQ(10, stats_mgr.getMaxSampleCountDefault());
+ EXPECT_EQ("00:00:05",
+ util::durationToText(stats_mgr.getMaxSampleAgeDefault(), 0));
+}
+
+// This test checks that using default multi threading settings works.
+TEST_F(Dhcp4ParserTest, multiThreadingDefaultSettings) {
+ std::string config = "{ " + genIfaceConfig() + "," +
+ "\"subnet4\": [ ]"
+ "}";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ ASSERT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(status, 0);
+
+ ConstElementPtr cfg = CfgMgr::instance().getStagingCfg()->getDHCPMultiThreading();
+ ASSERT_TRUE(cfg);
+
+ std::string content_json =
+ "{"
+ " \"enable-multi-threading\": true,\n"
+ " \"thread-pool-size\": 0,\n"
+ " \"packet-queue-size\": 64\n"
+ "}";
+ ConstElementPtr param;
+ ASSERT_NO_THROW(param = Element::fromJSON(content_json))
+ << "invalid context_json, test is broken";
+ ASSERT_TRUE(param->equals(*cfg))
+ << "expected: " << *(param) << std::endl
+ << " actual: " << *(cfg) << std::endl;
+}
+
+// This test checks that adding multi threading settings works.
+TEST_F(Dhcp4ParserTest, multiThreadingSettings) {
+ std::string content_json =
+ "{"
+ " \"enable-multi-threading\": true,\n"
+ " \"thread-pool-size\": 48,\n"
+ " \"packet-queue-size\": 1024\n"
+ "}";
+ std::string config = "{ " + genIfaceConfig() + "," +
+ "\"subnet4\": [ ], "
+ "\"multi-threading\": " + content_json + "}";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ extractConfig(config);
+
+ ConstElementPtr status;
+ ASSERT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ checkResult(status, 0);
+
+ ConstElementPtr cfg = CfgMgr::instance().getStagingCfg()->getDHCPMultiThreading();
+ ASSERT_TRUE(cfg);
+
+ ConstElementPtr param;
+ ASSERT_NO_THROW(param = Element::fromJSON(content_json))
+ << "invalid context_json, test is broken";
+ ASSERT_TRUE(param->equals(*cfg))
+ << "expected: " << *(param) << std::endl
+ << " actual: " << *(cfg) << std::endl;
+}
+
+// Verify that parsing for the global parameter, parked-packet-limit,
+// is correct.
+TEST_F(Dhcp4ParserTest, parkedPacketLimit) {
+ // Config without parked-packet-limit
+ string config_no_limit = "{ " + genIfaceConfig() + "," +
+ "\"subnet4\": [ ] "
+ "}";
+
+ // Config with parked-packet-limit
+ string config_limit = "{ " + genIfaceConfig() + "," +
+ "\"parked-packet-limit\": 777, "
+ "\"subnet4\": [ ] "
+ "}";
+
+ // Config with an invalid parked-packet-limit
+ string bad_limit = "{ " + genIfaceConfig() + "," +
+ "\"parked-packet-limit\": \"boo\", "
+ "\"subnet4\": [ ] "
+ "}";
+
+ // Should not exist after construction.
+ ASSERT_FALSE(CfgMgr::instance().getStagingCfg()->getConfiguredGlobal("parked-packet-limit"));
+
+ // Configuration with no limit should default to 256.
+ configure(config_no_limit, CONTROL_RESULT_SUCCESS, "");
+ ConstElementPtr ppl;
+ ASSERT_TRUE(ppl = CfgMgr::instance().getStagingCfg()->getConfiguredGlobal("parked-packet-limit"));
+ EXPECT_EQ(256, ppl->intValue());
+
+ // Clear the config
+ CfgMgr::instance().clear();
+
+ // Configuration with the limit should have the limit value.
+ configure(config_limit, CONTROL_RESULT_SUCCESS, "");
+
+ ASSERT_TRUE(ppl = CfgMgr::instance().getStagingCfg()->getConfiguredGlobal("parked-packet-limit"));
+ EXPECT_EQ(777, ppl->intValue());
+
+ // Make sure an invalid limit fails to parse.
+ ASSERT_THROW(parseDHCP4(bad_limit), std::exception);
+}
+
+} // namespace
diff --git a/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc b/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc
new file mode 100644
index 0000000..ee0e28a
--- /dev/null
+++ b/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc
@@ -0,0 +1,2234 @@
+// Copyright (C) 2012-2023 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <asiolink/interval_timer.h>
+#include <asiolink/io_service.h>
+#include <cc/command_interpreter.h>
+#include <config/command_mgr.h>
+#include <config/timeouts.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/libdhcp++.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/lease.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <dhcp4/ctrl_dhcp4_srv.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <hooks/hooks_manager.h>
+#include <log/logger_support.h>
+#include <stats/stats_mgr.h>
+#include <util/multi_threading_mgr.h>
+#include <util/chrono_time_utils.h>
+#include <testutils/io_utils.h>
+#include <testutils/unix_control_client.h>
+#include <testutils/sandbox.h>
+
+#include "marker_file.h"
+#include "test_libraries.h"
+
+#include <boost/scoped_ptr.hpp>
+#include <gtest/gtest.h>
+
+#include <fstream>
+#include <iomanip>
+#include <sstream>
+#include <thread>
+
+#include <arpa/inet.h>
+#include <unistd.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::config;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::hooks;
+using namespace isc::stats;
+using namespace isc::test;
+using namespace isc::util;
+namespace ph = std::placeholders;
+
+namespace {
+
+/// @brief Simple RAII class which stops IO service upon destruction
+/// of the object.
+class IOServiceWork {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// @param io_service Pointer to the IO service to be stopped.
+ explicit IOServiceWork(const IOServicePtr& io_service)
+ : io_service_(io_service) {
+ }
+
+ /// @brief Destructor.
+ ///
+ /// Stops IO service.
+ ~IOServiceWork() {
+ io_service_->stop();
+ }
+
+private:
+
+ /// @brief Pointer to the IO service to be stopped upon destruction.
+ IOServicePtr io_service_;
+
+};
+
+class NakedControlledDhcpv4Srv: public ControlledDhcpv4Srv {
+ // "Naked" DHCPv4 server, exposes internal fields
+public:
+ NakedControlledDhcpv4Srv() : ControlledDhcpv4Srv(0) {
+ CfgMgr::instance().setFamily(AF_INET);
+ }
+
+ /// Expose internal methods for the sake of testing
+ using Dhcpv4Srv::receivePacket;
+ using Dhcpv4Srv::network_state_;
+};
+
+/// @brief Fixture class intended for testing control channel in the DHCPv4Srv
+class CtrlChannelDhcpv4SrvTest : public ::testing::Test {
+public:
+ isc::test::Sandbox sandbox;
+
+ /// @brief Path to the UNIX socket being used to communicate with the server
+ std::string socket_path_;
+
+ /// @brief List of interfaces (defaults to "*").
+ std::string interfaces_;
+
+ /// @brief Pointer to the tested server object
+ boost::shared_ptr<NakedControlledDhcpv4Srv> server_;
+
+ /// @brief Default constructor
+ ///
+ /// Sets socket path to its default value.
+ CtrlChannelDhcpv4SrvTest() : interfaces_("\"*\"") {
+ const char* env = getenv("KEA_SOCKET_TEST_DIR");
+ if (env) {
+ socket_path_ = string(env) + "/kea4.sock";
+ } else {
+ socket_path_ = sandbox.join("kea4.sock");
+ }
+ reset();
+ IfaceMgr::instance().setTestMode(false);
+ IfaceMgr::instance().setDetectCallback(std::bind(&IfaceMgr::checkDetectIfaces,
+ IfaceMgr::instancePtr().get(), ph::_1));
+ }
+
+ /// @brief Destructor
+ ~CtrlChannelDhcpv4SrvTest() {
+ LeaseMgrFactory::destroy();
+ StatsMgr::instance().removeAll();
+
+ CommandMgr::instance().closeCommandSocket();
+ CommandMgr::instance().deregisterAll();
+ CommandMgr::instance().setConnectionTimeout(TIMEOUT_DHCP_SERVER_RECEIVE_COMMAND);
+
+ server_.reset();
+ IfaceMgr::instance().setTestMode(false);
+ IfaceMgr::instance().setDetectCallback(std::bind(&IfaceMgr::checkDetectIfaces,
+ IfaceMgr::instancePtr().get(), ph::_1));
+ IfaceMgr::instance().clearIfaces();
+ IfaceMgr::instance().closeSockets();
+ IfaceMgr::instance().detectIfaces();
+ };
+
+ /// @brief Returns pointer to the server's IO service.
+ ///
+ /// @return Pointer to the server's IO service or null pointer if the server
+ /// hasn't been created.
+ IOServicePtr getIOService() {
+ return (server_ ? server_->getIOService() : IOServicePtr());
+ }
+
+ void createUnixChannelServer() {
+ static_cast<void>(::remove(socket_path_.c_str()));
+
+ // Just a simple config. The important part here is the socket
+ // location information.
+ std::string header =
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [";
+
+ std::string body = "]"
+ " },"
+ " \"expired-leases-processing\": {"
+ " \"reclaim-timer-wait-time\": 60,"
+ " \"hold-reclaimed-time\": 500,"
+ " \"flush-reclaimed-timer-wait-time\": 60"
+ " },"
+ " \"rebind-timer\": 2000, "
+ " \"renew-timer\": 1000, "
+ " \"subnet4\": [ ],"
+ " \"valid-lifetime\": 4000,"
+ " \"control-socket\": {"
+ " \"socket-type\": \"unix\","
+ " \"socket-name\": \"";
+
+ std::string footer =
+ "\" },"
+ " \"lease-database\": {"
+ " \"type\": \"memfile\", \"persist\": false },"
+ " \"loggers\": [ {"
+ " \"name\": \"kea-dhcp4\","
+ " \"severity\": \"INFO\","
+ " \"debuglevel\": 0"
+ " } ]"
+ "}";
+
+ // Fill in the socket-name value with socket_path_ to
+ // make the actual configuration text.
+ std::string config_txt = header + interfaces_ + body + socket_path_ + footer;
+ ASSERT_NO_THROW(server_.reset(new NakedControlledDhcpv4Srv()));
+
+ ConstElementPtr config;
+ ASSERT_NO_THROW(config = parseDHCP4(config_txt));
+
+ // Parse the logger configuration explicitly into the staging config.
+ // Note this does not alter the current loggers, they remain in
+ // effect until we apply the logging config below. If no logging
+ // is supplied logging will revert to default logging.
+ server_->configureLogger(config, CfgMgr::instance().getStagingCfg());
+
+ // Let's apply the new logging. We do it early, so we'll be able to print
+ // out what exactly is wrong with the new config in case of problems.
+ CfgMgr::instance().getStagingCfg()->applyLoggingCfg();
+
+ ConstElementPtr answer = server_->processConfig(config);
+
+ // Commit the configuration so any subsequent reconfigurations
+ // will only close the command channel if its configuration has
+ // changed.
+ CfgMgr::instance().commit();
+
+ ASSERT_TRUE(answer);
+
+ int status = 0;
+ ConstElementPtr txt = isc::config::parseAnswer(status, answer);
+ // This should succeed. If not, print the error message.
+ ASSERT_EQ(0, status) << txt->str();
+
+ // Now check that the socket was indeed open.
+ ASSERT_GT(isc::config::CommandMgr::instance().getControlSocketFD(), -1);
+ }
+
+ /// @brief Reset hooks data
+ ///
+ /// Resets the data for the hooks-related portion of the test by ensuring
+ /// that no libraries are loaded and that any marker files are deleted.
+ void reset() {
+ // Unload any previously-loaded libraries.
+ EXPECT_TRUE(HooksManager::unloadLibraries());
+
+ // Get rid of any marker files.
+ static_cast<void>(remove(LOAD_MARKER_FILE));
+ static_cast<void>(remove(UNLOAD_MARKER_FILE));
+
+ IfaceMgr::instance().deleteAllExternalSockets();
+ CfgMgr::instance().clear();
+
+ // Remove unix socket file
+ static_cast<void>(::remove(socket_path_.c_str()));
+ }
+
+ /// @brief Conducts a command/response exchange via UnixCommandSocket
+ ///
+ /// This method connects to the given server over the given socket path.
+ /// If successful, it then sends the given command and retrieves the
+ /// server's response. Note that it calls the server's receivePacket()
+ /// method where needed to cause the server to process IO events on
+ /// control channel the control channel sockets.
+ ///
+ /// @param command the command text to execute in JSON form
+ /// @param response variable into which the received response should be
+ /// placed.
+ void sendUnixCommand(const std::string& command, std::string& response) {
+ response = "";
+ boost::scoped_ptr<UnixControlClient> client;
+ client.reset(new UnixControlClient());
+ ASSERT_TRUE(client);
+
+ // Connect to the server. This is expected to trigger server's acceptor
+ // handler when IOService::poll() is run.
+ ASSERT_TRUE(client->connectToServer(socket_path_));
+ ASSERT_NO_THROW(getIOService()->poll());
+
+ // Send the command. This will trigger server's handler which receives
+ // data over the unix domain socket. The server will start sending
+ // response to the client.
+ ASSERT_TRUE(client->sendCommand(command));
+ ASSERT_NO_THROW(getIOService()->poll());
+
+ // Read the response generated by the server. Note that getResponse
+ // only fails if there an IO error or no response data was present.
+ // It is not based on the response content.
+ ASSERT_TRUE(client->getResponse(response));
+
+ // Now disconnect and process the close event
+ client->disconnectFromServer();
+
+ ASSERT_NO_THROW(getIOService()->poll());
+ }
+
+ /// @brief Checks response for list-commands
+ ///
+ /// This method checks if the list-commands response is generally sane
+ /// and whether specified command is mentioned in the response.
+ ///
+ /// @param rsp response sent back by the server
+ /// @param command command expected to be on the list.
+ void checkListCommands(const ConstElementPtr& rsp, const std::string& command) {
+ ConstElementPtr params;
+ int status_code = -1;
+ EXPECT_NO_THROW(params = parseAnswer(status_code, rsp));
+ EXPECT_EQ(CONTROL_RESULT_SUCCESS, status_code);
+ ASSERT_TRUE(params);
+ ASSERT_EQ(Element::list, params->getType());
+
+ int cnt = 0;
+ for (size_t i = 0; i < params->size(); ++i) {
+ string tmp = params->get(i)->stringValue();
+ if (tmp == command) {
+ // Command found, but that's not enough. Need to continue working
+ // through the list to see if there are no duplicates.
+ cnt++;
+ }
+ }
+
+ // Exactly one command on the list is expected.
+ EXPECT_EQ(1, cnt) << "Command " << command << " not found";
+ }
+
+ /// @brief Check if the answer for write-config command is correct
+ ///
+ /// @param response_txt response in text form (as read from the control socket)
+ /// @param exp_status expected status (0 success, 1 failure)
+ /// @param exp_txt for success cases this defines the expected filename,
+ /// for failure cases this defines the expected error message
+ void checkConfigWrite(const std::string& response_txt, int exp_status,
+ const std::string& exp_txt = "") {
+
+ ConstElementPtr rsp;
+ EXPECT_NO_THROW(rsp = Element::fromJSON(response_txt));
+ ASSERT_TRUE(rsp);
+
+ int status;
+ ConstElementPtr params = parseAnswer(status, rsp);
+ EXPECT_EQ(exp_status, status);
+
+ if (exp_status == CONTROL_RESULT_SUCCESS) {
+ // Let's check couple things...
+
+ // The parameters must include filename
+ ASSERT_TRUE(params);
+ ASSERT_TRUE(params->get("filename"));
+ ASSERT_EQ(Element::string, params->get("filename")->getType());
+ EXPECT_EQ(exp_txt, params->get("filename")->stringValue());
+
+ // The parameters must include size. And the size
+ // must indicate some content.
+ ASSERT_TRUE(params->get("size"));
+ ASSERT_EQ(Element::integer, params->get("size")->getType());
+ int64_t size = params->get("size")->intValue();
+ EXPECT_LE(1, size);
+
+ // Now check if the file is really there and suitable for
+ // opening.
+ ifstream f(exp_txt, ios::binary | ios::ate);
+ ASSERT_TRUE(f.good());
+
+ // Now check that it is the correct size as reported.
+ EXPECT_EQ(size, static_cast<int64_t>(f.tellg()));
+
+ // Finally, check that it's really a JSON.
+ ElementPtr from_file = Element::fromJSONFile(exp_txt);
+ ASSERT_TRUE(from_file);
+ } else if (exp_status == CONTROL_RESULT_ERROR) {
+
+ // Let's check if the reason for failure was given.
+ ConstElementPtr text = rsp->get("text");
+ ASSERT_TRUE(text);
+ ASSERT_EQ(Element::string, text->getType());
+ EXPECT_EQ(exp_txt, text->stringValue());
+ } else {
+ ADD_FAILURE() << "Invalid expected status: " << exp_status;
+ }
+ }
+
+ /// @brief Handler for long command.
+ ///
+ /// It checks whether the received command is equal to the one specified
+ /// as an argument.
+ ///
+ /// @param expected_command String representing an expected command.
+ /// @param command_name Command name received by the handler.
+ /// @param arguments Command arguments received by the handler.
+ ///
+ /// @returns Success answer.
+ static ConstElementPtr
+ longCommandHandler(const std::string& expected_command,
+ const std::string& command_name,
+ const ConstElementPtr& arguments) {
+ // The handler is called with a command name and the structure holding
+ // command arguments. We have to rebuild the command from those
+ // two arguments so as it can be compared against expected_command.
+ ElementPtr entire_command = Element::createMap();
+ entire_command->set("command", Element::create(command_name));
+ entire_command->set("arguments", (arguments));
+
+ // The rebuilt command will have a different order of parameters so
+ // let's parse expected_command back to JSON to guarantee that
+ // both structures are built using the same order.
+ EXPECT_EQ(Element::fromJSON(expected_command)->str(),
+ entire_command->str());
+ return (createAnswer(CONTROL_RESULT_SUCCESS, "long command received ok"));
+ }
+
+ /// @brief Command handler which generates long response
+ ///
+ /// This handler generates a large response (over 400kB). It includes
+ /// a list of randomly generated strings to make sure that the test
+ /// can catch out of order delivery.
+ static ConstElementPtr longResponseHandler(const std::string&,
+ const ConstElementPtr&) {
+ ElementPtr arguments = Element::createList();
+ for (unsigned i = 0; i < 80000; ++i) {
+ std::ostringstream s;
+ s << std::setw(5) << i;
+ arguments->add(Element::create(s.str()));
+ }
+ return (createAnswer(CONTROL_RESULT_SUCCESS, arguments));
+ }
+};
+
+TEST_F(CtrlChannelDhcpv4SrvTest, commands) {
+
+ ASSERT_NO_THROW(
+ server_.reset(new NakedControlledDhcpv4Srv());
+ );
+
+ // Use empty parameters list
+ ElementPtr params(new isc::data::MapElement());
+ int rcode = -1;
+
+ // Case 1: send bogus command
+ ConstElementPtr result = ControlledDhcpv4Srv::processCommand("blah", params);
+ ConstElementPtr comment = parseAnswer(rcode, result);
+ EXPECT_EQ(1, rcode); // expect failure (no such command as blah)
+
+ // Case 2: send shutdown command without any parameters
+ result = ControlledDhcpv4Srv::processCommand("shutdown", params);
+ comment = parseAnswer(rcode, result);
+ EXPECT_EQ(0, rcode); // expect success
+ // Exit value should default to 0.
+ EXPECT_EQ(0, server_->getExitValue());
+
+ // Case 3: send shutdown command with exit-value parameter.
+ ConstElementPtr x(new isc::data::IntElement(77));
+ params->set("exit-value", x);
+
+ result = ControlledDhcpv4Srv::processCommand("shutdown", params);
+ comment = parseAnswer(rcode, result);
+ EXPECT_EQ(0, rcode); // expect success
+
+ // Exit value should match.
+ EXPECT_EQ(77, server_->getExitValue());
+}
+
+// Check that the "libreload" command will reload libraries
+TEST_F(CtrlChannelDhcpv4SrvTest, libreload) {
+ createUnixChannelServer();
+
+ // Ensure no marker files to start with.
+ ASSERT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
+ ASSERT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+
+ // Load two libraries
+ HookLibsCollection libraries;
+ libraries.push_back(make_pair(CALLOUT_LIBRARY_1, ConstElementPtr()));
+ HooksManager::loadLibraries(libraries);
+
+ // Check they are loaded.
+ HookLibsCollection loaded_libraries =
+ HooksManager::getLibraryInfo();
+ ASSERT_TRUE(libraries == loaded_libraries);
+
+ EXPECT_TRUE(checkMarkerFile(LOAD_MARKER_FILE, "1"));
+ EXPECT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+
+ // Now execute the "libreload" command. This should cause the libraries
+ // to unload and to reload.
+ std::string response;
+ sendUnixCommand("{ \"command\": \"libreload\" }", response);
+ EXPECT_EQ("{ \"result\": 0, "
+ "\"text\": \"Hooks libraries successfully reloaded "
+ "(WARNING: libreload is deprecated).\" }"
+ , response);
+
+ // Check that the libraries have unloaded and reloaded. The libraries are
+ // unloaded in the reverse order to which they are loaded. When they load,
+ // they should append information to the loading marker file.
+ EXPECT_TRUE(checkMarkerFile(UNLOAD_MARKER_FILE, "1"));
+ EXPECT_TRUE(checkMarkerFile(LOAD_MARKER_FILE, "11"));
+}
+
+// Check that the "libreload" command will fail to reload libraries which are
+// not compatible when multi-threading is enabled
+TEST_F(CtrlChannelDhcpv4SrvTest, libreloadFailMultiThreading) {
+ createUnixChannelServer();
+
+ // Ensure no marker files to start with.
+ ASSERT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
+ ASSERT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+
+ // Disable multi-threading to temporarily trick the hook manager
+ // into loading single-threaded libraries.
+ MultiThreadingMgr::instance().setMode(false);
+
+ // Load two libraries
+ HookLibsCollection libraries;
+ libraries.push_back(make_pair(CALLOUT_LIBRARY_1, ConstElementPtr()));
+ libraries.push_back(make_pair(CALLOUT_LIBRARY_2, ConstElementPtr()));
+ HooksManager::loadLibraries(libraries);
+
+ // Check they are loaded.
+ HookLibsCollection loaded_libraries =
+ HooksManager::getLibraryInfo();
+ ASSERT_TRUE(libraries == loaded_libraries);
+
+ // ... which also included checking that the marker file created by the
+ // load functions exists and holds the correct value (of "12" - the
+ // first library appends "1" to the file, the second appends "2"). Also
+ // check that the unload marker file does not yet exist.
+ EXPECT_TRUE(checkMarkerFile(LOAD_MARKER_FILE, "12"));
+ EXPECT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+
+ // Enable multi-threading before libreload command which should now fail
+ // as the second library is not multi-threading compatible.
+ MultiThreadingMgr::instance().setMode(true);
+
+ // Now execute the "libreload" command. This should cause the libraries
+ // to unload and to reload.
+ std::string response;
+ sendUnixCommand("{ \"command\": \"libreload\" }", response);
+ EXPECT_EQ("{ \"result\": 1, "
+ "\"text\": \"Failed to reload hooks libraries"
+ " (WARNING: libreload is deprecated).\" }"
+ , response);
+
+ // Check that the libraries have unloaded and failed to reload. The
+ // libraries are unloaded in the reverse order to which they are loaded.
+ // When they load, they should append information to the loading marker
+ // file. Failing to load the second library will also unload the first
+ // library.
+ EXPECT_TRUE(checkMarkerFile(UNLOAD_MARKER_FILE, "211"));
+ EXPECT_TRUE(checkMarkerFile(LOAD_MARKER_FILE, "121"));
+}
+
+// This test checks which commands are registered by the DHCPv4 server.
+TEST_F(CtrlChannelDhcpv4SrvTest, commandsRegistration) {
+
+ ConstElementPtr list_cmds = createCommand("list-commands");
+ ConstElementPtr answer;
+
+ // By default the list should be empty (except the standard list-commands
+ // supported by the CommandMgr itself)
+ EXPECT_NO_THROW(answer = CommandMgr::instance().processCommand(list_cmds));
+ ASSERT_TRUE(answer);
+ ASSERT_TRUE(answer->get("arguments"));
+ EXPECT_EQ("[ \"list-commands\" ]", answer->get("arguments")->str());
+
+ // Created server should register several additional commands.
+ ASSERT_NO_THROW(
+ server_.reset(new NakedControlledDhcpv4Srv());
+ );
+
+ EXPECT_NO_THROW(answer = CommandMgr::instance().processCommand(list_cmds));
+ ASSERT_TRUE(answer);
+
+ ASSERT_TRUE(answer->get("arguments"));
+ std::string command_list = answer->get("arguments")->str();
+
+ EXPECT_TRUE(command_list.find("\"list-commands\"") != string::npos);
+ EXPECT_TRUE(command_list.find("\"build-report\"") != string::npos);
+ EXPECT_TRUE(command_list.find("\"config-backend-pull\"") != string::npos);
+ EXPECT_TRUE(command_list.find("\"config-get\"") != string::npos);
+ EXPECT_TRUE(command_list.find("\"config-hash-get\"") != string::npos);
+ EXPECT_TRUE(command_list.find("\"config-set\"") != string::npos);
+ EXPECT_TRUE(command_list.find("\"config-write\"") != string::npos);
+ EXPECT_TRUE(command_list.find("\"leases-reclaim\"") != string::npos);
+ EXPECT_TRUE(command_list.find("\"libreload\"") != string::npos);
+ EXPECT_TRUE(command_list.find("\"server-tag-get\"") != string::npos);
+ EXPECT_TRUE(command_list.find("\"shutdown\"") != string::npos);
+ EXPECT_TRUE(command_list.find("\"statistic-get\"") != string::npos);
+ EXPECT_TRUE(command_list.find("\"statistic-get-all\"") != string::npos);
+ EXPECT_TRUE(command_list.find("\"statistic-remove\"") != string::npos);
+ EXPECT_TRUE(command_list.find("\"statistic-remove-all\"") != string::npos);
+ EXPECT_TRUE(command_list.find("\"statistic-reset\"") != string::npos);
+ EXPECT_TRUE(command_list.find("\"statistic-reset-all\"") != string::npos);
+ EXPECT_TRUE(command_list.find("\"statistic-sample-age-set\"") != string::npos);
+ EXPECT_TRUE(command_list.find("\"statistic-sample-age-set-all\"") != string::npos);
+ EXPECT_TRUE(command_list.find("\"statistic-sample-count-set\"") != string::npos);
+ EXPECT_TRUE(command_list.find("\"statistic-sample-count-set-all\"") != string::npos);
+ EXPECT_TRUE(command_list.find("\"status-get\"") != string::npos);
+ EXPECT_TRUE(command_list.find("\"version-get\"") != string::npos);
+
+ // Ok, and now delete the server. It should deregister its commands.
+ server_.reset();
+
+ // The list should be (almost) empty again.
+ EXPECT_NO_THROW(answer = CommandMgr::instance().processCommand(list_cmds));
+ ASSERT_TRUE(answer);
+ ASSERT_TRUE(answer->get("arguments"));
+ EXPECT_EQ("[ \"list-commands\" ]", answer->get("arguments")->str());
+}
+
+// Tests that the server properly responds to invalid commands sent
+// via ControlChannel
+TEST_F(CtrlChannelDhcpv4SrvTest, controlChannelNegative) {
+ createUnixChannelServer();
+ std::string response;
+
+ sendUnixCommand("{ \"command\": \"bogus\" }", response);
+ EXPECT_EQ("{ \"result\": 2,"
+ " \"text\": \"'bogus' command not supported.\" }", response);
+
+ sendUnixCommand("utter nonsense", response);
+ EXPECT_EQ("{ \"result\": 1, "
+ "\"text\": \"invalid first character u\" }",
+ response);
+}
+
+// Tests that the server properly responds to shutdown command sent
+// via ControlChannel
+TEST_F(CtrlChannelDhcpv4SrvTest, controlChannelShutdown) {
+ createUnixChannelServer();
+ std::string response;
+
+ sendUnixCommand("{ \"command\": \"shutdown\" }", response);
+ EXPECT_EQ("{ \"result\": 0, \"text\": \"Shutting down.\" }",response);
+}
+
+// Tests that the server properly responds to statistics commands. Note this
+// is really only intended to verify that the appropriate Statistics handler
+// is called based on the command. It is not intended to be an exhaustive
+// test of Dhcpv4 statistics.
+TEST_F(CtrlChannelDhcpv4SrvTest, controlChannelStats) {
+ createUnixChannelServer();
+ std::string response;
+
+ // Check statistic-get
+ sendUnixCommand("{ \"command\" : \"statistic-get\", "
+ " \"arguments\": {"
+ " \"name\":\"bogus\" }}", response);
+ EXPECT_EQ("{ \"arguments\": { }, \"result\": 0 }", response);
+
+ // Check statistic-get-all
+ sendUnixCommand("{ \"command\" : \"statistic-get-all\", "
+ " \"arguments\": {}}", response);
+
+ std::set<std::string> initial_stats = {
+ "pkt4-received",
+ "pkt4-discover-received",
+ "pkt4-offer-received",
+ "pkt4-request-received",
+ "pkt4-ack-received",
+ "pkt4-nak-received",
+ "pkt4-release-received",
+ "pkt4-decline-received",
+ "pkt4-inform-received",
+ "pkt4-unknown-received",
+ "pkt4-sent",
+ "pkt4-offer-sent",
+ "pkt4-ack-sent",
+ "pkt4-nak-sent",
+ "pkt4-parse-failed",
+ "pkt4-receive-drop",
+ "v4-allocation-fail",
+ "v4-allocation-fail-shared-network",
+ "v4-allocation-fail-subnet",
+ "v4-allocation-fail-no-pools",
+ "v4-allocation-fail-classes",
+ "v4-reservation-conflicts",
+ "v4-lease-reuses",
+ };
+
+ // preparing the schema which check if all statistics are set to zero
+ std::ostringstream s;
+ s << "{ \"arguments\": { ";
+ for (auto st = initial_stats.begin(); st != initial_stats.end();) {
+ s << "\"" << *st << "\": [ [ 0, \"";
+ s << isc::util::clockToText(StatsMgr::instance().getObservation(*st)->getInteger().second);
+ s << "\" ] ]";
+ if (++st != initial_stats.end()) {
+ s << ", ";
+ }
+ }
+ s << " }, \"result\": 0 }";
+
+ auto stats_get_all = s.str();
+
+ EXPECT_EQ(stats_get_all, response);
+
+ // Check statistic-reset
+ sendUnixCommand("{ \"command\" : \"statistic-reset\", "
+ " \"arguments\": {"
+ " \"name\":\"bogus\" }}", response);
+ EXPECT_EQ("{ \"result\": 1, \"text\": \"No 'bogus' statistic found\" }",
+ response);
+
+ // Check statistic-reset-all
+ sendUnixCommand("{ \"command\" : \"statistic-reset-all\", "
+ " \"arguments\": {}}", response);
+ EXPECT_EQ("{ \"result\": 0, \"text\": "
+ "\"All statistics reset to neutral values.\" }", response);
+
+ // Check statistic-remove
+ sendUnixCommand("{ \"command\" : \"statistic-remove\", "
+ " \"arguments\": {"
+ " \"name\":\"bogus\" }}", response);
+ EXPECT_EQ("{ \"result\": 1, \"text\": \"No 'bogus' statistic found\" }",
+ response);
+
+ // Check statistic-remove-all (deprecated).
+
+ // Check statistic-sample-age-set
+ sendUnixCommand("{ \"command\" : \"statistic-sample-age-set\", "
+ " \"arguments\": {"
+ " \"name\":\"bogus\", \"duration\": 1245 }}", response);
+ EXPECT_EQ("{ \"result\": 1, \"text\": \"No 'bogus' statistic found\" }",
+ response);
+
+ // Check statistic-sample-age-set-all
+ sendUnixCommand("{ \"command\" : \"statistic-sample-age-set-all\", "
+ " \"arguments\": {"
+ " \"duration\": 1245 }}", response);
+ EXPECT_EQ("{ \"result\": 0, \"text\": \"All statistics duration limit are set.\" }",
+ response);
+
+ // Check statistic-sample-count-set
+ sendUnixCommand("{ \"command\" : \"statistic-sample-count-set\", "
+ " \"arguments\": {"
+ " \"name\":\"bogus\", \"max-samples\": 100 }}", response);
+ EXPECT_EQ("{ \"result\": 1, \"text\": \"No 'bogus' statistic found\" }",
+ response);
+
+ // Check statistic-sample-count-set-all
+ sendUnixCommand("{ \"command\" : \"statistic-sample-count-set-all\", "
+ " \"arguments\": {"
+ " \"max-samples\": 100 }}", response);
+ EXPECT_EQ("{ \"result\": 0, \"text\": \"All statistics count limit are set.\" }",
+ response);
+}
+
+// Check that the "config-set" command will replace current configuration
+TEST_F(CtrlChannelDhcpv4SrvTest, configSet) {
+ createUnixChannelServer();
+
+ // Define strings to permutate the config arguments
+ // (Note the line feeds makes errors easy to find)
+ string set_config_txt = "{ \"command\": \"config-set\" \n";
+ string args_txt = " \"arguments\": { \n";
+ string dhcp4_cfg_txt =
+ " \"Dhcp4\": { \n"
+ " \"interfaces-config\": { \n"
+ " \"interfaces\": [\"*\"] \n"
+ " }, \n"
+ " \"valid-lifetime\": 4000, \n"
+ " \"renew-timer\": 1000, \n"
+ " \"rebind-timer\": 2000, \n"
+ " \"lease-database\": { \n"
+ " \"type\": \"memfile\", \n"
+ " \"persist\":false, \n"
+ " \"lfc-interval\": 0 \n"
+ " }, \n"
+ " \"expired-leases-processing\": { \n"
+ " \"reclaim-timer-wait-time\": 0, \n"
+ " \"hold-reclaimed-time\": 0, \n"
+ " \"flush-reclaimed-timer-wait-time\": 0 \n"
+ " },"
+ " \"subnet4\": [ \n";
+ string subnet1 =
+ " {\"subnet\": \"192.2.0.0/24\", \"id\": 1, \n"
+ " \"pools\": [{ \"pool\": \"192.2.0.1-192.2.0.50\" }]}\n";
+ string subnet2 =
+ " {\"subnet\": \"192.2.1.0/24\", \"id\": 2, \n"
+ " \"pools\": [{ \"pool\": \"192.2.1.1-192.2.1.50\" }]}\n";
+ string bad_subnet =
+ " {\"comment\": \"192.2.2.0/24\", \"id\": 10, \n"
+ " \"pools\": [{ \"pool\": \"192.2.2.1-192.2.2.50\" }]}\n";
+ string subnet_footer =
+ " ] \n";
+ string option_def =
+ " ,\"option-def\": [\n"
+ " {\n"
+ " \"name\": \"foo\",\n"
+ " \"code\": 163,\n"
+ " \"type\": \"uint32\",\n"
+ " \"array\": false,\n"
+ " \"record-types\": \"\",\n"
+ " \"space\": \"dhcp4\",\n"
+ " \"encapsulate\": \"\"\n"
+ " }\n"
+ "]\n";
+ string option_data =
+ " ,\"option-data\": [\n"
+ " {\n"
+ " \"name\": \"foo\",\n"
+ " \"code\": 163,\n"
+ " \"space\": \"dhcp4\",\n"
+ " \"csv-format\": true,\n"
+ " \"data\": \"12345\"\n"
+ " }\n"
+ "]\n";
+ string control_socket_header =
+ " ,\"control-socket\": { \n"
+ " \"socket-type\": \"unix\", \n"
+ " \"socket-name\": \"";
+ string control_socket_footer =
+ "\" \n} \n";
+ string logger_txt =
+ " ,\"loggers\": [ { \n"
+ " \"name\": \"kea\", \n"
+ " \"severity\": \"FATAL\", \n"
+ " \"output_options\": [{ \n"
+ " \"output\": \"/dev/null\" \n"
+ " }] \n"
+ " }] \n";
+
+ std::ostringstream os;
+
+ // Create a valid config with all the parts should parse
+ os << set_config_txt << ","
+ << args_txt
+ << dhcp4_cfg_txt
+ << subnet1
+ << subnet_footer
+ << option_def
+ << option_data
+ << control_socket_header
+ << socket_path_
+ << control_socket_footer
+ << logger_txt
+ << "}\n" // close dhcp4
+ << "}}";
+
+ // Send the config-set command
+ std::string response;
+ sendUnixCommand(os.str(), response);
+
+ // Verify the configuration was successful. The config contains random
+ // socket name (/tmp/kea-<value-changing-each-time>/kea4.sock), so the
+ // hash will be different each time. As such, we can do simplified checks:
+ // - verify the "result": 0 is there
+ // - verify the "text": "Configuration successful." is there
+ EXPECT_NE(response.find("\"result\": 0"), std::string::npos);
+ EXPECT_NE(response.find("\"text\": \"Configuration successful.\""), std::string::npos);
+
+ // Check that the config was indeed applied.
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ EXPECT_EQ(1, subnets->size());
+
+ OptionDefinitionPtr def =
+ LibDHCP::getRuntimeOptionDef(DHCP4_OPTION_SPACE, 163);
+ ASSERT_TRUE(def);
+
+ // Create a config with malformed subnet that should fail to parse.
+ os.str("");
+ os << set_config_txt << ","
+ << args_txt
+ << dhcp4_cfg_txt
+ << bad_subnet
+ << subnet_footer
+ << control_socket_header
+ << socket_path_
+ << control_socket_footer
+ << "}\n" // close dhcp4
+ << "}}";
+
+ // Send the config-set command
+ sendUnixCommand(os.str(), response);
+
+ // Should fail with a syntax error
+ EXPECT_EQ("{ \"result\": 1, "
+ "\"text\": \"subnet configuration failed: mandatory 'subnet' "
+ "parameter is missing for a subnet being configured (<wire>:19:17)\" }",
+ response);
+
+ // Check that the config was not lost
+ subnets = CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ EXPECT_EQ(1, subnets->size());
+
+ def = LibDHCP::getRuntimeOptionDef(DHCP4_OPTION_SPACE, 163);
+ ASSERT_TRUE(def);
+
+ // Create a valid config with two subnets and no command channel.
+ // It should succeed, client should still receive the response
+ os.str("");
+ os << set_config_txt << ","
+ << args_txt
+ << dhcp4_cfg_txt
+ << subnet1
+ << ",\n"
+ << subnet2
+ << subnet_footer
+ << "}\n" // close dhcp4
+ << "}}";
+
+ // Verify the control channel socket exists.
+ ASSERT_TRUE(fileExists(socket_path_));
+
+ // Send the config-set command.
+ sendUnixCommand(os.str(), response);
+
+ // Verify the control channel socket no longer exists.
+ EXPECT_FALSE(fileExists(socket_path_));
+
+ // With no command channel, should still receive the response. The config contains random
+ // socket name (/tmp/kea-<value-changing-each-time>/kea4.sock), so the
+ // hash will be different each time. As such, we can do simplified checks:
+ // - verify the "result": 0 is there
+ // - verify the "text": "Configuration successful." is there
+ EXPECT_NE(response.find("\"result\": 0"), std::string::npos);
+ EXPECT_NE(response.find("\"text\": \"Configuration successful.\""), std::string::npos);
+
+ // Check that the config was not lost
+ subnets = CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ EXPECT_EQ(2, subnets->size());
+
+ // Clean up after the test.
+ CfgMgr::instance().clear();
+}
+
+// Tests if the server returns its configuration using config-get.
+// Note there are separate tests that verify if toElement() called by the
+// config-get handler are actually converting the configuration correctly.
+TEST_F(CtrlChannelDhcpv4SrvTest, configGet) {
+ createUnixChannelServer();
+ std::string response;
+
+ sendUnixCommand("{ \"command\": \"config-get\" }", response);
+ ConstElementPtr rsp;
+
+ // The response should be a valid JSON.
+ EXPECT_NO_THROW(rsp = Element::fromJSON(response));
+ ASSERT_TRUE(rsp);
+
+ int status;
+ ConstElementPtr cfg = parseAnswer(status, rsp);
+ EXPECT_EQ(CONTROL_RESULT_SUCCESS, status);
+
+ // Ok, now roughly check if the response seems legit.
+ ASSERT_TRUE(cfg);
+ ASSERT_EQ(Element::map, cfg->getType());
+ EXPECT_TRUE(cfg->get("Dhcp4"));
+ EXPECT_TRUE(cfg->get("Dhcp4")->get("loggers"));
+}
+
+// Tests if the server returns the hash of its configuration using
+// config-hash-get.
+TEST_F(CtrlChannelDhcpv4SrvTest, configHashGet) {
+ createUnixChannelServer();
+ std::string response;
+
+ sendUnixCommand("{ \"command\": \"config-hash-get\" }", response);
+ ConstElementPtr rsp;
+
+ // The response should be a valid JSON.
+ EXPECT_NO_THROW(rsp = Element::fromJSON(response));
+ ASSERT_TRUE(rsp);
+
+ int status;
+ ConstElementPtr args = parseAnswer(status, rsp);
+ EXPECT_EQ(CONTROL_RESULT_SUCCESS, status);
+ // the parseAnswer is trying to be smart with ignoring hash.
+ // But this time we really want to see the hash, so we'll retrieve
+ // the arguments manually.
+ args = rsp->get(CONTROL_ARGUMENTS);
+
+ // Ok, now roughly check if the response seems legit.
+ ASSERT_TRUE(args);
+ ASSERT_EQ(Element::map, args->getType());
+ ConstElementPtr hash = args->get("hash");
+ ASSERT_TRUE(hash);
+ ASSERT_EQ(Element::string, hash->getType());
+ // SHA-256 -> 64 hex digits.
+ EXPECT_EQ(64, hash->stringValue().size());
+}
+
+// Verify that the "config-test" command will do what we expect.
+TEST_F(CtrlChannelDhcpv4SrvTest, configTest) {
+ createUnixChannelServer();
+
+ // Define strings to permutate the config arguments
+ // (Note the line feeds makes errors easy to find)
+ string set_config_txt = "{ \"command\": \"config-set\" \n";
+ string config_test_txt = "{ \"command\": \"config-test\" \n";
+ string args_txt = " \"arguments\": { \n";
+ string dhcp4_cfg_txt =
+ " \"Dhcp4\": { \n"
+ " \"interfaces-config\": { \n"
+ " \"interfaces\": [\"*\"] \n"
+ " }, \n"
+ " \"valid-lifetime\": 4000, \n"
+ " \"renew-timer\": 1000, \n"
+ " \"rebind-timer\": 2000, \n"
+ " \"lease-database\": { \n"
+ " \"type\": \"memfile\", \n"
+ " \"persist\":false, \n"
+ " \"lfc-interval\": 0 \n"
+ " }, \n"
+ " \"expired-leases-processing\": { \n"
+ " \"reclaim-timer-wait-time\": 0, \n"
+ " \"hold-reclaimed-time\": 0, \n"
+ " \"flush-reclaimed-timer-wait-time\": 0 \n"
+ " },"
+ " \"subnet4\": [ \n";
+ string subnet1 =
+ " {\"subnet\": \"192.2.0.0/24\", \"id\": 1, \n"
+ " \"pools\": [{ \"pool\": \"192.2.0.1-192.2.0.50\" }]}\n";
+ string subnet2 =
+ " {\"subnet\": \"192.2.1.0/24\", \"id\": 2, \n"
+ " \"pools\": [{ \"pool\": \"192.2.1.1-192.2.1.50\" }]}\n";
+ string bad_subnet =
+ " {\"comment\": \"192.2.2.0/24\", \"id\": 10, \n"
+ " \"pools\": [{ \"pool\": \"192.2.2.1-192.2.2.50\" }]}\n";
+ string subnet_footer =
+ " ] \n";
+ string control_socket_header =
+ " ,\"control-socket\": { \n"
+ " \"socket-type\": \"unix\", \n"
+ " \"socket-name\": \"";
+ string control_socket_footer =
+ "\" \n} \n";
+ string logger_txt =
+ " ,\"loggers\": [ { \n"
+ " \"name\": \"kea\", \n"
+ " \"severity\": \"FATAL\", \n"
+ " \"output_options\": [{ \n"
+ " \"output\": \"/dev/null\" \n"
+ " }] \n"
+ " }] \n";
+
+ std::ostringstream os;
+
+ // Create a valid config with all the parts should parse
+ os << set_config_txt << ","
+ << args_txt
+ << dhcp4_cfg_txt
+ << subnet1
+ << subnet_footer
+ << control_socket_header
+ << socket_path_
+ << control_socket_footer
+ << logger_txt
+ << "}\n" // close dhcp4
+ << "}}";
+
+ // Send the config-set command
+ std::string response;
+ sendUnixCommand(os.str(), response);
+
+ // Verify the configuration was successful. The config contains random
+ // socket name (/tmp/kea-<value-changing-each-time>/kea4.sock), so the
+ // hash will be different each time. As such, we can do simplified checks:
+ // - verify the "result": 0 is there
+ // - verify the "text": "Configuration successful." is there
+ EXPECT_NE(response.find("\"result\": 0"), std::string::npos);
+ EXPECT_NE(response.find("\"text\": \"Configuration successful.\""), std::string::npos);
+
+ // Check that the config was indeed applied.
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ EXPECT_EQ(1, subnets->size());
+
+ // Create a config with malformed subnet that should fail to parse.
+ os.str("");
+ os << config_test_txt << ","
+ << args_txt
+ << dhcp4_cfg_txt
+ << bad_subnet
+ << subnet_footer
+ << control_socket_header
+ << socket_path_
+ << control_socket_footer
+ << "}\n" // close dhcp4
+ << "}}";
+
+ // Send the config-test command
+ sendUnixCommand(os.str(), response);
+
+ // Should fail with a syntax error
+ EXPECT_EQ("{ \"result\": 1, "
+ "\"text\": \"subnet configuration failed: mandatory 'subnet' parameter "
+ "is missing for a subnet being configured (<wire>:19:17)\" }",
+ response);
+
+ // Check that the config was not lost
+ subnets = CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ EXPECT_EQ(1, subnets->size());
+
+ // Create a valid config with two subnets and no command channel.
+ os.str("");
+ os << config_test_txt << ","
+ << args_txt
+ << dhcp4_cfg_txt
+ << subnet1
+ << ",\n"
+ << subnet2
+ << subnet_footer
+ << "}\n" // close dhcp4
+ << "}}";
+
+ // Verify the control channel socket exists.
+ ASSERT_TRUE(fileExists(socket_path_));
+
+ // Send the config-test command.
+ sendUnixCommand(os.str(), response);
+
+ // Verify the control channel socket still exists.
+ EXPECT_TRUE(fileExists(socket_path_));
+
+ // Verify the configuration was successful.
+ EXPECT_EQ("{ \"result\": 0, \"text\": \"Configuration seems sane. "
+ "Control-socket, hook-libraries, and D2 configuration were "
+ "sanity checked, but not applied.\" }",
+ response);
+
+ // Check that the config was not applied.
+ subnets = CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ EXPECT_EQ(1, subnets->size());
+
+ // Clean up after the test.
+ CfgMgr::instance().clear();
+}
+
+// This test verifies that the DHCP server handles version-get commands
+TEST_F(CtrlChannelDhcpv4SrvTest, getVersion) {
+ createUnixChannelServer();
+
+ std::string response;
+
+ // Send the version-get command
+ sendUnixCommand("{ \"command\": \"version-get\" }", response);
+ EXPECT_TRUE(response.find("\"result\": 0") != string::npos);
+ EXPECT_TRUE(response.find("log4cplus") != string::npos);
+ EXPECT_FALSE(response.find("GTEST_VERSION") != string::npos);
+
+ // Send the build-report command
+ sendUnixCommand("{ \"command\": \"build-report\" }", response);
+ EXPECT_TRUE(response.find("\"result\": 0") != string::npos);
+ EXPECT_TRUE(response.find("GTEST_VERSION") != string::npos);
+}
+
+// This test verifies that the DHCP server handles server-tag-get command
+TEST_F(CtrlChannelDhcpv4SrvTest, serverTagGet) {
+ createUnixChannelServer();
+
+ std::string response;
+ std::string expected;
+
+ // Send the server-tag-get command
+ sendUnixCommand("{ \"command\": \"server-tag-get\" }", response);
+ expected = "{ \"arguments\": { \"server-tag\": \"\" }, \"result\": 0 }";
+ EXPECT_EQ(expected, response);
+
+ // Set a value to the server tag
+ CfgMgr::instance().getCurrentCfg()->setServerTag("foobar");
+
+ // Retry...
+ sendUnixCommand("{ \"command\": \"server-tag-get\" }", response);
+ expected = "{ \"arguments\": { \"server-tag\": \"foobar\" }, \"result\": 0 }";
+}
+
+// This test verifies that the DHCP server handles status-get commands
+TEST_F(CtrlChannelDhcpv4SrvTest, statusGet) {
+ createUnixChannelServer();
+
+ // start_ is initialized by init.
+ ASSERT_THROW(server_->init("/no/such/file"), BadValue);
+
+ std::string response_txt;
+
+ // Send the status-get command.
+ sendUnixCommand("{ \"command\": \"status-get\" }", response_txt);
+ ConstElementPtr response;
+ ASSERT_NO_THROW(response = Element::fromJSON(response_txt));
+ ASSERT_TRUE(response);
+ ASSERT_EQ(Element::map, response->getType());
+ EXPECT_EQ(2, response->size());
+ ConstElementPtr result = response->get("result");
+ ASSERT_TRUE(result);
+ ASSERT_EQ(Element::integer, result->getType());
+ EXPECT_EQ(0, result->intValue());
+ ConstElementPtr arguments = response->get("arguments");
+ ASSERT_EQ(Element::map, arguments->getType());
+
+ // The returned pid should be the pid of our process.
+ auto found_pid = arguments->get("pid");
+ ASSERT_TRUE(found_pid);
+ EXPECT_EQ(static_cast<int64_t>(getpid()), found_pid->intValue());
+
+ // It is hard to check the actual uptime (and reload) as it is based
+ // on current time. Let's just make sure it is within a reasonable
+ // range.
+ auto found_uptime = arguments->get("uptime");
+ ASSERT_TRUE(found_uptime);
+ EXPECT_LE(found_uptime->intValue(), 5);
+ EXPECT_GE(found_uptime->intValue(), 0);
+
+ auto found_reload = arguments->get("reload");
+ ASSERT_TRUE(found_reload);
+ EXPECT_LE(found_reload->intValue(), 5);
+ EXPECT_GE(found_reload->intValue(), 0);
+
+ auto found_multi_threading = arguments->get("multi-threading-enabled");
+ ASSERT_TRUE(found_multi_threading);
+ EXPECT_TRUE(found_multi_threading->boolValue());
+
+ auto found_thread_count = arguments->get("thread-pool-size");
+ ASSERT_TRUE(found_thread_count);
+ // The default value varies between systems.
+ // Let's just make sure it's a positive value.
+ EXPECT_LE(0, found_thread_count->intValue());
+
+ auto found_queue_size = arguments->get("packet-queue-size");
+ ASSERT_TRUE(found_queue_size);
+ EXPECT_EQ(64, found_queue_size->intValue());
+
+ auto found_queue_stats = arguments->get("packet-queue-statistics");
+ ASSERT_TRUE(found_queue_stats);
+ EXPECT_FALSE(found_queue_stats->str().empty());
+
+ MultiThreadingMgr::instance().setMode(true);
+ MultiThreadingMgr::instance().setThreadPoolSize(4);
+ MultiThreadingMgr::instance().setPacketQueueSize(64);
+ sendUnixCommand("{ \"command\": \"status-get\" }", response_txt);
+ ASSERT_NO_THROW(response = Element::fromJSON(response_txt));
+ ASSERT_TRUE(response);
+ ASSERT_EQ(Element::map, response->getType());
+ EXPECT_EQ(2, response->size());
+ result = response->get("result");
+ ASSERT_TRUE(result);
+ ASSERT_EQ(Element::integer, result->getType());
+ EXPECT_EQ(0, result->intValue());
+ arguments = response->get("arguments");
+ ASSERT_EQ(Element::map, arguments->getType());
+
+ // The returned pid should be the pid of our process.
+ found_pid = arguments->get("pid");
+ ASSERT_TRUE(found_pid);
+ EXPECT_EQ(static_cast<int64_t>(getpid()), found_pid->intValue());
+
+ // It is hard to check the actual uptime (and reload) as it is based
+ // on current time. Let's just make sure it is within a reasonable
+ // range.
+ found_uptime = arguments->get("uptime");
+ ASSERT_TRUE(found_uptime);
+ EXPECT_LE(found_uptime->intValue(), 5);
+ EXPECT_GE(found_uptime->intValue(), 0);
+
+ found_reload = arguments->get("reload");
+ ASSERT_TRUE(found_reload);
+ EXPECT_LE(found_reload->intValue(), 5);
+ EXPECT_GE(found_reload->intValue(), 0);
+
+ found_multi_threading = arguments->get("multi-threading-enabled");
+ ASSERT_TRUE(found_multi_threading);
+ EXPECT_TRUE(found_multi_threading->boolValue());
+
+ found_thread_count = arguments->get("thread-pool-size");
+ ASSERT_TRUE(found_thread_count);
+ EXPECT_EQ(found_thread_count->intValue(), 4);
+
+ found_queue_size = arguments->get("packet-queue-size");
+ ASSERT_TRUE(found_queue_size);
+ EXPECT_EQ(found_queue_size->intValue(), 64);
+
+ found_queue_stats = arguments->get("packet-queue-statistics");
+ ASSERT_TRUE(found_queue_stats);
+ ASSERT_EQ(Element::list, found_queue_stats->getType());
+ EXPECT_EQ(3, found_queue_stats->size());
+}
+
+// Checks that socket status exists in status-get responses.
+TEST_F(CtrlChannelDhcpv4SrvTest, statusGetSockets) {
+ // Create dummy interfaces to test socket status.
+ isc::dhcp::test::IfaceMgrTestConfig test_config(true);
+
+ // Send the status-get command.
+ createUnixChannelServer();
+ string response_text;
+ sendUnixCommand(R"({ "command": "status-get" })", response_text);
+ ConstElementPtr response;
+ ASSERT_NO_THROW(response = Element::fromJSON(response_text));
+ ASSERT_TRUE(response);
+ ASSERT_EQ(Element::map, response->getType());
+ ConstElementPtr result(response->get("result"));
+ ASSERT_TRUE(result);
+ ASSERT_EQ(Element::integer, result->getType());
+ EXPECT_EQ(0, result->intValue());
+ ConstElementPtr arguments(response->get("arguments"));
+ ASSERT_TRUE(arguments);
+ ASSERT_EQ(Element::map, arguments->getType());
+
+ ConstElementPtr sockets(arguments->get("sockets"));
+ ASSERT_TRUE(sockets);
+ ASSERT_EQ(Element::map, sockets->getType());
+
+ ConstElementPtr status(sockets->get("status"));
+ ASSERT_TRUE(status);
+ ASSERT_EQ(Element::string, status->getType());
+ EXPECT_EQ("ready", status->stringValue());
+
+ ConstElementPtr errors(sockets->get("errors"));
+ ASSERT_FALSE(errors);
+}
+
+// Checks that socket status includes errors in status-get responses.
+TEST_F(CtrlChannelDhcpv4SrvTest, statusGetSocketsErrors) {
+ // Create dummy interfaces to test socket status and add custom down and no-address interfaces.
+ isc::dhcp::test::IfaceMgrTestConfig test_config(true);
+ test_config.addIface("down_interface", 4);
+ test_config.setIfaceFlags("down_interface", FlagLoopback(false), FlagUp(false),
+ FlagRunning(true), FlagInactive4(false),
+ FlagInactive6(false));
+ test_config.addIface("no_address", 5);
+
+ // Send the status-get command.
+ createUnixChannelServer();
+ string response_text;
+ sendUnixCommand(R"({ "command": "status-get" })", response_text);
+ ConstElementPtr response;
+ ASSERT_NO_THROW(response = Element::fromJSON(response_text));
+ ASSERT_TRUE(response);
+ ASSERT_EQ(Element::map, response->getType());
+ ConstElementPtr result(response->get("result"));
+ ASSERT_TRUE(result);
+ ASSERT_EQ(Element::integer, result->getType());
+ EXPECT_EQ(0, result->intValue());
+ ConstElementPtr arguments(response->get("arguments"));
+ ASSERT_TRUE(arguments);
+ ASSERT_EQ(Element::map, arguments->getType());
+
+ ConstElementPtr sockets(arguments->get("sockets"));
+ ASSERT_TRUE(sockets);
+ ASSERT_EQ(Element::map, sockets->getType());
+
+ ConstElementPtr status(sockets->get("status"));
+ ASSERT_TRUE(status);
+ ASSERT_EQ(Element::string, status->getType());
+ EXPECT_EQ("failed", status->stringValue());
+
+ ConstElementPtr errors(sockets->get("errors"));
+ ASSERT_TRUE(errors);
+ ASSERT_EQ(Element::list, errors->getType());
+ ASSERT_EQ(2, errors->size());
+
+ ConstElementPtr error(errors->get(0));
+ ASSERT_TRUE(error);
+ ASSERT_EQ(Element::string, error->getType());
+ ASSERT_EQ("the interface down_interface is down", error->stringValue());
+
+ error = errors->get(1);
+ ASSERT_TRUE(error);
+ ASSERT_EQ(Element::string, error->getType());
+ ASSERT_EQ("the interface no_address has no usable IPv4 addresses configured",
+ error->stringValue());
+}
+
+// This test verifies that the DHCP server handles config-backend-pull command
+TEST_F(CtrlChannelDhcpv4SrvTest, configBackendPull) {
+ createUnixChannelServer();
+
+ std::string response;
+ std::string expected;
+
+ // Send the config-backend-pull command. Note there is no configured backed.
+ sendUnixCommand("{ \"command\": \"config-backend-pull\" }", response);
+ expected = "{ \"result\": 3, \"text\": \"No config backend.\" }";
+ EXPECT_EQ(expected, response);
+}
+
+// This test verifies that the DHCP server immediately reclaims expired
+// leases on leases-reclaim command
+TEST_F(CtrlChannelDhcpv4SrvTest, controlLeasesReclaim) {
+ createUnixChannelServer();
+
+ // Create expired leases. Leases are expired by 40 seconds ago
+ // (valid lifetime = 60, cltt = now - 100).
+ HWAddrPtr hwaddr0(new HWAddr(HWAddr::fromText("00:01:02:03:04:05")));
+ Lease4Ptr lease0(new Lease4(IOAddress("10.0.0.1"), hwaddr0,
+ ClientIdPtr(), 60,
+ time(NULL) - 100, SubnetID(1)));
+ HWAddrPtr hwaddr1(new HWAddr(HWAddr::fromText("01:02:03:04:05:06")));
+ Lease4Ptr lease1(new Lease4(IOAddress("10.0.0.2"), hwaddr1,
+ ClientIdPtr(), 60,
+ time(NULL) - 100, SubnetID(1)));
+
+ // Add leases to the database.
+ LeaseMgr& lease_mgr = LeaseMgrFactory::instance();
+ ASSERT_NO_THROW(lease_mgr.addLease(lease0));
+ ASSERT_NO_THROW(lease_mgr.addLease(lease1));
+
+ // Make sure they have been added.
+ ASSERT_TRUE(lease_mgr.getLease4(IOAddress("10.0.0.1")));
+ ASSERT_TRUE(lease_mgr.getLease4(IOAddress("10.0.0.2")));
+
+ // No arguments
+ std::string response;
+ sendUnixCommand("{ \"command\": \"leases-reclaim\" }", response);
+ EXPECT_EQ("{ \"result\": 1, \"text\": "
+ "\"Missing mandatory 'remove' parameter.\" }", response);
+
+ // Bad argument name
+ sendUnixCommand("{ \"command\": \"leases-reclaim\", "
+ "\"arguments\": { \"reclaim\": true } }", response);
+ EXPECT_EQ("{ \"result\": 1, \"text\": "
+ "\"Missing mandatory 'remove' parameter.\" }", response);
+
+ // Bad remove argument type
+ sendUnixCommand("{ \"command\": \"leases-reclaim\", "
+ "\"arguments\": { \"remove\": \"bogus\" } }", response);
+ EXPECT_EQ("{ \"result\": 1, \"text\": "
+ "\"'remove' parameter expected to be a boolean.\" }", response);
+
+ // Send the command
+ sendUnixCommand("{ \"command\": \"leases-reclaim\", "
+ "\"arguments\": { \"remove\": false } }", response);
+ EXPECT_EQ("{ \"result\": 0, \"text\": "
+ "\"Reclamation of expired leases is complete.\" }", response);
+
+ // Leases should be reclaimed, but not removed
+ ASSERT_NO_THROW(lease0 = lease_mgr.getLease4(IOAddress("10.0.0.1")));
+ ASSERT_NO_THROW(lease1 = lease_mgr.getLease4(IOAddress("10.0.0.2")));
+ ASSERT_TRUE(lease0);
+ ASSERT_TRUE(lease1);
+ EXPECT_TRUE(lease0->stateExpiredReclaimed());
+ EXPECT_TRUE(lease1->stateExpiredReclaimed());
+}
+
+// This test verifies that the DHCP server immediately reclaims expired
+// leases on leases-reclaim command with remove = true
+TEST_F(CtrlChannelDhcpv4SrvTest, controlLeasesReclaimRemove) {
+ createUnixChannelServer();
+
+ // Create expired leases. Leases are expired by 40 seconds ago
+ // (valid lifetime = 60, cltt = now - 100).
+ HWAddrPtr hwaddr0(new HWAddr(HWAddr::fromText("00:01:02:03:04:05")));
+ Lease4Ptr lease0(new Lease4(IOAddress("10.0.0.1"), hwaddr0,
+ ClientIdPtr(), 60,
+ time(NULL) - 100, SubnetID(1)));
+ HWAddrPtr hwaddr1(new HWAddr(HWAddr::fromText("01:02:03:04:05:06")));
+ Lease4Ptr lease1(new Lease4(IOAddress("10.0.0.2"), hwaddr1,
+ ClientIdPtr(), 60,
+ time(NULL) - 100, SubnetID(1)));
+
+ // Add leases to the database.
+ LeaseMgr& lease_mgr = LeaseMgrFactory::instance();
+ ASSERT_NO_THROW(lease_mgr.addLease(lease0));
+ ASSERT_NO_THROW(lease_mgr.addLease(lease1));
+
+ // Make sure they have been added.
+ ASSERT_TRUE(lease_mgr.getLease4(IOAddress("10.0.0.1")));
+ ASSERT_TRUE(lease_mgr.getLease4(IOAddress("10.0.0.2")));
+
+ // Send the command
+ std::string response;
+ sendUnixCommand("{ \"command\": \"leases-reclaim\", "
+ "\"arguments\": { \"remove\": true } }", response);
+ EXPECT_EQ("{ \"result\": 0, \"text\": "
+ "\"Reclamation of expired leases is complete.\" }", response);
+
+ // Leases should have been removed.
+ ASSERT_NO_THROW(lease0 = lease_mgr.getLease4(IOAddress("10.0.0.1")));
+ ASSERT_NO_THROW(lease1 = lease_mgr.getLease4(IOAddress("10.0.0.2")));
+ ASSERT_FALSE(lease0);
+ ASSERT_FALSE(lease1);
+}
+
+// Tests that the server properly responds to shutdown command sent
+// via ControlChannel
+TEST_F(CtrlChannelDhcpv4SrvTest, listCommands) {
+ createUnixChannelServer();
+ std::string response;
+
+ sendUnixCommand("{ \"command\": \"list-commands\" }", response);
+
+ ConstElementPtr rsp;
+ EXPECT_NO_THROW(rsp = Element::fromJSON(response));
+
+ // We expect the server to report at least the following commands:
+ checkListCommands(rsp, "build-report");
+ checkListCommands(rsp, "config-backend-pull");
+ checkListCommands(rsp, "config-get");
+ checkListCommands(rsp, "config-hash-get");
+ checkListCommands(rsp, "config-reload");
+ checkListCommands(rsp, "config-set");
+ checkListCommands(rsp, "config-test");
+ checkListCommands(rsp, "config-write");
+ checkListCommands(rsp, "list-commands");
+ checkListCommands(rsp, "leases-reclaim");
+ checkListCommands(rsp, "libreload");
+ checkListCommands(rsp, "version-get");
+ checkListCommands(rsp, "server-tag-get");
+ checkListCommands(rsp, "shutdown");
+ checkListCommands(rsp, "statistic-get");
+ checkListCommands(rsp, "statistic-get-all");
+ checkListCommands(rsp, "statistic-remove");
+ checkListCommands(rsp, "statistic-remove-all");
+ checkListCommands(rsp, "statistic-reset");
+ checkListCommands(rsp, "statistic-reset-all");
+ checkListCommands(rsp, "statistic-sample-age-set");
+ checkListCommands(rsp, "statistic-sample-age-set-all");
+ checkListCommands(rsp, "statistic-sample-count-set");
+ checkListCommands(rsp, "statistic-sample-count-set-all");
+}
+
+// Tests if config-write can be called without any parameters.
+TEST_F(CtrlChannelDhcpv4SrvTest, configWriteNoFilename) {
+ createUnixChannelServer();
+ std::string response;
+
+ // This is normally set by the command line -c parameter.
+ server_->setConfigFile("test1.json");
+
+ // If the filename is not explicitly specified, the name used
+ // in -c command line switch is used.
+ sendUnixCommand("{ \"command\": \"config-write\" }", response);
+
+ checkConfigWrite(response, CONTROL_RESULT_SUCCESS, "test1.json");
+ ::remove("test1.json");
+}
+
+// Tests if config-write can be called with a valid filename as parameter.
+TEST_F(CtrlChannelDhcpv4SrvTest, configWriteFilename) {
+ createUnixChannelServer();
+ std::string response;
+
+ sendUnixCommand("{ \"command\": \"config-write\", "
+ "\"arguments\": { \"filename\": \"test2.json\" } }", response);
+
+ checkConfigWrite(response, CONTROL_RESULT_SUCCESS, "test2.json");
+ ::remove("test2.json");
+}
+
+// Tests if config-reload attempts to reload a file and reports that the
+// file is missing.
+TEST_F(CtrlChannelDhcpv4SrvTest, configReloadMissingFile) {
+ createUnixChannelServer();
+ std::string response;
+
+ // This is normally set to whatever value is passed to -c when the server is
+ // started, but we're not starting it that way, so need to set it by hand.
+ server_->setConfigFile("test6.json");
+
+ // Tell the server to reload its configuration. It should attempt to load
+ // test6.json (and fail, because the file is not there).
+ sendUnixCommand("{ \"command\": \"config-reload\" }", response);
+
+ // Verify the reload was rejected.
+ EXPECT_EQ("{ \"result\": 1, \"text\": \"Config reload failed: "
+ "configuration error using file 'test6.json': Unable to open file "
+ "test6.json\" }",
+ response);
+}
+
+// Tests if config-reload attempts to reload a file and reports that the
+// file is not a valid JSON.
+TEST_F(CtrlChannelDhcpv4SrvTest, configReloadBrokenFile) {
+ createUnixChannelServer();
+ std::string response;
+
+ // This is normally set to whatever value is passed to -c when the server is
+ // started, but we're not starting it that way, so need to set it by hand.
+ server_->setConfigFile("test7.json");
+
+ // Although Kea is smart, its AI routines are not smart enough to handle
+ // this one... at least not yet.
+ ofstream f("test7.json", ios::trunc);
+ f << "gimme some addrs, bro!";
+ f.close();
+
+ // Now tell Kea to reload its config.
+ sendUnixCommand("{ \"command\": \"config-reload\" }", response);
+
+ // Verify the reload will fail.
+ EXPECT_EQ("{ \"result\": 1, \"text\": \"Config reload failed: "
+ "configuration error using file 'test7.json': "
+ "test7.json:1.1: Invalid character: g\" }",
+ response);
+
+ ::remove("test7.json");
+}
+
+// Tests if config-reload attempts to reload a file and reports that the
+// file is loaded correctly.
+TEST_F(CtrlChannelDhcpv4SrvTest, configReloadValid) {
+ createUnixChannelServer();
+ std::string response;
+
+ // This is normally set to whatever value is passed to -c when the server is
+ // started, but we're not starting it that way, so need to set it by hand.
+ server_->setConfigFile("test8.json");
+
+ // Ok, enough fooling around. Let's create a valid config.
+ const std::string cfg_txt =
+ "{ \"Dhcp4\": {"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"subnet4\": ["
+ " { \"id\": 1, \"subnet\": \"192.0.2.0/24\" },"
+ " { \"id\": 2, \"subnet\": \"192.0.3.0/24\" }"
+ " ],"
+ " \"valid-lifetime\": 4000,"
+ " \"lease-database\": {"
+ " \"type\": \"memfile\", \"persist\": false }"
+ "} }";
+ ofstream f("test8.json", ios::trunc);
+ f << cfg_txt;
+ f.close();
+
+ // This command should reload test8.json config.
+ sendUnixCommand("{ \"command\": \"config-reload\" }", response);
+
+ // Verify the configuration was successful. The config contains random
+ // socket name (/tmp/kea-<value-changing-each-time>/kea4.sock), so the
+ // hash will be different each time. As such, we can do simplified checks:
+ // - verify the "result": 0 is there
+ // - verify the "text": "Configuration successful." is there
+ EXPECT_NE(response.find("\"result\": 0"), std::string::npos);
+ EXPECT_NE(response.find("\"text\": \"Configuration successful.\""), std::string::npos);
+
+ // Check that the config was indeed applied.
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ EXPECT_EQ(2, subnets->size());
+
+ ::remove("test8.json");
+}
+
+// Tests if config-reload attempts to reload a file and reports that the
+// file is loaded correctly.
+TEST_F(CtrlChannelDhcpv4SrvTest, configReloadDetectInterfaces) {
+ interfaces_ = "\"eth0\"";
+ IfacePtr eth0 = IfaceMgrTestConfig::createIface("eth0", ETH0_INDEX,
+ "11:22:33:44:55:66");
+ auto detectIfaces = [&](bool update_only) {
+ if (!update_only) {
+ eth0->addAddress(IOAddress("10.0.0.1"));
+ eth0->addAddress(IOAddress("fe80::3a60:77ff:fed5:cdef"));
+ eth0->addAddress(IOAddress("2001:db8:1::1"));
+ IfaceMgr::instance().addInterface(eth0);
+ }
+ return (false);
+ };
+ IfaceMgr::instance().setDetectCallback(detectIfaces);
+ IfaceMgr::instance().clearIfaces();
+ IfaceMgr::instance().closeSockets();
+ IfaceMgr::instance().detectIfaces();
+ createUnixChannelServer();
+ std::string response;
+
+ // This is normally set to whatever value is passed to -c when the server is
+ // started, but we're not starting it that way, so need to set it by hand.
+ server_->setConfigFile("test8.json");
+
+ // Ok, enough fooling around. Let's create a valid config.
+ const std::string cfg_txt =
+ "{ \"Dhcp4\": {"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"eth1\" ]"
+ " },"
+ " \"subnet4\": ["
+ " { \"id\": 1, \"subnet\": \"192.0.2.0/24\" },"
+ " { \"id\": 2, \"subnet\": \"192.0.3.0/24\" }"
+ " ],"
+ " \"valid-lifetime\": 4000,"
+ " \"lease-database\": {"
+ " \"type\": \"memfile\", \"persist\": false }"
+ "} }";
+ ofstream f("test8.json", ios::trunc);
+ f << cfg_txt;
+ f.close();
+
+ IfacePtr eth1 = IfaceMgrTestConfig::createIface("eth1", ETH1_INDEX,
+ "AA:BB:CC:DD:EE:FF");
+ auto detectUpdateIfaces = [&](bool update_only) {
+ if (!update_only) {
+ eth1->addAddress(IOAddress("192.0.2.3"));
+ eth1->addAddress(IOAddress("fe80::3a60:77ff:fed5:abcd"));
+ eth1->addAddress(IOAddress("3001:db8:100::1"));
+ IfaceMgr::instance().addInterface(eth1);
+ }
+ return (false);
+ };
+ IfaceMgr::instance().setDetectCallback(detectUpdateIfaces);
+
+ // This command should reload test8.json config.
+ sendUnixCommand("{ \"command\": \"config-reload\" }", response);
+
+ // Verify the configuration was successful. The config contains random
+ // socket name (/tmp/kea-<value-changing-each-time>/kea4.sock), so the
+ // hash will be different each time. As such, we can do simplified checks:
+ // - verify the "result": 0 is there
+ // - verify the "text": "Configuration successful." is there
+ EXPECT_NE(response.find("\"result\": 0"), std::string::npos);
+ EXPECT_NE(response.find("\"text\": \"Configuration successful.\""), std::string::npos);
+
+ // Check that the config was indeed applied.
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ EXPECT_EQ(2, subnets->size());
+
+ ::remove("test8.json");
+}
+
+// This test verifies that disable DHCP service command performs sanity check on
+// parameters.
+TEST_F(CtrlChannelDhcpv4SrvTest, dhcpDisableBadParam) {
+ createUnixChannelServer();
+ std::string response;
+
+ sendUnixCommand("{"
+ " \"command\": \"dhcp-disable\","
+ " \"arguments\": {"
+ " \"max-period\": -3"
+ " }"
+ "}", response);
+ ConstElementPtr rsp;
+
+ // The response should be a valid JSON.
+ EXPECT_NO_THROW(rsp = Element::fromJSON(response));
+ ASSERT_TRUE(rsp);
+
+ EXPECT_EQ("{ \"result\": 1, \"text\": \"'max-period' must be positive "
+ "integer\" }", response);
+
+ sendUnixCommand("{"
+ " \"command\": \"dhcp-disable\","
+ " \"arguments\": {"
+ " \"origin\": \"\""
+ " }"
+ "}", response);
+
+ // The response should be a valid JSON.
+ EXPECT_NO_THROW(rsp = Element::fromJSON(response));
+ ASSERT_TRUE(rsp);
+
+ EXPECT_EQ("{ \"result\": 1, \"text\": \"invalid value used for 'origin' "
+ "parameter: (empty string)\" }", response);
+
+ sendUnixCommand("{"
+ " \"command\": \"dhcp-disable\","
+ " \"arguments\": {"
+ " \"origin\": \"test\""
+ " }"
+ "}", response);
+
+ // The response should be a valid JSON.
+ EXPECT_NO_THROW(rsp = Element::fromJSON(response));
+ ASSERT_TRUE(rsp);
+
+ EXPECT_EQ("{ \"result\": 1, \"text\": \"invalid value used for 'origin' "
+ "parameter: test\" }", response);
+}
+
+// This test verifies if it is possible to disable DHCP service via command.
+TEST_F(CtrlChannelDhcpv4SrvTest, dhcpDisable) {
+ createUnixChannelServer();
+ std::string response;
+
+ sendUnixCommand("{ \"command\": \"dhcp-disable\" }", response);
+ ConstElementPtr rsp;
+
+ // The response should be a valid JSON.
+ EXPECT_NO_THROW(rsp = Element::fromJSON(response));
+ ASSERT_TRUE(rsp);
+
+ int status;
+ ConstElementPtr cfg = parseAnswer(status, rsp);
+ EXPECT_EQ(CONTROL_RESULT_SUCCESS, status);
+
+ EXPECT_FALSE(server_->network_state_->isServiceEnabled());
+
+ server_->network_state_->enableService(NetworkState::Origin::USER_COMMAND);
+
+ EXPECT_TRUE(server_->network_state_->isServiceEnabled());
+
+ sendUnixCommand("{"
+ " \"command\": \"dhcp-disable\","
+ " \"arguments\": {"
+ " \"origin\": \"user\""
+ " }"
+ "}", response);
+
+ // The response should be a valid JSON.
+ EXPECT_NO_THROW(rsp = Element::fromJSON(response));
+ ASSERT_TRUE(rsp);
+
+ cfg = parseAnswer(status, rsp);
+ EXPECT_EQ(CONTROL_RESULT_SUCCESS, status);
+
+ EXPECT_FALSE(server_->network_state_->isServiceEnabled());
+
+ server_->network_state_->enableService(NetworkState::Origin::USER_COMMAND);
+
+ EXPECT_TRUE(server_->network_state_->isServiceEnabled());
+
+ sendUnixCommand("{"
+ " \"command\": \"dhcp-disable\","
+ " \"arguments\": {"
+ " \"origin\": \"ha-partner\""
+ " }"
+ "}", response);
+
+ // The response should be a valid JSON.
+ EXPECT_NO_THROW(rsp = Element::fromJSON(response));
+ ASSERT_TRUE(rsp);
+
+ cfg = parseAnswer(status, rsp);
+ EXPECT_EQ(CONTROL_RESULT_SUCCESS, status);
+
+ EXPECT_FALSE(server_->network_state_->isServiceEnabled());
+
+ server_->network_state_->enableService(NetworkState::Origin::HA_COMMAND);
+
+ EXPECT_TRUE(server_->network_state_->isServiceEnabled());
+}
+
+// This test verifies that it is possible to disable DHCP service for a short
+// period of time, after which the service is automatically enabled.
+TEST_F(CtrlChannelDhcpv4SrvTest, dhcpDisableTemporarily) {
+ createUnixChannelServer();
+ std::string response;
+
+ // Send a command to disable DHCP service for 3 seconds.
+ sendUnixCommand("{"
+ " \"command\": \"dhcp-disable\","
+ " \"arguments\": {"
+ " \"max-period\": 3"
+ " }"
+ "}", response);
+ ConstElementPtr rsp;
+
+ // The response should be a valid JSON.
+ EXPECT_NO_THROW(rsp = Element::fromJSON(response));
+ ASSERT_TRUE(rsp);
+
+ int status;
+ ConstElementPtr cfg = parseAnswer(status, rsp);
+ EXPECT_EQ(CONTROL_RESULT_SUCCESS, status);
+
+ // The service should be disabled.
+ EXPECT_FALSE(server_->network_state_->isServiceEnabled());
+ // And the timer should be scheduled which counts the time to automatic
+ // enabling of the service.
+ EXPECT_TRUE(server_->network_state_->isDelayedEnableAll());
+}
+
+// This test verifies that enable DHCP service command performs sanity check on
+// parameters.
+TEST_F(CtrlChannelDhcpv4SrvTest, dhcpEnableBadParam) {
+ createUnixChannelServer();
+ std::string response;
+
+ sendUnixCommand("{"
+ " \"command\": \"dhcp-enable\","
+ " \"arguments\": {"
+ " \"origin\": \"\""
+ " }"
+ "}", response);
+ ConstElementPtr rsp;
+
+ // The response should be a valid JSON.
+ EXPECT_NO_THROW(rsp = Element::fromJSON(response));
+ ASSERT_TRUE(rsp);
+
+ EXPECT_EQ("{ \"result\": 1, \"text\": \"invalid value used for 'origin' "
+ "parameter: (empty string)\" }", response);
+
+ sendUnixCommand("{"
+ " \"command\": \"dhcp-enable\","
+ " \"arguments\": {"
+ " \"origin\": \"test\""
+ " }"
+ "}", response);
+
+ // The response should be a valid JSON.
+ EXPECT_NO_THROW(rsp = Element::fromJSON(response));
+ ASSERT_TRUE(rsp);
+
+ EXPECT_EQ("{ \"result\": 1, \"text\": \"invalid value used for 'origin' "
+ "parameter: test\" }", response);
+}
+
+// This test verifies if it is possible to enable DHCP service via command.
+TEST_F(CtrlChannelDhcpv4SrvTest, dhcpEnable) {
+ createUnixChannelServer();
+ std::string response;
+
+ sendUnixCommand("{ \"command\": \"dhcp-enable\" }", response);
+ ConstElementPtr rsp;
+
+ // The response should be a valid JSON.
+ EXPECT_NO_THROW(rsp = Element::fromJSON(response));
+ ASSERT_TRUE(rsp);
+
+ int status;
+ ConstElementPtr cfg = parseAnswer(status, rsp);
+ EXPECT_EQ(CONTROL_RESULT_SUCCESS, status);
+
+ EXPECT_TRUE(server_->network_state_->isServiceEnabled());
+
+ server_->network_state_->disableService(NetworkState::Origin::USER_COMMAND);
+
+ EXPECT_FALSE(server_->network_state_->isServiceEnabled());
+
+ sendUnixCommand("{"
+ " \"command\": \"dhcp-enable\","
+ " \"arguments\": {"
+ " \"origin\": \"user\""
+ " }"
+ "}", response);
+
+ // The response should be a valid JSON.
+ EXPECT_NO_THROW(rsp = Element::fromJSON(response));
+ ASSERT_TRUE(rsp);
+
+ cfg = parseAnswer(status, rsp);
+ EXPECT_EQ(CONTROL_RESULT_SUCCESS, status);
+
+ EXPECT_TRUE(server_->network_state_->isServiceEnabled());
+
+ server_->network_state_->disableService(NetworkState::Origin::HA_COMMAND);
+
+ EXPECT_FALSE(server_->network_state_->isServiceEnabled());
+
+ sendUnixCommand("{"
+ " \"command\": \"dhcp-enable\","
+ " \"arguments\": {"
+ " \"origin\": \"ha-partner\""
+ " }"
+ "}", response);
+
+ // The response should be a valid JSON.
+ EXPECT_NO_THROW(rsp = Element::fromJSON(response));
+ ASSERT_TRUE(rsp);
+
+ cfg = parseAnswer(status, rsp);
+ EXPECT_EQ(CONTROL_RESULT_SUCCESS, status);
+
+ EXPECT_TRUE(server_->network_state_->isServiceEnabled());
+}
+
+/// Verify that concurrent connections over the control channel can be
+/// established.
+/// @todo Future Kea 1.3 tickets will modify the behavior of the CommandMgr
+/// such that the server will be able to send response in multiple chunks.
+/// This test will need to be extended. For now, the receive and write
+/// operations are atomic and there is no conflict between concurrent
+/// connections.
+TEST_F(CtrlChannelDhcpv4SrvTest, concurrentConnections) {
+ createUnixChannelServer();
+
+ boost::scoped_ptr<UnixControlClient> client1(new UnixControlClient());
+ ASSERT_TRUE(client1);
+
+ boost::scoped_ptr<UnixControlClient> client2(new UnixControlClient());
+ ASSERT_TRUE(client2);
+
+ // Client 1 connects.
+ ASSERT_TRUE(client1->connectToServer(socket_path_));
+ ASSERT_NO_THROW(getIOService()->poll());
+
+ // Client 2 connects.
+ ASSERT_TRUE(client2->connectToServer(socket_path_));
+ ASSERT_NO_THROW(getIOService()->poll());
+
+ // Send the command while another client is connected.
+ ASSERT_TRUE(client2->sendCommand("{ \"command\": \"list-commands\" }"));
+ ASSERT_NO_THROW(getIOService()->poll());
+
+ std::string response;
+ // The server should respond ok.
+ ASSERT_TRUE(client2->getResponse(response));
+ EXPECT_TRUE(response.find("\"result\": 0") != std::string::npos);
+
+ // Disconnect the servers.
+ client1->disconnectFromServer();
+ client2->disconnectFromServer();
+ ASSERT_NO_THROW(getIOService()->poll());
+}
+
+// This test verifies that the server can receive and process a large command.
+TEST_F(CtrlChannelDhcpv4SrvTest, longCommand) {
+
+ std::ostringstream command;
+
+ // This is the desired size of the command sent to the server (1MB). The
+ // actual size sent will be slightly greater than that.
+ const size_t command_size = 1024 * 1000;
+
+ while (command.tellp() < command_size) {
+
+ // We're sending command 'foo' with arguments being a list of
+ // strings. If this is the first transmission, send command name
+ // and open the arguments list. Also insert the first argument
+ // so as all subsequent arguments can be prefixed with a comma.
+ if (command.tellp() == 0) {
+ command << "{ \"command\": \"foo\", \"arguments\": [ \"begin\"";
+
+ } else {
+ // Generate a random number and insert it into the stream as
+ // 10 digits long string.
+ std::ostringstream arg;
+ arg << setw(10) << std::rand();
+ // Append the argument in the command.
+ command << ", \"" << arg.str() << "\"\n";
+
+ // If we have hit the limit of the command size, close braces to
+ // get appropriate JSON.
+ if (command.tellp() > command_size) {
+ command << "] }";
+ }
+ }
+ }
+
+ ASSERT_NO_THROW(
+ CommandMgr::instance().registerCommand("foo",
+ std::bind(&CtrlChannelDhcpv4SrvTest::longCommandHandler,
+ command.str(), ph::_1, ph::_2));
+ );
+
+ createUnixChannelServer();
+
+ std::string response;
+ std::thread th([this, &response, &command]() {
+
+ // IO service will be stopped automatically when this object goes
+ // out of scope and is destroyed. This is useful because we use
+ // asserts which may break the thread in various exit points.
+ IOServiceWork work(getIOService());
+
+ // Create client which we will use to send command to the server.
+ boost::scoped_ptr<UnixControlClient> client(new UnixControlClient());
+ ASSERT_TRUE(client);
+
+ // Connect to the server. This will trigger acceptor handler on the
+ // server side and create a new connection.
+ ASSERT_TRUE(client->connectToServer(socket_path_));
+
+ // Initially the remaining_string holds the entire command and we
+ // will be erasing the portions that we have sent.
+ std::string remaining_data = command.str();
+ while (!remaining_data.empty()) {
+ // Send the command in chunks of 1024 bytes.
+ const size_t l = remaining_data.size() < 1024 ? remaining_data.size() : 1024;
+ ASSERT_TRUE(client->sendCommand(remaining_data.substr(0, l)));
+ remaining_data.erase(0, l);
+ }
+
+ // Set timeout to 5 seconds to allow the time for the server to send
+ // a response.
+ const unsigned int timeout = 5;
+ ASSERT_TRUE(client->getResponse(response, timeout));
+
+ // We're done. Close the connection to the server.
+ client->disconnectFromServer();
+ });
+
+ // Run the server until the command has been processed and response
+ // received.
+ getIOService()->run();
+
+ // Wait for the thread to complete.
+ th.join();
+
+ EXPECT_EQ("{ \"result\": 0, \"text\": \"long command received ok\" }",
+ response);
+}
+
+// This test verifies that the server can send long response to the client.
+TEST_F(CtrlChannelDhcpv4SrvTest, longResponse) {
+ // We need to generate large response. The simplest way is to create
+ // a command and a handler which will generate some static response
+ // of a desired size.
+ ASSERT_NO_THROW(
+ CommandMgr::instance().registerCommand("foo",
+ std::bind(&CtrlChannelDhcpv4SrvTest::longResponseHandler, ph::_1, ph::_2));
+ );
+
+ createUnixChannelServer();
+
+ // The UnixControlClient doesn't have any means to check that the entire
+ // response has been received. What we want to do is to generate a
+ // reference response using our command handler and then compare
+ // what we have received over the unix domain socket with this reference
+ // response to figure out when to stop receiving.
+ std::string reference_response = longResponseHandler("foo", ConstElementPtr())->str();
+
+ // In this stream we're going to collect out partial responses.
+ std::ostringstream response;
+
+ // The client is synchronous so it is useful to run it in a thread.
+ std::thread th([this, &response, reference_response]() {
+
+ // IO service will be stopped automatically when this object goes
+ // out of scope and is destroyed. This is useful because we use
+ // asserts which may break the thread in various exit points.
+ IOServiceWork work(getIOService());
+
+ // Remember the response size so as we know when we should stop
+ // receiving.
+ const size_t long_response_size = reference_response.size();
+
+ // Create the client and connect it to the server.
+ boost::scoped_ptr<UnixControlClient> client(new UnixControlClient());
+ ASSERT_TRUE(client);
+ ASSERT_TRUE(client->connectToServer(socket_path_));
+
+ // Send the stub command.
+ std::string command = "{ \"command\": \"foo\", \"arguments\": { } }";
+ ASSERT_TRUE(client->sendCommand(command));
+
+ // Keep receiving response data until we have received the full answer.
+ while (response.tellp() < long_response_size) {
+ std::string partial;
+ const unsigned int timeout = 5;
+ ASSERT_TRUE(client->getResponse(partial, timeout));
+ response << partial;
+ }
+
+ // We have received the entire response, so close the connection and
+ // stop the IO service.
+ client->disconnectFromServer();
+ });
+
+ // Run the server until the entire response has been received.
+ getIOService()->run();
+
+ // Wait for the thread to complete.
+ th.join();
+
+ // Make sure we have received correct response.
+ EXPECT_EQ(reference_response, response.str());
+}
+
+// This test verifies that the server signals timeout if the transmission
+// takes too long, having received a partial command.
+TEST_F(CtrlChannelDhcpv4SrvTest, connectionTimeoutPartialCommand) {
+ createUnixChannelServer();
+
+ // Set connection timeout to 2s to prevent long waiting time for the
+ // timeout during this test.
+ const unsigned short timeout = 2000;
+ CommandMgr::instance().setConnectionTimeout(timeout);
+
+ // Server's response will be assigned to this variable.
+ std::string response;
+
+ // It is useful to create a thread and run the server and the client
+ // at the same time and independently.
+ std::thread th([this, &response]() {
+
+ // IO service will be stopped automatically when this object goes
+ // out of scope and is destroyed. This is useful because we use
+ // asserts which may break the thread in various exit points.
+ IOServiceWork work(getIOService());
+
+ // Create the client and connect it to the server.
+ boost::scoped_ptr<UnixControlClient> client(new UnixControlClient());
+ ASSERT_TRUE(client);
+ ASSERT_TRUE(client->connectToServer(socket_path_));
+
+ // Send partial command. The server will be waiting for the remaining
+ // part to be sent and will eventually signal a timeout.
+ std::string command = "{ \"command\": \"foo\" ";
+ ASSERT_TRUE(client->sendCommand(command));
+
+ // Let's wait up to 15s for the server's response. The response
+ // should arrive sooner assuming that the timeout mechanism for
+ // the server is working properly.
+ const unsigned int timeout = 15;
+ ASSERT_TRUE(client->getResponse(response, timeout));
+
+ // Explicitly close the client's connection.
+ client->disconnectFromServer();
+ });
+
+ // Run the server until stopped.
+ getIOService()->run();
+
+ // Wait for the thread to return.
+ th.join();
+
+ // Check that the server has signalled a timeout.
+ EXPECT_EQ("{ \"result\": 1, \"text\": "
+ "\"Connection over control channel timed out, "
+ "discarded partial command of 19 bytes\" }", response);
+}
+
+// This test verifies that the server signals timeout if the transmission
+// takes too long, having received no data from the client.
+TEST_F(CtrlChannelDhcpv4SrvTest, connectionTimeoutNoData) {
+ createUnixChannelServer();
+
+ // Set connection timeout to 2s to prevent long waiting time for the
+ // timeout during this test.
+ const unsigned short timeout = 2000;
+ CommandMgr::instance().setConnectionTimeout(timeout);
+
+ // Server's response will be assigned to this variable.
+ std::string response;
+
+ // It is useful to create a thread and run the server and the client
+ // at the same time and independently.
+ std::thread th([this, &response]() {
+
+ // IO service will be stopped automatically when this object goes
+ // out of scope and is destroyed. This is useful because we use
+ // asserts which may break the thread in various exit points.
+ IOServiceWork work(getIOService());
+
+ // Create the client and connect it to the server.
+ boost::scoped_ptr<UnixControlClient> client(new UnixControlClient());
+ ASSERT_TRUE(client);
+ ASSERT_TRUE(client->connectToServer(socket_path_));
+
+ // Having sent nothing let's just wait and see if Server times us out.
+ // Let's wait up to 15s for the server's response. The response
+ // should arrive sooner assuming that the timeout mechanism for
+ // the server is working properly.
+ const unsigned int timeout = 15;
+ ASSERT_TRUE(client->getResponse(response, timeout));
+
+ // Explicitly close the client's connection.
+ client->disconnectFromServer();
+ });
+
+ // Run the server until stopped.
+ getIOService()->run();
+
+ // Wait for the thread to return.
+ th.join();
+
+ // Check that the server has signalled a timeout.
+ EXPECT_EQ("{ \"result\": 1, \"text\": "
+ "\"Connection over control channel timed out\" }", response);
+}
+
+} // End of anonymous namespace
diff --git a/src/bin/dhcp4/tests/d2_unittest.cc b/src/bin/dhcp4/tests/d2_unittest.cc
new file mode 100644
index 0000000..ba82087
--- /dev/null
+++ b/src/bin/dhcp4/tests/d2_unittest.cc
@@ -0,0 +1,529 @@
+// Copyright (C) 2014-2023 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <dhcp/iface_mgr.h>
+#include <dhcp4/json_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, "
+ " \"use-conflict-resolution\" : true "
+ "}";
+
+ return (dhcp_ddns::NameChangeRequest::fromJSON(stream.str()));
+}
+
+void
+Dhcp4SrvD2Test::reset() {
+ CfgMgr::instance().clear();
+
+ std::string config = "{ \"interfaces-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& server_ip,
+ const size_t port,
+ const std::string& sender_ip,
+ const size_t sender_port,
+ const size_t max_queue_size) {
+ std::ostringstream config;
+ config <<
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"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\" : \"" << server_ip << "\", "
+ " \"server-port\" : " << port << ", "
+ " \"sender-ip\" : \"" << sender_ip << "\", "
+ " \"sender-port\" : " << sender_port << ", "
+ " \"max-queue-size\" : " << max_queue_size << ", "
+ " \"ncr-protocol\" : \"UDP\", "
+ " \"ncr-format\" : \"JSON\", "
+ " \"override-no-update\" : true, "
+ " \"override-client-update\" : true, "
+ " \"replace-client-name\" : \"when-present\", "
+ " \"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) << "parse comment: " << comment->stringValue();
+ } else {
+ ASSERT_EQ(1, rcode) << "parse comment: " << comment->stringValue();
+ }
+
+ if (rcode == 0) {
+ CfgMgr::instance().commit();
+ }
+}
+
+// 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(mgr.clearQueue());
+ EXPECT_EQ(0, mgr.getQueueSize());
+ 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, which it does under all OSes
+// except Solaris 11.
+/// @todo Eventually we should find a way to test this under Solaris.
+#ifndef OS_SOLARIS
+TEST_F(Dhcp4SrvD2Test, forceUDPSendFailure) {
+#else
+TEST_F(Dhcp4SrvD2Test, DISABLED_forceUDPSendFailure) {
+#endif
+ // 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.
+ // Pass in a non-zero sender port to avoid validation error when
+ // server-ip/port are same as sender-ip/port
+ ASSERT_NO_FATAL_FAILURE(configureD2(true, SHOULD_PASS, "0.0.0.0", 0,
+ "0.0.0.0", 53001));
+ ASSERT_TRUE(mgr.ddnsEnabled());
+ ASSERT_NO_THROW(mgr.clearQueue());
+ EXPECT_EQ(0, mgr.getQueueSize());
+ try {
+ srv_.startD2();
+ } catch (const std::exception& ex) {
+ FAIL() << "startD2 failed with " << ex.what();
+ } catch (...) {
+ FAIL() << "startD2 failed";
+ }
+ 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(mgr.clearQueue());
+ EXPECT_EQ(0, mgr.getQueueSize());
+ 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());
+}
+
+// Tests invalid config with TCP protocol
+TEST_F(Dhcp4SrvD2Test, badTCP) {
+ std::string config =
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ " \"dhcp-ddns\" : {"
+ " \"enable-updates\" : true, "
+ " \"server-ip\" : \"127.0.0.1\", "
+ " \"server-port\" : 53001, "
+ " \"sender-ip\" : \"0.0.0.0\", "
+ " \"sender-port\" : 0, "
+ " \"max-queue-size\" : 1024, "
+ " \"ncr-protocol\" : \"TCP\", "
+ " \"ncr-format\" : \"JSON\", "
+ " \"override-no-update\" : true, "
+ " \"override-client-update\" : true, "
+ " \"replace-client-name\" : \"when-present\", "
+ " \"generated-prefix\" : \"test.prefix\", "
+ " \"qualifying-suffix\" : \"test.suffix.\" },"
+ "\"valid-lifetime\": 4000 }";
+
+ ElementPtr json = Element::fromJSON(config);
+ ConstElementPtr status;
+
+ CfgMgr::instance().clear();
+
+ EXPECT_NO_THROW(status = configureDhcp4Server(srv_, json));
+ ASSERT_TRUE(status);
+ int rcode;
+ ConstElementPtr comment = config::parseAnswer(rcode, status);
+ EXPECT_EQ(1, rcode);
+}
+
+// Tests invalid config with bad sender family
+TEST_F(Dhcp4SrvD2Test, badFamily) {
+ std::string config =
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ " \"dhcp-ddns\" : {"
+ " \"enable-updates\" : true, "
+ " \"server-ip\" : \"127.0.0.1\", "
+ " \"server-port\" : 53001, "
+ " \"sender-ip\" : \"::\", "
+ " \"sender-port\" : 0, "
+ " \"max-queue-size\" : 1024, "
+ " \"ncr-protocol\" : \"UDP\", "
+ " \"ncr-format\" : \"JSON\", "
+ " \"override-no-update\" : true, "
+ " \"override-client-update\" : true, "
+ " \"replace-client-name\" : \"when-present\", "
+ " \"generated-prefix\" : \"test.prefix\", "
+ " \"qualifying-suffix\" : \"test.suffix.\" },"
+ "\"valid-lifetime\": 4000 }";
+
+ ElementPtr json = Element::fromJSON(config);
+ ConstElementPtr status;
+
+ CfgMgr::instance().clear();
+
+ EXPECT_NO_THROW(status = configureDhcp4Server(srv_, json));
+ ASSERT_TRUE(status);
+ int rcode;
+ ConstElementPtr comment = config::parseAnswer(rcode, status);
+ EXPECT_EQ(1, rcode);
+}
+
+// Tests invalid config with server == sender
+TEST_F(Dhcp4SrvD2Test, senderEqServer) {
+ std::string config =
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ " \"dhcp-ddns\" : {"
+ " \"enable-updates\" : true, "
+ " \"server-ip\" : \"127.0.0.1\", "
+ " \"server-port\" : 53001, "
+ " \"sender-ip\" : \"127.0.0.1\", "
+ " \"sender-port\" : 53001, "
+ " \"max-queue-size\" : 1024, "
+ " \"ncr-protocol\" : \"UDP\", "
+ " \"ncr-format\" : \"JSON\", "
+ " \"override-no-update\" : true, "
+ " \"override-client-update\" : true, "
+ " \"replace-client-name\" : \"when-present\", "
+ " \"generated-prefix\" : \"test.prefix\", "
+ " \"qualifying-suffix\" : \"test.suffix.\" },"
+ "\"valid-lifetime\": 4000 }";
+
+ ElementPtr json = Element::fromJSON(config);
+ ConstElementPtr status;
+
+ CfgMgr::instance().clear();
+
+ EXPECT_NO_THROW(status = configureDhcp4Server(srv_, json));
+ ASSERT_TRUE(status);
+ int rcode;
+ ConstElementPtr comment = config::parseAnswer(rcode, status);
+ EXPECT_EQ(1, rcode);
+}
+
+} // namespace test
+} // namespace dhcp
+} // namespace isc
+
diff --git a/src/bin/dhcp4/tests/d2_unittest.h b/src/bin/dhcp4/tests/d2_unittest.h
new file mode 100644
index 0000000..20474e3
--- /dev/null
+++ b/src/bin/dhcp4/tests/d2_unittest.h
@@ -0,0 +1,115 @@
+// Copyright (C) 2014-2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+/// @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 <cc/command_interpreter.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, 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 server_ip IP address for the D2 server
+ /// @param port port for the D2 server
+ /// @param sender_ip NCR sender's IP address
+ /// @param sender_port NCR sender port
+ /// @param max_queue_size maximum number of NCRs allowed in sender's queue
+ void configureD2(bool enable_updates, bool exp_result = SHOULD_PASS,
+ const std::string& server_ip = "127.0.0.1",
+ const size_t port = 53001,
+ const std::string& sender_ip = "0.0.0.0",
+ const size_t sender_port = 0,
+ const size_t max_queue_size = 1024);
+
+ /// @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 Constructs 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 attempt.
+ 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
diff --git a/src/bin/dhcp4/tests/decline_unittest.cc b/src/bin/dhcp4/tests/decline_unittest.cc
new file mode 100644
index 0000000..ef3bc1d
--- /dev/null
+++ b/src/bin/dhcp4/tests/decline_unittest.cc
@@ -0,0 +1,367 @@
+// Copyright (C) 2014-2023 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+#include <config.h>
+#include <asiolink/io_address.h>
+#include <cc/data.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/subnet_id.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <dhcp4/tests/dhcp4_client.h>
+#include <stats/stats_mgr.h>
+#include <boost/shared_ptr.hpp>
+#include <sstream>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::stats;
+
+namespace {
+/// @brief Set of JSON configurations used throughout the Decline tests.
+///
+/// - Configuration 0:
+/// - Used for testing Decline message processing
+/// - 1 subnet: 10.0.0.0/24
+/// - 1 pool: 10.0.0.10-10.0.0.100
+/// - Router option present: 10.0.0.200 and 10.0.0.201
+const char* DECLINE_CONFIGS[] = {
+// Configuration 0
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"option-data\": [ {"
+ " \"name\": \"routers\","
+ " \"data\": \"10.0.0.200,10.0.0.201\""
+ " } ]"
+ " } ]"
+ "}",
+// Configuration 1 - only use when mysql is enabled
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"lease-database\": {"
+ "\"type\": \"mysql\","
+ "\"name\": \"keatest\","
+ "\"user\": \"keatest\","
+ "\"password\": \"keatest\""
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"option-data\": [ {"
+ " \"name\": \"routers\","
+ " \"data\": \"10.0.0.200,10.0.0.201\""
+ " } ]"
+ " } ]"
+ "}",
+// Configuration 2 - only use when pgsql is enabled
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"lease-database\": {"
+ "\"type\": \"postgresql\","
+ "\"name\": \"keatest\","
+ "\"user\": \"keatest\","
+ "\"password\": \"keatest\""
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"option-data\": [ {"
+ " \"name\": \"routers\","
+ " \"data\": \"10.0.0.200,10.0.0.201\""
+ " } ]"
+ " } ]"
+ "}",
+};
+
+}
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+void
+Dhcpv4SrvTest::acquireLease(Dhcp4Client& client) {
+ // Perform 4-way exchange with the server but to not request any
+ // specific address in the DHCPDISCOVER message.
+ ASSERT_NO_THROW(client.doDORA());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Response must not be relayed.
+ EXPECT_FALSE(resp->isRelayed());
+ // Make sure that the server id is present.
+ EXPECT_EQ("10.0.0.1", client.config_.serverid_.toText());
+ // Make sure that the client has got the lease with the requested address.
+ ASSERT_NE(client.config_.lease_.addr_.toText(), "0.0.0.0");
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(client.config_.lease_.addr_);
+ ASSERT_TRUE(lease);
+}
+
+void
+Dhcpv4SrvTest::acquireAndDecline(Dhcp4Client& client,
+ const std::string& hw_address_1,
+ const std::string& client_id_1,
+ const std::string& hw_address_2,
+ const std::string& client_id_2,
+ ExpectedResult expected_result,
+ uint8_t config_index) {
+
+ // Set this global statistic explicitly to zero.
+ isc::stats::StatsMgr::instance().setValue("declined-addresses",
+ static_cast<int64_t>(0));
+
+ // Ok, do the normal lease acquisition.
+ CfgMgr::instance().clear();
+
+ // Configure DHCP server.
+ configure(DECLINE_CONFIGS[config_index], *client.getServer());
+ // Explicitly set the client id.
+ client.includeClientId(client_id_1);
+ // Explicitly set the HW address.
+ client.setHWAddress(hw_address_1);
+ // Perform 4-way exchange to obtain a new lease.
+ acquireLease(client);
+
+ // Let's get the subnet-id and generate statistics name out of it.
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ ASSERT_EQ(1, subnets->size());
+ std::stringstream name;
+ name << "subnet[" << (*subnets->begin())->getID() << "].declined-addresses";
+
+ // Set the subnet specific statistic explicitly to zero.
+ isc::stats::StatsMgr::instance().setValue(name.str(), static_cast<int64_t>(0));
+
+ // Check the declined-addresses (subnet) statistic before the Decline operation.
+ ObservationPtr declined_cnt = StatsMgr::instance().getObservation(name.str());
+ ASSERT_TRUE(declined_cnt);
+ uint64_t before = declined_cnt->getInteger().first;
+
+ // Check the global declined-addresses statistic before the Decline.
+ ObservationPtr declined_global = StatsMgr::instance()
+ .getObservation("declined-addresses");
+ ASSERT_TRUE(declined_global);
+ uint64_t before_global = declined_cnt->getInteger().first;
+
+ // Remember the acquired address.
+ IOAddress declined_address = client.config_.lease_.addr_;
+
+ // Explicitly set the client id for DHCPDECLINE.
+ client.includeClientId(client_id_2);
+ // Explicitly set the HW address for DHCPDECLINE.
+ client.setHWAddress(hw_address_2);
+
+ // Send the decline and make sure that the lease is removed from the
+ // server.
+ ASSERT_NO_THROW(client.doDecline());
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(declined_address);
+
+ declined_cnt = StatsMgr::instance().getObservation(name.str());
+ ASSERT_TRUE(declined_cnt);
+ uint64_t after = declined_cnt->getInteger().first;
+
+ declined_global = StatsMgr::instance().getObservation("declined-addresses");
+ ASSERT_TRUE(declined_global);
+ uint64_t after_global = declined_global->getInteger().first;
+
+ ASSERT_TRUE(lease);
+ // We check if the decline process was successful by checking if the
+ // lease is in the database and what is its state.
+ if (expected_result == SHOULD_PASS) {
+ ASSERT_EQ(Lease::STATE_DECLINED, lease->state_);
+
+ ASSERT_TRUE(lease->hwaddr_);
+ ASSERT_TRUE(lease->hwaddr_->hwaddr_.empty());
+ ASSERT_FALSE(lease->client_id_);
+ ASSERT_TRUE(lease->hostname_.empty());
+ ASSERT_FALSE(lease->fqdn_fwd_);
+ ASSERT_FALSE(lease->fqdn_rev_);
+
+ // The decline succeeded, so the declined-addresses statistic should
+ // be increased by one
+ ASSERT_EQ(after, before + 1);
+ ASSERT_EQ(after_global, before_global + 1);
+ } else {
+ // the decline was supposed, to be rejected.
+ ASSERT_EQ(Lease::STATE_DEFAULT, lease->state_);
+
+ ASSERT_TRUE(lease->hwaddr_);
+ ASSERT_FALSE(lease->hwaddr_->hwaddr_.empty());
+ ASSERT_TRUE(lease->client_id_);
+ ASSERT_FALSE(lease->fqdn_fwd_);
+ ASSERT_FALSE(lease->fqdn_rev_);
+
+ // The decline failed, so the declined-addresses should be the same
+ // as before
+ ASSERT_EQ(before, after);
+ ASSERT_EQ(before_global, after_global);
+ }
+}
+
+} // end of isc::dhcp::test namespace
+} // end of isc::dhcp namespace
+} // end of isc namespace
+
+namespace {
+
+/// @brief Test fixture class for testing DHCPDECLINE message handling.
+///
+/// @todo This class is very similar to ReleaseTest. Maybe we could
+/// merge those two classes one day and use derived classes?
+class DeclineTest : public Dhcpv4SrvTest {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Sets up fake interfaces.
+ DeclineTest()
+ : Dhcpv4SrvTest(),
+ iface_mgr_test_config_(true) {
+ }
+
+ /// @brief Interface Manager's fake configuration control.
+ IfaceMgrTestConfig iface_mgr_test_config_;
+
+};
+
+// This test checks that the client can acquire and decline the lease.
+TEST_F(DeclineTest, declineNoIdentifierChangeMemfile) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ acquireAndDecline(client, "01:02:03:04:05:06", "12:14",
+ "01:02:03:04:05:06", "12:14",
+ SHOULD_PASS);
+}
+
+#ifdef HAVE_MYSQL
+// This test checks that the client can acquire and decline the lease.
+TEST_F(DeclineTest, declineNoIdentifierChangeMySQL) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ acquireAndDecline(client, "01:02:03:04:05:06", "12:14",
+ "01:02:03:04:05:06", "12:14",
+ SHOULD_PASS, 1);
+}
+#endif
+
+#ifdef HAVE_PGSQL
+// This test checks that the client can acquire and decline the lease.
+TEST_F(DeclineTest, declineNoIdentifierChangePgSQL) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ acquireAndDecline(client, "01:02:03:04:05:06", "12:14",
+ "01:02:03:04:05:06", "12:14",
+ SHOULD_PASS, 2);
+}
+#endif
+
+// This test verifies the decline correctness in the following case:
+// - Client acquires new lease using HW address only
+// - Client sends the DHCPDECLINE with valid HW address and without
+// client identifier.
+// - The server successfully declines the lease.
+TEST_F(DeclineTest, declineHWAddressOnly) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ acquireAndDecline(client, "01:02:03:04:05:06", "",
+ "01:02:03:04:05:06", "",
+ SHOULD_PASS);
+}
+
+// This test verifies the decline correctness in the following case:
+// - Client acquires new lease using the client identifier and HW address
+// - Client sends the DHCPDECLINE with valid HW address but with no
+// client identifier.
+// - The server successfully declines the lease.
+TEST_F(DeclineTest, declineNoClientId) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ acquireAndDecline(client, "01:02:03:04:05:06", "12:14",
+ "01:02:03:04:05:06", "",
+ SHOULD_PASS);
+}
+
+// This test verifies the decline correctness in the following case:
+// - Client acquires new lease using HW address
+// - Client sends the DHCPDECLINE with valid HW address and some
+// client identifier.
+// - The server identifies the lease using HW address and declines
+// this lease.
+TEST_F(DeclineTest, declineNoClientId2) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ acquireAndDecline(client, "01:02:03:04:05:06", "",
+ "01:02:03:04:05:06", "12:14",
+ SHOULD_PASS);
+}
+
+// This test checks the server's behavior in the following case:
+// - Client acquires new lease using the client identifier and HW address
+// - Client sends the DHCPDECLINE with the valid HW address but with invalid
+// client identifier.
+// - The server should not remove the lease.
+TEST_F(DeclineTest, declineNonMatchingClientId) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ acquireAndDecline(client, "01:02:03:04:05:06", "12:14",
+ "01:02:03:04:05:06", "12:15:16",
+ SHOULD_FAIL);
+}
+
+// This test checks the server's behavior in the following case:
+// - Client acquires new lease using client identifier and HW address
+// - Client sends the DHCPDECLINE with the same client identifier but
+// different HW address.
+// - The server uses client identifier to find the client's lease and
+// declines it.
+TEST_F(DeclineTest, declineNonMatchingHWAddress) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ acquireAndDecline(client, "01:02:03:04:05:06", "12:14",
+ "06:06:06:06:06:06", "12:14",
+ SHOULD_PASS);
+}
+
+// This test checks the server's behavior in the following case:
+// - Client acquires new lease (address A).
+// - Client sends DHCPDECLINE with the requested IP address set to a different
+// address B than it has acquired from the server.
+// - Server determines that the client is trying to decline a
+// wrong address and will refuse to decline.
+TEST_F(DeclineTest, declineNonMatchingIPAddress) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Configure DHCP server.
+ configure(DECLINE_CONFIGS[0], *client.getServer());
+ // Perform 4-way exchange to obtain a new lease.
+ acquireLease(client);
+
+ // Remember the acquired address.
+ IOAddress leased_address = client.config_.lease_.addr_;
+
+ // Modify the client's address to force it to decline a different address
+ // than it has obtained from the server.
+ client.config_.lease_.addr_ = IOAddress(leased_address.toUint32() + 1);
+
+ // Send DHCPDECLINE and make sure it was unsuccessful, i.e. the lease
+ // remains in the database.
+ ASSERT_NO_THROW(client.doDecline());
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(leased_address);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(Lease::STATE_DEFAULT, lease->state_);
+}
+
+} // end of anonymous namespace
diff --git a/src/bin/dhcp4/tests/dhcp4_client.cc b/src/bin/dhcp4/tests/dhcp4_client.cc
new file mode 100644
index 0000000..b054a0d
--- /dev/null
+++ b/src/bin/dhcp4/tests/dhcp4_client.cc
@@ -0,0 +1,600 @@
+// Copyright (C) 2014-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/option.h>
+#include <dhcp/option_int_array.h>
+#include <dhcp/option_vendor.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcpsrv/lease.h>
+#include <dhcp4/tests/dhcp4_client.h>
+#include <util/multi_threading_mgr.h>
+#include <util/range_utilities.h>
+#include <boost/pointer_cast.hpp>
+#include <cstdlib>
+
+using namespace isc::asiolink;
+using namespace isc::util;
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+Dhcp4Client::Configuration::Configuration()
+ : routers_(), dns_servers_(), log_servers_(), quotes_servers_(),
+ serverid_("0.0.0.0"), siaddr_(IOAddress::IPV4_ZERO_ADDRESS()) {
+ reset();
+}
+
+void
+Dhcp4Client::Configuration::reset() {
+ routers_.clear();
+ dns_servers_.clear();
+ log_servers_.clear();
+ quotes_servers_.clear();
+ serverid_ = IOAddress("0.0.0.0");
+ siaddr_ = IOAddress::IPV4_ZERO_ADDRESS();
+ sname_.clear();
+ boot_file_name_.clear();
+ lease_ = Lease4();
+}
+
+Dhcp4Client::Dhcp4Client(const Dhcp4Client::State& state) :
+ config_(),
+ ciaddr_(),
+ curr_transid_(0),
+ dest_addr_("255.255.255.255"),
+ hwaddr_(generateHWAddr()),
+ clientid_(),
+ iface_name_("eth0"),
+ iface_index_(ETH0_INDEX),
+ relay_addr_("192.0.2.2"),
+ requested_options_(),
+ server_facing_relay_addr_("10.0.0.2"),
+ srv_(boost::shared_ptr<NakedDhcpv4Srv>(new NakedDhcpv4Srv(0))),
+ state_(state),
+ use_relay_(false),
+ circuit_id_() {
+}
+
+Dhcp4Client::Dhcp4Client(boost::shared_ptr<NakedDhcpv4Srv> srv,
+ const Dhcp4Client::State& state) :
+ config_(),
+ ciaddr_(),
+ curr_transid_(0),
+ dest_addr_("255.255.255.255"),
+ fqdn_(),
+ hwaddr_(generateHWAddr()),
+ clientid_(),
+ iface_name_("eth0"),
+ iface_index_(ETH0_INDEX),
+ relay_addr_("192.0.2.2"),
+ requested_options_(),
+ server_facing_relay_addr_("10.0.0.2"),
+ srv_(srv),
+ state_(state),
+ use_relay_(false),
+ circuit_id_() {
+}
+
+void
+Dhcp4Client::addRequestedAddress(const IOAddress& addr) {
+ if (context_.query_) {
+ Option4AddrLstPtr opt(new Option4AddrLst(DHO_DHCP_REQUESTED_ADDRESS,
+ addr));
+ context_.query_->addOption(opt);
+ }
+}
+
+void
+Dhcp4Client::appendClientId() {
+ if (!context_.query_) {
+ isc_throw(Dhcp4ClientError, "pointer to the query must not be NULL"
+ " when adding Client Identifier option");
+ }
+
+ if (clientid_) {
+ OptionPtr opt(new Option(Option::V4, DHO_DHCP_CLIENT_IDENTIFIER,
+ clientid_->getClientId()));
+ context_.query_->addOption(opt);
+ }
+}
+
+void
+Dhcp4Client::appendServerId() {
+ OptionPtr opt(new Option4AddrLst(DHO_DHCP_SERVER_IDENTIFIER,
+ config_.serverid_));
+ context_.query_->addOption(opt);
+}
+
+void
+Dhcp4Client::appendName() {
+ if (!context_.query_) {
+ isc_throw(Dhcp4ClientError, "pointer to the query must not be NULL"
+ " when adding FQDN or Hostname option");
+ }
+
+ if (fqdn_) {
+ context_.query_->addOption(fqdn_);
+
+ } else if (hostname_) {
+ context_.query_->addOption(hostname_);
+ }
+}
+
+void
+Dhcp4Client::appendPRL() {
+ if (!context_.query_) {
+ isc_throw(Dhcp4ClientError, "pointer to the query must not be NULL"
+ " when adding option codes to the PRL option");
+
+ } else if (!requested_options_.empty()) {
+ // Include Parameter Request List if at least one option code
+ // has been specified to be requested.
+ OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4,
+ DHO_DHCP_PARAMETER_REQUEST_LIST));
+ for (std::set<uint8_t>::const_iterator opt = requested_options_.begin();
+ opt != requested_options_.end(); ++opt) {
+ prl->addValue(*opt);
+ }
+ context_.query_->addOption(prl);
+ }
+}
+
+void
+Dhcp4Client::applyConfiguration() {
+ Pkt4Ptr resp = context_.response_;
+ if (!resp) {
+ return;
+ }
+
+ // Let's keep the old lease in case this is a response to Inform.
+ Lease4 old_lease = config_.lease_;
+ config_.reset();
+
+ // Routers
+ Option4AddrLstPtr opt_routers = boost::dynamic_pointer_cast<
+ Option4AddrLst>(resp->getOption(DHO_ROUTERS));
+ if (opt_routers) {
+ config_.routers_ = opt_routers->getAddresses();
+ }
+ // DNS Servers
+ Option4AddrLstPtr opt_dns_servers = boost::dynamic_pointer_cast<
+ Option4AddrLst>(resp->getOption(DHO_DOMAIN_NAME_SERVERS));
+ if (opt_dns_servers) {
+ config_.dns_servers_ = opt_dns_servers->getAddresses();
+ }
+ // Log Servers
+ Option4AddrLstPtr opt_log_servers = boost::dynamic_pointer_cast<
+ Option4AddrLst>(resp->getOption(DHO_LOG_SERVERS));
+ if (opt_log_servers) {
+ config_.log_servers_ = opt_log_servers->getAddresses();
+ }
+ // Quotes Servers
+ Option4AddrLstPtr opt_quotes_servers = boost::dynamic_pointer_cast<
+ Option4AddrLst>(resp->getOption(DHO_COOKIE_SERVERS));
+ if (opt_quotes_servers) {
+ config_.quotes_servers_ = opt_quotes_servers->getAddresses();
+ }
+ // Vendor Specific options
+ OptionVendorPtr opt_vendor = boost::dynamic_pointer_cast<
+ OptionVendor>(resp->getOption(DHO_VIVSO_SUBOPTIONS));
+ if (opt_vendor) {
+ config_.vendor_suboptions_ = opt_vendor->getOptions();
+ }
+ // siaddr
+ config_.siaddr_ = resp->getSiaddr();
+ // sname
+ OptionBuffer buf = resp->getSname();
+ // sname is a fixed length field holding null terminated string. Use
+ // of c_str() guarantees that only a useful portion (ending with null
+ // character) is assigned.
+ config_.sname_.assign(std::string(buf.begin(), buf.end()).c_str());
+ // (boot)file
+ buf = resp->getFile();
+ config_.boot_file_name_.assign(std::string(buf.begin(), buf.end()).c_str());
+ // Server Identifier
+ OptionCustomPtr opt_serverid = boost::dynamic_pointer_cast<
+ OptionCustom>(resp->getOption(DHO_DHCP_SERVER_IDENTIFIER));
+ if (opt_serverid) {
+ config_.serverid_ = opt_serverid->readAddress();
+ }
+
+ // If the message sent was Inform, we don't want to throw
+ // away the old lease info, just the bits about options.
+ if (context_.query_->getType() == DHCPINFORM) {
+ config_.lease_ = old_lease;
+ } else {
+ /// @todo Set the valid lifetime, t1, t2 etc.
+ config_.lease_ = Lease4(IOAddress(context_.response_->getYiaddr()),
+ context_.response_->getHWAddr(),
+ 0, 0, 0, time(NULL), 0, false, false,
+ "");
+ }
+}
+
+void
+Dhcp4Client::createLease(const IOAddress& addr, const uint32_t valid_lft) {
+ Lease4 lease(addr, hwaddr_, 0, 0, valid_lft,
+ time(NULL), 0, false, false, "");
+ config_.lease_ = lease;
+}
+
+Pkt4Ptr
+Dhcp4Client::createMsg(const uint8_t msg_type) {
+ Pkt4Ptr msg(new Pkt4(msg_type, curr_transid_++));
+ msg->setHWAddr(hwaddr_);
+ return (msg);
+}
+
+void
+Dhcp4Client::appendExtraOptions() {
+ // If there are any custom options specified, add them all to the message.
+ if (!extra_options_.empty()) {
+ for (OptionCollection::iterator opt = extra_options_.begin();
+ opt != extra_options_.end(); ++opt) {
+ // Call base class function so that unittests can add multiple
+ // options with the same code.
+ context_.query_->Pkt::addOption(opt->second);
+ }
+ }
+}
+
+void
+Dhcp4Client::appendClasses() {
+ for (ClientClasses::const_iterator cclass = classes_.cbegin();
+ cclass != classes_.cend(); ++cclass) {
+ context_.query_->addClass(*cclass);
+ }
+}
+
+void
+Dhcp4Client::doDiscover(const boost::shared_ptr<IOAddress>& requested_addr) {
+ context_.query_ = createMsg(DHCPDISCOVER);
+ // Request options if any.
+ appendPRL();
+ // Include FQDN or Hostname.
+ appendName();
+ // Include Client Identifier
+ appendClientId();
+ if (requested_addr) {
+ addRequestedAddress(*requested_addr);
+ }
+ // Override the default ciaddr if specified by a test.
+ if (!ciaddr_.unspecified()) {
+ context_.query_->setCiaddr(ciaddr_);
+ }
+ appendExtraOptions();
+ appendClasses();
+
+ // Send the message to the server.
+ sendMsg(context_.query_);
+ // Expect response.
+ context_.response_ = receiveOneMsg();
+}
+
+void
+Dhcp4Client::doDORA(const boost::shared_ptr<IOAddress>& requested_addr) {
+ doDiscover(requested_addr);
+ if (context_.response_ && (context_.response_->getType() == DHCPOFFER)) {
+ doRequest();
+ }
+}
+
+void
+Dhcp4Client::doInform(const bool set_ciaddr) {
+ context_.query_ = createMsg(DHCPINFORM);
+ // Request options if any.
+ appendPRL();
+ // Any other options to be sent by a client.
+ appendExtraOptions();
+ // Add classes.
+ appendClasses();
+ // The client sending a DHCPINFORM message has an IP address obtained
+ // by some other means, e.g. static configuration. The lease which we
+ // are using here is most likely set by the createLease method.
+ if (set_ciaddr) {
+ context_.query_->setCiaddr(config_.lease_.addr_);
+ }
+ context_.query_->setLocalAddr(config_.lease_.addr_);
+ // Send the message to the server.
+ sendMsg(context_.query_);
+ // Expect response. If there is no response, return.
+ context_.response_ = receiveOneMsg();
+ if (!context_.response_) {
+ return;
+ }
+ // If DHCPACK has been returned by the server, use the returned
+ // configuration.
+ if (context_.response_->getType() == DHCPACK) {
+ applyConfiguration();
+ }
+}
+
+void
+Dhcp4Client::doRelease() {
+ // There is no response for Release message.
+ context_.response_.reset();
+
+ if (config_.lease_.addr_.isV4Zero()) {
+ isc_throw(Dhcp4ClientError, "failed to send the release"
+ " message because client doesn't have a lease");
+ }
+ context_.query_ = createMsg(DHCPRELEASE);
+ // Set ciaddr to the address which we want to release.
+ context_.query_->setCiaddr(config_.lease_.addr_);
+ // Include client identifier.
+ appendClientId();
+
+ // Remove configuration.
+ config_.reset();
+
+ // Send the message to the server.
+ sendMsg(context_.query_);
+}
+
+void
+Dhcp4Client::doDecline() {
+ if (config_.lease_.addr_.isV4Zero()) {
+ isc_throw(Dhcp4ClientError, "failed to send the decline"
+ " message because client doesn't have a lease");
+ }
+
+ context_.query_ = createMsg(DHCPDECLINE);
+
+ // Set ciaddr to 0.
+ context_.query_->setCiaddr(IOAddress("0.0.0.0"));
+
+ // Include Requested IP Address Option
+ addRequestedAddress(config_.lease_.addr_);
+
+ // Include client identifier.
+ appendClientId();
+
+ // Include server identifier.
+ appendServerId();
+
+ // Remove configuration.
+ config_.reset();
+
+ // Send the message to the server.
+ sendMsg(context_.query_);
+}
+
+void
+Dhcp4Client::doRequest() {
+ context_.query_ = createMsg(DHCPREQUEST);
+
+ // Override the default ciaddr if specified by a test.
+ if (!ciaddr_.unspecified()) {
+ context_.query_->setCiaddr(ciaddr_);
+ } else if ((state_ == SELECTING) || (state_ == INIT_REBOOT)) {
+ context_.query_->setCiaddr(IOAddress("0.0.0.0"));
+ } else {
+ context_.query_->setCiaddr(IOAddress(config_.lease_.addr_));
+ }
+
+ // Requested IP address.
+ if (state_ == SELECTING) {
+ if (context_.response_ &&
+ (context_.response_->getType() == DHCPOFFER) &&
+ (context_.response_->getYiaddr() != IOAddress("0.0.0.0"))) {
+ addRequestedAddress(context_.response_->getYiaddr());
+ } else {
+ isc_throw(Dhcp4ClientError, "error sending the DHCPREQUEST because"
+ " the received DHCPOFFER message was invalid");
+ }
+ } else if (state_ == INIT_REBOOT) {
+ addRequestedAddress(config_.lease_.addr_);
+ }
+
+ // Server identifier.
+ if (state_ == SELECTING) {
+ if (context_.response_) {
+ OptionPtr opt_serverid =
+ context_.response_->getOption(DHO_DHCP_SERVER_IDENTIFIER);
+ if (!opt_serverid) {
+ isc_throw(Dhcp4ClientError, "missing server identifier in the"
+ " server's response");
+ }
+ context_.query_->addOption(opt_serverid);
+ }
+ }
+
+ // Request options if any.
+ appendPRL();
+ // Include FQDN or Hostname.
+ appendName();
+ // Include Client Identifier
+ appendClientId();
+ // Any other options to be sent by a client.
+ appendExtraOptions();
+ // Add classes.
+ appendClasses();
+ // Send the message to the server.
+ sendMsg(context_.query_);
+ // Expect response.
+ context_.response_ = receiveOneMsg();
+ // If the server has responded, store the configuration received.
+ if (context_.response_) {
+ applyConfiguration();
+ }
+}
+
+void
+Dhcp4Client::receiveResponse() {
+ context_.response_ = receiveOneMsg();
+ // If the server has responded, store the configuration received.
+ if (context_.response_) {
+ applyConfiguration();
+ }
+}
+
+void
+Dhcp4Client::includeClientId(const std::string& clientid) {
+ if (clientid.empty()) {
+ clientid_.reset();
+
+ } else {
+ clientid_ = ClientId::fromText(clientid);
+ }
+}
+
+void
+Dhcp4Client::includeFQDN(const uint8_t flags, const std::string& fqdn_name,
+ Option4ClientFqdn::DomainNameType fqdn_type) {
+ fqdn_.reset(new Option4ClientFqdn(flags, Option4ClientFqdn::RCODE_CLIENT(),
+ fqdn_name, fqdn_type));
+}
+
+void
+Dhcp4Client::includeHostname(const std::string& name) {
+ if (name.empty()) {
+ hostname_.reset();
+ } else {
+ hostname_.reset(new OptionString(Option::V4, DHO_HOST_NAME, name));
+ }
+}
+
+HWAddrPtr
+Dhcp4Client::generateHWAddr(const uint8_t htype) const {
+ if (htype != HTYPE_ETHER) {
+ isc_throw(isc::NotImplemented,
+ "The hardware address type " << static_cast<int>(htype)
+ << " is currently not supported");
+ }
+ std::vector<uint8_t> hwaddr(HWAddr::ETHERNET_HWADDR_LEN);
+ // Generate ethernet hardware address by assigning random byte values.
+ isc::util::fillRandom(hwaddr.begin(), hwaddr.end());
+ return (HWAddrPtr(new HWAddr(hwaddr, htype)));
+}
+
+void
+Dhcp4Client::modifyHWAddr() {
+ if (!hwaddr_) {
+ hwaddr_ = generateHWAddr();
+ return;
+ }
+ // Modify the HW address by adding 1 to its last byte.
+ ++hwaddr_->hwaddr_[hwaddr_->hwaddr_.size() - 1];
+}
+
+void
+Dhcp4Client::requestOption(const uint8_t option) {
+ if (option != 0) {
+ requested_options_.insert(option);
+ }
+}
+
+void
+Dhcp4Client::requestOptions(const uint8_t option1, const uint8_t option2,
+ const uint8_t option3) {
+ requested_options_.clear();
+ requestOption(option1);
+ requestOption(option2);
+ requestOption(option3);
+}
+
+Pkt4Ptr
+Dhcp4Client::receiveOneMsg() {
+ Pkt4Ptr msg = srv_->receiveOneMsg();
+ if (!msg) {
+ return (Pkt4Ptr());
+ }
+
+ // Copy the original message to simulate reception over the wire.
+ msg->pack();
+ Pkt4Ptr msg_copy(new Pkt4(static_cast<const uint8_t*>
+ (msg->getBuffer().getData()),
+ msg->getBuffer().getLength()));
+ msg_copy->setRemoteAddr(msg->getLocalAddr());
+ msg_copy->setLocalAddr(msg->getRemoteAddr());
+ msg_copy->setRemotePort(msg->getLocalPort());
+ msg_copy->setLocalPort(msg->getRemotePort());
+ msg_copy->setIface(msg->getIface());
+ msg_copy->setIndex(msg->getIndex());
+
+ msg_copy->unpack();
+
+ return (msg_copy);
+}
+
+void
+Dhcp4Client::sendMsg(const Pkt4Ptr& msg) {
+ srv_->shutdown_ = false;
+ if (use_relay_) {
+ try {
+ msg->setHops(1);
+ msg->setGiaddr(relay_addr_);
+ msg->setLocalAddr(server_facing_relay_addr_);
+ // Insert RAI
+ OptionPtr rai(new Option(Option::V4, DHO_DHCP_AGENT_OPTIONS));
+ // Insert circuit id, if specified.
+ if (!circuit_id_.empty()) {
+ rai->addOption(OptionPtr(new Option(Option::V4, RAI_OPTION_AGENT_CIRCUIT_ID,
+ OptionBuffer(circuit_id_.begin(),
+ circuit_id_.end()))));
+ }
+ msg->addOption(rai);
+ } catch (...) {
+ // If relay options have already been added in the unittest, ignore
+ // exception on add.
+ }
+ }
+ // Repack the message to simulate wire-data parsing.
+ msg->pack();
+ Pkt4Ptr msg_copy(new Pkt4(static_cast<const uint8_t*>
+ (msg->getBuffer().getData()),
+ msg->getBuffer().getLength()));
+ msg_copy->setRemoteAddr(msg->getLocalAddr());
+ msg_copy->setLocalAddr(dest_addr_);
+ msg_copy->setIface(iface_name_);
+ msg_copy->setIndex(iface_index_);
+ // Copy classes
+ const ClientClasses& classes = msg->getClasses();
+ for (ClientClasses::const_iterator cclass = classes.cbegin();
+ cclass != classes.cend(); ++cclass) {
+ msg_copy->addClass(*cclass);
+ }
+ srv_->fakeReceive(msg_copy);
+
+ try {
+ // Invoke run_one instead of run, because we want to avoid triggering
+ // IO service.
+ srv_->run_one();
+ } catch (...) {
+ // Suppress errors, as the DHCPv4 server does.
+ }
+
+ // Make sure the server processed all packets in MT.
+ isc::util::MultiThreadingMgr::instance().getThreadPool().wait(3);
+}
+
+void
+Dhcp4Client::setHWAddress(const std::string& hwaddr_str) {
+ if (hwaddr_str.empty()) {
+ hwaddr_.reset();
+ } else {
+ hwaddr_.reset(new HWAddr(HWAddr::fromText(hwaddr_str)));
+ }
+}
+
+void
+Dhcp4Client::addExtraOption(const OptionPtr& opt) {
+ extra_options_.insert(std::make_pair(opt->getType(), opt));
+}
+
+void
+Dhcp4Client::addClass(const ClientClass& client_class) {
+ if (!classes_.contains(client_class)) {
+ classes_.insert(client_class);
+ }
+}
+
+} // end of namespace isc::dhcp::test
+} // end of namespace isc::dhcp
+} // end of namespace isc
diff --git a/src/bin/dhcp4/tests/dhcp4_client.h b/src/bin/dhcp4/tests/dhcp4_client.h
new file mode 100644
index 0000000..a7265a4
--- /dev/null
+++ b/src/bin/dhcp4/tests/dhcp4_client.h
@@ -0,0 +1,533 @@
+// Copyright (C) 2014-2020 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#ifndef DHCP4_CLIENT_H
+#define DHCP4_CLIENT_H
+
+#include <asiolink/io_address.h>
+#include <dhcp/hwaddr.h>
+#include <dhcp/option.h>
+#include <dhcp/pkt4.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <util/optional.h>
+#include <boost/noncopyable.hpp>
+#include <boost/shared_ptr.hpp>
+#include <set>
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @brief General error emitted by the DHCP4 test client.
+class Dhcp4ClientError : public isc::Exception {
+public:
+ Dhcp4ClientError(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// @brief DHCPv4 client used for unit testing.
+///
+/// This class implements a DHCPv4 "client" which interoperates with the
+/// @c NakedDhcpv4Srv class. It calls @c NakedDhcpv4Srv::fakeReceive to
+/// deliver client messages to the server for processing. The server places
+/// the response in the @c NakedDhcpv4Srv::fake_sent_ container. The client
+/// pops messages from this container which simulates reception of the
+/// response from the server.
+///
+/// The client maintains the leases it acquired from the server.
+///
+/// The client exposes a set of functions which simulate different exchange
+/// types between the client and the server. It also provides the access to
+/// the objects encapsulating responses from the server so as it is possible
+/// to verify from the unit test that the server's response is correct.
+class Dhcp4Client : public boost::noncopyable {
+public:
+
+ /// @brief States of the DHCP client.
+ enum State {
+ SELECTING,
+ INIT_REBOOT,
+ RENEWING,
+ REBINDING
+ };
+
+ /// @brief Holds the DHCPv4 messages taking part in transaction between
+ /// the client and the server.
+ struct Context {
+ /// @brief Holds the last sent message from the client to the server.
+ Pkt4Ptr query_;
+ /// @brief Holds the last sent message by the server to the client.
+ Pkt4Ptr response_;
+ };
+
+ /// @brief Holds the configuration of the client received from the
+ /// DHCP server.
+ struct Configuration {
+ /// @brief Holds IP addresses received in the Routers option.
+ Option4AddrLst::AddressContainer routers_;
+ /// @brief Holds IP addresses received in the DNS Servers option.
+ Option4AddrLst::AddressContainer dns_servers_;
+ /// @brief Holds IP addresses received in the Log Servers option.
+ Option4AddrLst::AddressContainer log_servers_;
+ /// @brief Holds IP addresses received in the Quotes Servers option.
+ Option4AddrLst::AddressContainer quotes_servers_;
+ /// @brief Vendor Specific options
+ OptionCollection vendor_suboptions_;
+ /// @brief Holds a lease obtained by the client.
+ Lease4 lease_;
+ /// @brief Holds server id of the server which responded to the client's
+ /// request.
+ asiolink::IOAddress serverid_;
+ /// @brief Holds returned siaddr.
+ asiolink::IOAddress siaddr_;
+ /// @brief Holds returned sname.
+ std::string sname_;
+ /// @brief Holds returned (boot)file.
+ std::string boot_file_name_;
+
+ /// @brief Constructor.
+ Configuration();
+
+ /// @brief Sets configuration values to defaults.
+ void reset();
+ };
+
+ /// @brief Creates a new client.
+ ///
+ /// @param Initial client's state.
+ Dhcp4Client(const State& state = SELECTING);
+
+ /// @brief Creates a new client that communicates with a specified server.
+ ///
+ /// @param srv An instance of the DHCPv4 server to be used.
+ /// @param state Initial client's state.
+ Dhcp4Client(boost::shared_ptr<NakedDhcpv4Srv> srv,
+ const State& state = SELECTING);
+
+ /// @brief Creates a lease for the client using the specified address
+ /// and valid lifetime.
+ ///
+ /// This method creates the lease using the specified address and
+ /// valid lease lifetime. The client will use this lease in any
+ /// future communication with the DHCP server. One of the use cases
+ /// for this method is to pre-configure the client with the explicitly
+ /// given address before it sends the DHCPINFORM to the DHCP server.
+ /// The client will inject the leased address into the ciaddr field
+ /// of the DHCPINFORM message.
+ ///
+ /// @param addr Lease address.
+ /// @param valid_lft Valid lifetime.
+ void createLease(const asiolink::IOAddress& addr, const uint32_t valid_lft);
+
+ /// @brief Sends DHCPDISCOVER message to the server and receives response.
+ ///
+ /// The message being sent to the server includes Parameter Request List
+ /// option if any options to be requested have been specified using the
+ /// @c requestOptions or @c requestOption methods.
+ ///
+ /// The configuration returned by the server in the DHCPOFFER message is
+ /// NOT stored in the client configuration: @c config_.
+ ///
+ /// @param requested_addr A pointer to the IP Address to be sent in the
+ /// Requested IP Address option or NULL if the option should not be
+ /// included.
+ void doDiscover(const boost::shared_ptr<asiolink::IOAddress>&
+ requested_addr = boost::shared_ptr<asiolink::IOAddress>());
+
+ /// @brief Perform 4-way exchange with a server.
+ ///
+ /// This method calls @c doDiscover and @c doRequest to perform the 4-way
+ /// exchange with the server.
+ ///
+ /// @param requested_addr A pointer to the address to be requested using the
+ /// Requested IP Address option.
+ void doDORA(const boost::shared_ptr<asiolink::IOAddress>&
+ requested_addr = boost::shared_ptr<asiolink::IOAddress>());
+
+ /// @brief Sends DHCPINFORM message to the server and receives response.
+ ///
+ /// This function simulates sending the DHCPINFORM message to the server
+ /// and receiving server's response (if any). The server's response and the
+ /// message sent to the server is stored in the context structure and can
+ /// be accessed using @c getContext function.
+ ///
+ /// The configuration returned by the server is stored in the
+ /// @c config_ public member and can be accessed directly.
+ ///
+ /// @param set_ciaddr Indicates if the ciaddr should be set for an
+ /// outgoing message and defaults to true. Note, that the RFC2131 mandates
+ /// setting the ciaddr for DHCPINFORM but the server may still want to
+ /// respond if the ciaddr is not set.
+ ///
+ /// @throw This function doesn't thrown exceptions on its own, but it calls
+ /// functions that are not exception safe, so it may emit an exception if
+ /// an error occurs.
+ void doInform(const bool set_ciaddr = true);
+
+ /// @brief Sends DHCPRELEASE Message to the server.
+ ///
+ /// This method simulates sending the DHCPRELEASE message to the server.
+ /// The released lease is removed from the client's configuration.
+ void doRelease();
+
+
+ /// @brief Sends DHCPDECLINE Message to the server.
+ ///
+ /// This method simulates sending the DHCPDECLINE message to the server.
+ /// The released lease is removed from the client's configuration.
+ void doDecline();
+
+ /// @brief Sends DHCPREQUEST Message to the server and receives a response.
+ ///
+ /// This method simulates sending the DHCPREQUEST message to the server and
+ /// receiving a response. The DHCPREQUEST message can be used by the client
+ /// being in various states:
+ /// - SELECTING - client is trying to obtain a new lease and it has selected
+ /// the server using the DHCPDISCOVER.
+ /// - INIT-REBOOT - client cached an address it was previously using and is
+ /// now trying to verify if this address is still valid.
+ /// - RENEW - client's renewal timer has passed and the client is trying to
+ /// extend the lifetime of the lease.
+ /// - REBIND - client's rebind timer has passed and the client is trying to
+ /// extend the lifetime of the lease from any server.
+ ///
+ /// Depending on the state that the client is in, different combinations of
+ /// - ciaddr
+ /// - Requested IP Address option
+ /// - server identifier
+ /// are used (as per RFC2131, section 4.3.2). Therefore, the unit tests
+ /// must set the appropriate state of the client prior to calling this
+ /// method using the @c setState function.
+ ///
+ /// When the server returns the DHCPACK the configuration carried in the
+ /// DHCPACK message is applied and can be obtained from the @c config_.
+ void doRequest();
+
+ /// @brief Receives a response from the server.
+ ///
+ /// This method is useful to receive response from the server after
+ /// parking a packet. In this case, the packet is not received as a
+ /// result of initial exchange, e.g. @c doRequest. The test can call
+ /// this method to complete the transaction when it expects that the
+ /// packet has been unparked.
+ void receiveResponse();
+
+ /// @brief Generates a hardware address used by the client.
+ ///
+ /// It assigns random values to the bytes of the hardware address.
+ ///
+ /// @param htype hardware address type. Currently the only type
+ /// supported is Ethernet hardware address.
+ ///
+ /// @return Pointer to the generated hardware address.
+ HWAddrPtr generateHWAddr(const uint8_t htype = HTYPE_ETHER) const;
+
+ /// @brief Returns HW address used by the client.
+ HWAddrPtr getHWAddress() const {
+ return (hwaddr_);
+ }
+
+ /// @brief Returns current context.
+ const Context& getContext() const {
+ return (context_);
+ }
+
+ /// @brief Returns the server that the client is communicating with.
+ boost::shared_ptr<NakedDhcpv4Srv> getServer() const {
+ return (srv_);
+ }
+
+ /// @brief Creates the client id from the client id in the textual format.
+ ///
+ /// The generated client id will be added to the client's messages to the
+ /// server.
+ ///
+ /// @param clientid Client id in the textual format. Use the empty client id
+ /// value to not include the client id.
+ void includeClientId(const std::string& clientid);
+
+ /// @brief Creates an instance of the Client FQDN option to be included
+ /// in the client's message.
+ ///
+ /// @param flags Flags.
+ /// @param fqdn_name Name in the textual format.
+ /// @param fqdn_type Type of the name (fully qualified or partial).
+ void includeFQDN(const uint8_t flags, const std::string& fqdn_name,
+ Option4ClientFqdn::DomainNameType fqdn_type);
+
+ /// @brief Creates an instance of the Hostname option to be included
+ /// in the client's message.
+ ///
+ /// @param name Name to be stored in the option.
+ void includeHostname(const std::string& name);
+
+ /// @brief Modifies the client's HW address (adds one to it).
+ ///
+ /// The HW address should be modified to test negative scenarios when the
+ /// client acquires a lease and tries to renew it with a different HW
+ /// address. The server should detect the HW address mismatch and react
+ /// accordingly.
+ ///
+ /// The HW address modification affects the value returned by the
+ /// @c Dhcp4Client::getHWAddress.
+ void modifyHWAddr();
+
+ /// @brief Specify an option to be requested by a client.
+ ///
+ /// This function adds option code to the collection of option
+ /// codes to be requested by a client.
+ ///
+ /// @param option Option code to be requested. The value of 0 is
+ /// ignored and the function is no-op.
+ void requestOption(const uint8_t option);
+
+ /// @brief Specifies options to be requested by the client.
+ ///
+ /// This function configures the client to request options having
+ /// specified codes using Parameter Request List option. The default
+ /// value of 0 specify that the option is not requested.
+ ///
+ /// If there are options specified to be requested before the function
+ /// is called, the new option codes override previously specified ones.
+ /// In order to clear the list of requested options call
+ /// @c requestOptions(0).
+ ///
+ /// @param option1 First option to be requested.
+ /// @param option2 Second option to be requested (optional).
+ /// @param option3 Third option to be requested (optional).
+ void requestOptions(const uint8_t option1,
+ const uint8_t option2 = 0,
+ const uint8_t option3 = 0);
+
+ /// @brief Sets circuit-id value to be included in the circuit-id
+ /// sub option of the RAI option.
+ ///
+ /// @param circuit_id New circuit-id value.
+ void setCircuitId(const std::string& circuit_id) {
+ circuit_id_ = circuit_id;
+ }
+
+ /// @brief Sets destination address for the messages being sent by the
+ /// client.
+ ///
+ /// By default, the client uses broadcast address 255.255.255.255 to
+ /// communicate with the server. In certain cases it may be desired
+ /// that different address is used. This function sets the new address
+ /// for all future exchanges with the server.
+ ///
+ /// @param dest_addr New destination address.
+ void setDestAddress(const asiolink::IOAddress& dest_addr) {
+ dest_addr_ = dest_addr;
+ }
+
+ /// @brief Sets the explicit hardware address for the client.
+ ///
+ /// @param hwaddr_str String representation of the HW address. Use an
+ /// empty string to set the NULL hardware address.
+ void setHWAddress(const std::string& hwaddr_str);
+
+ /// @brief Sets the interface over which the messages should be sent.
+ ///
+ /// @param iface_name Name of the interface over which the messages should
+ /// be sent.
+ void setIfaceName(const std::string& iface_name) {
+ iface_name_ = iface_name;
+ }
+
+ /// @brief Sets the interface over which the messages should be sent.
+ ///
+ /// @param iface_index Index of the interface over which the
+ /// messages should be sent.
+ void setIfaceIndex(uint32_t iface_index) {
+ iface_index_ = iface_index;
+ }
+
+ /// @brief Sets client state.
+ ///
+ /// Depending on the current state the client's behavior is different
+ /// when sending Request messages as per RFC2131, section 4.3.2.
+ ///
+ /// @param state New client's state.
+ void setState(const State& state) {
+ state_ = state;
+ }
+
+ /// @brief Simulate sending messages through a relay.
+ ///
+ /// @param use Parameter which 'true' value indicates that client should
+ /// simulate sending messages via relay.
+ /// @param relay_addr Relay address
+ /// @param sf_relay_addr Server facing relay address.
+ void useRelay(const bool use = true,
+ const asiolink::IOAddress& relay_addr =
+ asiolink::IOAddress("192.0.2.2"),
+ const asiolink::IOAddress& sf_relay_addr =
+ asiolink::IOAddress("10.0.0.2")) {
+ use_relay_ = use;
+ relay_addr_ = relay_addr;
+ server_facing_relay_addr_ = sf_relay_addr;
+ }
+
+ /// @brief Current client's configuration obtained from the server.
+ Configuration config_;
+
+ /// @brief Specific ciaddr to be used in client's messages.
+ ///
+ /// If this value is "unspecified" the default values will be used
+ /// by the client. If this value is specified, it will override ciaddr
+ /// in the client's messages.
+ isc::util::Optional<asiolink::IOAddress> ciaddr_;
+
+ /// @brief Adds extra option (an option the client will always send)
+ ///
+ /// @param opt additional option to be sent
+ void addExtraOption(const OptionPtr& opt);
+
+ /// @brief Add a client class.
+ ///
+ /// @param client_class name of the class to be added.
+ void addClass(const ClientClass& client_class);
+
+private:
+ /// @brief Appends extra options, previously added with addExtraOption()
+ ///
+ /// @brief Copies options from extra_options_ into outgoing message
+ void appendExtraOptions();
+
+ /// @brief Appends extra classes, previously added with addClass()
+ ///
+ /// @brief Add client classes from classes_ to incoming message
+ void appendClasses();
+
+ /// @brief Creates and adds Requested IP Address option to the client's
+ /// query.
+ ///
+ /// @param addr Address to be added in the Requested IP Address option.
+ void addRequestedAddress(const asiolink::IOAddress& addr);
+
+ /// @brief Stores configuration received from the server.
+ ///
+ /// This methods stores the configuration obtained from the DHCP server
+ /// in the @c Configuration structure. This configuration includes:
+ /// - obtained lease
+ /// - server id of the server that provided the configuration
+ /// - lease
+ /// - selected options (used by unit tests):
+ /// - DNS Servers
+ /// - Routers
+ /// - Log Servers
+ /// - Quotes Servers
+ void applyConfiguration();
+
+ /// @brief Creates client's side DHCP message.
+ ///
+ /// @param msg_type Type of the message to be created.
+ /// @return An instance of the message created.
+ Pkt4Ptr createMsg(const uint8_t msg_type);
+
+ /// @brief Includes the Client Identifier option in the client's message.
+ ///
+ /// This function creates an instance of the Client Identifier option
+ /// if the client identifier has been specified and includes this
+ /// option in the client's message to the server.
+ void appendClientId();
+
+ /// @brief Includes the Server Identifier option in the client's message.
+ ///
+ /// This function creates an instance of the Server Identifier option.
+ /// It uses whatever information is stored in config_.serverid_.
+ void appendServerId();
+
+ /// @brief Includes FQDN or Hostname option in the client's message.
+ ///
+ /// This method checks if @c fqdn_ or @c hostname_ is specified and
+ /// includes it in the client's message. If both are specified, the
+ /// @c fqdn_ will be used.
+ void appendName();
+
+ /// @brief Include PRL Option in the query message.
+ ///
+ /// This function creates the instance of the PRL (Parameter Request List)
+ /// option and adds option codes from the @c requested_options_ to it.
+ /// It later adds the PRL option to the @c context_.query_ message
+ /// if it is non-NULL.
+ void appendPRL();
+
+ /// @brief Simulates reception of the message from the server.
+ ///
+ /// @return Received message.
+ Pkt4Ptr receiveOneMsg();
+
+ /// @brief Simulates sending a message to the server.
+ ///
+ /// This function instantly triggers processing of the message by the
+ /// server. The server's response can be gathered by invoking the
+ /// @c receiveOneMsg function.
+ ///
+ /// @param msg Message to be sent.
+ void sendMsg(const Pkt4Ptr& msg);
+
+ /// @brief Current context (sent and received message).
+ Context context_;
+
+ /// @biref Current transaction id (altered on each send).
+ uint32_t curr_transid_;
+
+ /// @brief Currently used destination address.
+ asiolink::IOAddress dest_addr_;
+
+ /// @brief FQDN requested by the client.
+ Option4ClientFqdnPtr fqdn_;
+
+ /// @brief Hostname requested by the client.
+ OptionStringPtr hostname_;
+
+ /// @brief Current hardware address of the client.
+ HWAddrPtr hwaddr_;
+
+ /// @brief Current client identifier.
+ ClientIdPtr clientid_;
+
+ /// @brief Interface to be used to send the messages (name).
+ std::string iface_name_;
+
+ /// @brief Interface to be used to send the messages (index).
+ uint32_t iface_index_;
+
+ /// @brief Relay address to use.
+ asiolink::IOAddress relay_addr_;
+
+ /// @brief Collection of options codes to be requested by the client.
+ std::set<uint8_t> requested_options_;
+
+ /// @brief Address of the relay interface connected to the server.
+ asiolink::IOAddress server_facing_relay_addr_;
+
+ /// @brief Pointer to the server that the client is communicating with.
+ boost::shared_ptr<NakedDhcpv4Srv> srv_;
+
+ /// @brief Current state of the client.
+ State state_;
+
+ /// @brief Enable relaying messages to the server.
+ bool use_relay_;
+
+ /// @brief Specifies value to be inserted into circuit-id sub option
+ /// of the RAI option.
+ std::string circuit_id_;
+
+ /// @brief Extra options the client will send.
+ OptionCollection extra_options_;
+
+ /// @brief Extra classes to add to the query.
+ ClientClasses classes_;
+};
+
+} // end of namespace isc::dhcp::test
+} // end of namespace isc::dhcp
+} // end of namespace isc
+
+#endif // DHCP4_CLIENT_H
diff --git a/src/bin/dhcp4/tests/dhcp4_process_tests.sh.in b/src/bin/dhcp4/tests/dhcp4_process_tests.sh.in
new file mode 100644
index 0000000..76e7177
--- /dev/null
+++ b/src/bin/dhcp4/tests/dhcp4_process_tests.sh.in
@@ -0,0 +1,591 @@
+#!/bin/sh
+
+# Copyright (C) 2014-2023 Internet Systems Consortium, Inc. ("ISC")
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# shellcheck disable=SC1091
+# SC1091: Not following: ... was not specified as input (see shellcheck -x).
+
+# shellcheck disable=SC2039
+# SC2039: In POSIX sh, 'local' is undefined.
+
+# Exit with error if commands exit with non-zero and if undefined variables are
+# used.
+set -eu
+
+# Path to the temporary configuration file.
+CFG_FILE="@abs_top_builddir@/src/bin/dhcp4/tests/test_config.json"
+# Path to the Kea log file.
+LOG_FILE="@abs_top_builddir@/src/bin/dhcp4/tests/test.log"
+# Path to the Kea lease file.
+LEASE_FILE="@abs_top_builddir@/src/bin/dhcp4/tests/test_leases.csv"
+# Path to the Kea LFC application
+export KEA_LFC_EXECUTABLE="@abs_top_builddir@/src/bin/lfc/kea-lfc"
+# Path to test hooks library
+HOOK_PATH="@abs_top_builddir@/src/bin/dhcp4/tests/.libs/libco3.so"
+# Kea configuration to be stored in the configuration file.
+CONFIG="{
+ \"Dhcp4\":
+ {
+ \"interfaces-config\": {
+ \"interfaces\": [ ]
+ },
+ \"valid-lifetime\": 4000,
+ \"renew-timer\": 1000,
+ \"rebind-timer\": 2000,
+ \"lease-database\":
+ {
+ \"type\": \"memfile\",
+ \"name\": \"$LEASE_FILE\",
+ \"persist\": false,
+ \"lfc-interval\": 0
+ },
+ \"subnet4\": [
+ {
+ \"id\": 1,
+ \"subnet\": \"10.0.0.0/8\",
+ \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ]
+ } ],
+ \"dhcp-ddns\": {
+ \"enable-updates\": true,
+ \"qualifying-suffix\": \"\"
+ },
+ \"loggers\": [
+ {
+ \"name\": \"kea-dhcp4\",
+ \"output_options\": [
+ {
+ \"output\": \"$LOG_FILE\"
+ }
+ ],
+ \"severity\": \"INFO\"
+ }
+ ]
+ }
+}"
+
+# Invalid configuration (syntax error) to check that Kea can check syntax.
+# This config has following errors:
+# - it should be interfaces-config/interfaces, not interfaces
+# - it should be subnet4/pools, no subnet4/pool
+CONFIG_BAD_SYNTAX="{
+ \"Dhcp4\":
+ {
+ \"interfaces\": [ ],
+ \"valid-lifetime\": 4000,
+ \"renew-timer\": 1000,
+ \"rebind-timer\": 2000,
+ \"lease-database\":
+ {
+ \"type\": \"memfile\",
+ \"persist\": false
+ },
+ \"subnet4\": [
+ {
+ \"id\": 1,
+ \"subnet\": \"10.0.0.0/8\",
+ \"pool\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ]
+ } ],
+ \"loggers\": [
+ {
+ \"name\": \"kea-dhcp4\",
+ \"output_options\": [
+ {
+ \"output\": \"$LOG_FILE\"
+ }
+ ],
+ \"severity\": \"INFO\"
+ }
+ ]
+ }
+}"
+
+# This config has bad pool values. The pool it out of scope for the subnet
+# it is defined in. Syntactically the config is correct, though.
+CONFIG_BAD_VALUES="{
+ \"Dhcp4\":
+ {
+ \"interfaces-config\": {
+ \"interfaces\": [ ]
+ },
+ \"valid-lifetime\": 4000,
+ \"renew-timer\": 1000,
+ \"rebind-timer\": 2000,
+ \"lease-database\":
+ {
+ \"type\": \"memfile\",
+ \"persist\": false
+ },
+ \"subnet4\": [
+ {
+ \"id\": 1,
+ \"subnet\": \"10.0.0.0/8\",
+ \"pools\": [ { \"pool\": \"192.168.0.10-192.168.0.100\" } ]
+ } ],
+ \"loggers\": [
+ {
+ \"name\": \"kea-dhcp4\",
+ \"output_options\": [
+ {
+ \"output\": \"$LOG_FILE\"
+ }
+ ],
+ \"severity\": \"INFO\"
+ }
+ ]
+ }
+}"
+
+# Invalid configuration (negative valid-lifetime) to check that Kea
+# gracefully handles reconfiguration errors.
+CONFIG_INVALID="{
+ \"Dhcp4\":
+ {
+ \"interfaces-config\": {
+ \"interfaces\": [ ]
+ },
+ \"valid-lifetime\": -3,
+ \"renew-timer\": 1000,
+ \"rebind-timer\": 2000,
+ \"lease-database\":
+ {
+ \"type\": \"memfile\",
+ \"persist\": false
+ },
+ \"subnet4\": [
+ {
+ \"id\": 1,
+ \"subnet\": \"10.0.0.0/8\",
+ \"pool\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ]
+ } ],
+ \"loggers\": [
+ {
+ \"name\": \"kea-dhcp4\",
+ \"output_options\": [
+ {
+ \"output\": \"$LOG_FILE\"
+ }
+ ],
+ \"severity\": \"INFO\"
+ }
+ ]
+ }
+}"
+
+# Invalid configuration (hook explicitly fails to load) to check that performing
+# extra configuration checks detects the error.
+INVALID_CONFIG_HOOKS_LOAD="{
+ \"Dhcp4\":
+ {
+ \"interfaces-config\": {
+ \"interfaces\": [ ]
+ },
+ \"multi-threading\": {
+ \"enable-multi-threading\": false
+ },
+ \"valid-lifetime\": 4000,
+ \"renew-timer\": 1000,
+ \"rebind-timer\": 2000,
+ \"lease-database\":
+ {
+ \"type\": \"memfile\",
+ \"name\": \"$LEASE_FILE\",
+ \"persist\": false,
+ \"lfc-interval\": 0
+ },
+ \"subnet4\": [
+ {
+ \"id\": 1,
+ \"subnet\": \"10.0.0.0/8\",
+ \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ]
+ } ],
+ \"dhcp-ddns\": {
+ \"enable-updates\": true,
+ \"qualifying-suffix\": \"\"
+ },
+ \"hooks-libraries\": [
+ {
+ \"library\": \"$HOOK_PATH\",
+ \"parameters\": {
+ \"mode\": \"fail-on-load\"
+ }
+ } ],
+ \"loggers\": [
+ {
+ \"name\": \"kea-dhcp4\",
+ \"output_options\": [
+ {
+ \"output\": \"$LOG_FILE\"
+ }
+ ],
+ \"severity\": \"INFO\"
+ }
+ ]
+ }
+}"
+
+# Invalid configuration (hook point returns error) to check that performing
+# extra configuration checks detects the error.
+INVALID_CONFIG_HOOKS_CALLOUT_FAIL="{
+ \"Dhcp4\":
+ {
+ \"interfaces-config\": {
+ \"interfaces\": [ ]
+ },
+ \"multi-threading\": {
+ \"enable-multi-threading\": false
+ },
+ \"valid-lifetime\": 4000,
+ \"renew-timer\": 1000,
+ \"rebind-timer\": 2000,
+ \"lease-database\":
+ {
+ \"type\": \"memfile\",
+ \"name\": \"$LEASE_FILE\",
+ \"persist\": false,
+ \"lfc-interval\": 0
+ },
+ \"subnet4\": [
+ {
+ \"id\": 1,
+ \"subnet\": \"10.0.0.0/8\",
+ \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ]
+ } ],
+ \"dhcp-ddns\": {
+ \"enable-updates\": true,
+ \"qualifying-suffix\": \"\"
+ },
+ \"hooks-libraries\": [
+ {
+ \"library\": \"$HOOK_PATH\",
+ \"parameters\": {
+ \"mode\": \"fail-without-error\"
+ }
+ } ],
+ \"loggers\": [
+ {
+ \"name\": \"kea-dhcp4\",
+ \"output_options\": [
+ {
+ \"output\": \"$LOG_FILE\"
+ }
+ ],
+ \"severity\": \"INFO\"
+ }
+ ]
+ }
+}"
+
+# Set the location of the executable.
+bin="kea-dhcp4"
+bin_path="@abs_top_builddir@/src/bin/dhcp4"
+
+# Import common test library.
+. "@abs_top_builddir@/src/lib/testutils/dhcp_test_lib.sh"
+
+# This test verifies that syntax checking works properly. This function
+# requires 3 parameters:
+# test_name
+# config - string with a content of the config (will be written to a file)
+# expected_code - expected exit code returned by kea (0 - success, 1 - failure)
+syntax_check_test() {
+ local test_name="${1}"
+ local config="${2}"
+ local expected_code="${3}"
+ local check_type="${4}"
+
+ # Log the start of the test and print test name.
+ test_start "${test_name}"
+ # Create correct configuration file.
+ create_config "${config}"
+ # Check it
+ printf "Running command %s.\n" "\"${bin_path}/${bin} ${check_type} ${CFG_FILE}\""
+ run_command \
+ "${bin_path}/${bin}" "${check_type}" "${CFG_FILE}"
+ if [ "${EXIT_CODE}" -ne "${expected_code}" ]; then
+ printf 'ERROR: expected exit code %s, got %s\n' "${expected_code}" "${EXIT_CODE}"
+ clean_exit 1
+ fi
+ test_finish 0
+}
+
+# This test verifies that DHCPv4 can be reconfigured with a SIGHUP signal.
+dynamic_reconfiguration_test() {
+ # Log the start of the test and print test name.
+ test_start "dhcpv4_srv.dynamic_reconfiguration"
+ # Create new configuration file.
+ create_config "${CONFIG}"
+ # Instruct Kea to log to the specific file.
+ set_logger
+ # Start Kea.
+ start_kea ${bin_path}/${bin}
+ # Wait up to 20s for Kea to start.
+ wait_for_kea 20
+ if [ "${_WAIT_FOR_KEA}" -eq 0 ]; then
+ printf "ERROR: timeout waiting for Kea to start.\n"
+ clean_exit 1
+ fi
+
+ # Check if it is still running. It could have terminated (e.g. as a result
+ # of configuration failure).
+ get_pid ${bin}
+ if [ "${_GET_PIDS_NUM}" -ne 1 ]; then
+ printf "ERROR: expected one Kea process to be started. Found %d processes\
+ started.\n" "${_GET_PIDS_NUM}"
+ clean_exit 1
+ fi
+
+ # Check in the log file, how many times server has been configured. It should
+ # be just once on startup.
+ get_reconfigs
+ if [ "${_GET_RECONFIGS}" -ne 1 ]; then
+ printf "ERROR: server hasn't been configured.\n"
+ clean_exit 1
+ else
+ printf "Server successfully configured.\n"
+ fi
+
+ # Now use invalid configuration.
+ create_config "${CONFIG_INVALID}"
+
+ # Try to reconfigure by sending SIGHUP
+ send_signal 1 ${bin}
+
+ # The configuration should fail and the error message should be there.
+ wait_for_message 10 "DHCP4_CONFIG_LOAD_FAIL" 1
+
+ # After receiving SIGHUP the server should try to reconfigure itself.
+ # The configuration provided is invalid so it should result in
+ # reconfiguration failure but the server should still be running.
+ get_reconfigs
+ if [ "${_GET_RECONFIGS}" -ne 1 ]; then
+ printf "ERROR: server has been reconfigured despite bogus configuration.\n"
+ clean_exit 1
+ elif [ "${_GET_RECONFIG_ERRORS}" -ne 1 ]; then
+ printf "ERROR: server did not report reconfiguration error despite attempt\
+ to configure it with invalid configuration.\n"
+ clean_exit 1
+ fi
+
+ # Make sure the server is still operational.
+ get_pid ${bin}
+ if [ "${_GET_PIDS_NUM}" -ne 1 ]; then
+ printf "ERROR: Kea process was killed when attempting reconfiguration.\n"
+ clean_exit 1
+ fi
+
+ # Restore the good configuration.
+ create_config "${CONFIG}"
+
+ # Reconfigure the server with SIGHUP.
+ send_signal 1 ${bin}
+
+ # There should be two occurrences of the DHCP4_CONFIG_COMPLETE messages.
+ # Wait for it up to 10s.
+ wait_for_message 10 "DHCP4_CONFIG_COMPLETE" 2
+
+ # After receiving SIGHUP the server should get reconfigured and the
+ # reconfiguration should be noted in the log file. We should now
+ # have two configurations logged in the log file.
+ if [ "${_WAIT_FOR_MESSAGE}" -eq 0 ]; then
+ printf "ERROR: server hasn't been reconfigured.\n"
+ clean_exit 1
+ else
+ printf "Server successfully reconfigured.\n"
+ fi
+
+ # Make sure the server is still operational.
+ get_pid ${bin}
+ if [ "${_GET_PIDS_NUM}" -ne 1 ]; then
+ printf "ERROR: Kea process was killed when attempting reconfiguration.\n"
+ clean_exit 1
+ fi
+
+ # When the server receives a signal the call to select() function is
+ # interrupted. This should not be logged as an error.
+ get_log_messages "DHCP4_PACKET_RECEIVE_FAIL"
+ assert_eq 0 "${_GET_LOG_MESSAGES}" \
+ "Expected get_log_messages DHCP4_PACKET_RECEIVE_FAIL return %d, \
+returned %d."
+
+ # All ok. Shut down Kea and exit.
+ test_finish 0
+}
+
+# This test verifies that DHCPv4 server is shut down gracefully when it
+# receives a SIGINT or SIGTERM signal.
+shutdown_test() {
+ test_name=${1} # Test name
+ signum=${2} # Signal number
+ # Log the start of the test and print test name.
+ test_start "${test_name}"
+ # Create new configuration file.
+ create_config "${CONFIG}"
+ # Instruct Kea to log to the specific file.
+ set_logger
+ # Start Kea.
+ start_kea ${bin_path}/${bin}
+ # Wait up to 20s for Kea to start.
+ wait_for_kea 20
+ if [ "${_WAIT_FOR_KEA}" -eq 0 ]; then
+ printf "ERROR: timeout waiting for Kea to start.\n"
+ clean_exit 1
+ fi
+
+ # Check if it is still running. It could have terminated (e.g. as a result
+ # of configuration failure).
+ get_pid ${bin}
+ if [ "${_GET_PIDS_NUM}" -ne 1 ]; then
+ printf "ERROR: expected one Kea process to be started. Found %d processes\
+ started.\n" "${_GET_PIDS_NUM}"
+ clean_exit 1
+ fi
+
+ # Check in the log file, how many times server has been configured. It should
+ # be just once on startup.
+ get_reconfigs
+ if [ "${_GET_RECONFIGS}" -ne 1 ]; then
+ printf "ERROR: server hasn't been configured.\n"
+ clean_exit 1
+ else
+ printf "Server successfully configured.\n"
+ fi
+
+ # Send signal to Kea (SIGTERM, SIGINT etc.)
+ send_signal "${signum}" "${bin}"
+
+ # Wait up to 10s for the server's graceful shutdown. The graceful shut down
+ # should be recorded in the log file with the appropriate message.
+ wait_for_message 10 "DHCP4_SHUTDOWN" 1
+ if [ "${_WAIT_FOR_MESSAGE}" -eq 0 ]; then
+ printf "ERROR: Server did not record shutdown in the log.\n"
+ clean_exit 1
+ fi
+
+ # Make sure the server is down.
+ wait_for_server_down 5 ${bin}
+ assert_eq 1 "${_WAIT_FOR_SERVER_DOWN}" \
+ "Expected wait_for_server_down return %d, returned %d"
+
+ # When the server receives a signal the call to select() function is
+ # interrupted. This should not be logged as an error.
+ get_log_messages "DHCP4_PACKET_RECEIVE_FAIL"
+ assert_eq 0 "${_GET_LOG_MESSAGES}" \
+ "Expected get_log_messages return %d, returned %d."
+
+ test_finish 0
+}
+
+# This test verifies that DHCPv4 can be configured to run lease file cleanup
+# periodically.
+lfc_timer_test() {
+ # Log the start of the test and print test name.
+ test_start "dhcpv4_srv.lfc_timer_test"
+ # Create a configuration with the LFC enabled, by replacing the section
+ # with the lfc-interval and persist parameters.
+ LFC_CONFIG=$(printf '%s' "${CONFIG}" | sed -e 's/\"lfc-interval\": 0/\"lfc-interval\": 3/g' \
+ | sed -e 's/\"persist\": false/\"persist\": true/g')
+ # Create new configuration file.
+ create_config "${LFC_CONFIG}"
+ # Instruct Kea to log to the specific file.
+ set_logger
+ # Start Kea.
+ start_kea ${bin_path}/${bin}
+ # Wait up to 20s for Kea to start.
+ wait_for_kea 20
+ if [ "${_WAIT_FOR_KEA}" -eq 0 ]; then
+ printf "ERROR: timeout waiting for Kea to start.\n"
+ clean_exit 1
+ fi
+
+ # Check if it is still running. It could have terminated (e.g. as a result
+ # of configuration failure).
+ get_pid ${bin}
+ if [ "${_GET_PIDS_NUM}" -ne 1 ]; then
+ printf "ERROR: expected one Kea process to be started. Found %d processes\
+ started.\n" "${_GET_PIDS_NUM}"
+ clean_exit 1
+ fi
+
+ # Check if Kea emits the log message indicating that LFC is started.
+ wait_for_message 10 "DHCPSRV_MEMFILE_LFC_EXECUTE" 1
+ if [ "${_WAIT_FOR_MESSAGE}" -eq 0 ]; then
+ printf "ERROR: Server did not execute LFC.\n"
+ clean_exit 1
+ fi
+
+ # Give it a short time to run.
+ sleep 1
+
+ # Modify the interval.
+ LFC_CONFIG=$(printf '%s' "${LFC_CONFIG}" | sed -e 's/\"lfc-interval\": 3/\"lfc-interval\": 4/g')
+ # Create new configuration file.
+ create_config "${LFC_CONFIG}"
+
+ # Reconfigure the server with SIGHUP.
+ send_signal 1 ${bin}
+
+ # There should be two occurrences of the DHCP4_CONFIG_COMPLETE messages.
+ # Wait for it up to 10s.
+ wait_for_message 10 "DHCP4_CONFIG_COMPLETE" 2
+
+ # After receiving SIGHUP the server should get reconfigured and the
+ # reconfiguration should be noted in the log file. We should now
+ # have two configurations logged in the log file.
+ if [ "${_WAIT_FOR_MESSAGE}" -eq 0 ]; then
+ printf "ERROR: server hasn't been reconfigured.\n"
+ clean_exit 1
+ else
+ printf "Server successfully reconfigured.\n"
+ fi
+
+ # Make sure the server is still operational.
+ get_pid ${bin}
+ if [ "${_GET_PIDS_NUM}" -ne 1 ]; then
+ printf "ERROR: Kea process was killed when attempting reconfiguration.\n"
+ clean_exit 1
+ fi
+
+ # Wait for the LFC to run the second time.
+ wait_for_message 10 "DHCPSRV_MEMFILE_LFC_EXECUTE" 2
+ if [ "${_WAIT_FOR_MESSAGE}" -eq 0 ]; then
+ printf "ERROR: Server did not execute LFC.\n"
+ clean_exit 1
+ fi
+
+ # Send signal to Kea SIGTERM
+ send_signal 15 ${bin}
+
+ # Wait up to 10s for the server's graceful shutdown. The graceful shut down
+ # should be recorded in the log file with the appropriate message.
+ wait_for_message 10 "DHCP4_SHUTDOWN" 1
+ if [ "${_WAIT_FOR_MESSAGE}" -eq 0 ]; then
+ printf "ERROR: Server did not record shutdown in the log.\n"
+ clean_exit 1
+ fi
+
+ # Make sure the server is down.
+ wait_for_server_down 5 ${bin}
+ assert_eq 1 "${_WAIT_FOR_SERVER_DOWN}" \
+ "Expected wait_for_server_down return %d, returned %d"
+
+ # All ok. Shut down Kea and exit.
+ test_finish 0
+}
+
+server_pid_file_test "${CONFIG}" DHCP4_ALREADY_RUNNING
+dynamic_reconfiguration_test
+shutdown_test "dhcpv4.sigterm_test" 15
+shutdown_test "dhcpv4.sigint_test" 2
+version_test "dhcpv4.version"
+logger_vars_test "dhcpv4.variables"
+lfc_timer_test
+syntax_check_test "dhcpv4.syntax_check_success" "${CONFIG}" 0 -t
+syntax_check_test "dhcpv4.syntax_check_bad_syntax" "${CONFIG_BAD_SYNTAX}" 1 -t
+syntax_check_test "dhcpv4.syntax_check_bad_values" "${CONFIG_BAD_VALUES}" 1 -t
+syntax_check_test "dhcpv4.syntax_check_hooks_load_fail" "${INVALID_CONFIG_HOOKS_LOAD}" 1 -T
+syntax_check_test "dhcpv4.syntax_check_hooks_callout_fail" "${INVALID_CONFIG_HOOKS_CALLOUT_FAIL}" 1 -T
+password_redact_test "dhcpv4.password_redact_test" "$(kea_dhcp_config 4)" 0
diff --git a/src/bin/dhcp4/tests/dhcp4_srv_unittest.cc b/src/bin/dhcp4/tests/dhcp4_srv_unittest.cc
new file mode 100644
index 0000000..7a1c460
--- /dev/null
+++ b/src/bin/dhcp4/tests/dhcp4_srv_unittest.cc
@@ -0,0 +1,5222 @@
+// Copyright (C) 2011-2023 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <asiolink/io_address.h>
+#include <cc/command_interpreter.h>
+#include <config/command_mgr.h>
+#include <config_backend/base_config_backend.h>
+#include <dhcp4/dhcp4_log.h>
+#include <dhcp4/dhcp4_srv.h>
+#include <dhcp4/json_config_parser.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <dhcp4/tests/dhcp4_client.h>
+#include <dhcp/tests/pkt_captures.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/iface_mgr.h>
+#include <dhcp/libdhcp++.h>
+#include <dhcp/option.h>
+#include <dhcp/option_int.h>
+#include <dhcp/option4_addrlst.h>
+#include <dhcp/option_custom.h>
+#include <dhcp/option_int_array.h>
+#include <dhcp/pkt_filter.h>
+#include <dhcp/pkt_filter_inet.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/lease_mgr.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <dhcpsrv/utils.h>
+#include <dhcpsrv/host_mgr.h>
+#include <stats/stats_mgr.h>
+#include <testutils/gtest_utils.h>
+#include <util/encode/hex.h>
+
+#ifdef HAVE_MYSQL
+#include <mysql/testutils/mysql_schema.h>
+#endif
+
+#ifdef HAVE_PGSQL
+#include <pgsql/testutils/pgsql_schema.h>
+#endif
+
+#include <boost/scoped_ptr.hpp>
+
+#include <iostream>
+#include <cstdlib>
+#include <sstream>
+
+#include <arpa/inet.h>
+#include <dirent.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::data;
+using namespace isc::asiolink;
+using namespace isc::cb;
+using namespace isc::config;
+using namespace isc::dhcp::test;
+using namespace isc::util;
+
+namespace {
+
+const char* CONFIGS[] = {
+ // Configuration 0:
+ // - 1 subnet: 10.254.226.0/25
+ // - used for recorded traffic (see PktCaptures::captureRelayedDiscover)
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.254.226.0/25\" } ],"
+ " \"subnet\": \"10.254.226.0/24\", "
+ " \"rebind-timer\": 2000, "
+ " \"renew-timer\": 1000, "
+ " \"valid-lifetime\": 4000,"
+ " \"interface\": \"eth0\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }",
+
+ // Configuration 1:
+ // - 1 subnet: 192.0.2.0/24
+ // - MySQL Host Data Source configured
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"hosts-database\": {"
+ " \"type\": \"mysql\","
+ " \"name\": \"keatest\","
+ " \"user\": \"keatest\","
+ " \"password\": \"keatest\""
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"rebind-timer\": 2000, "
+ " \"renew-timer\": 1000, "
+ " \"valid-lifetime\": 4000,"
+ " \"interface\": \"eth0\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }",
+
+ // Configuration 2:
+ // - 1 subnet, 2 global options (one forced with always-send)
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ] }, "
+ " \"rebind-timer\": 2000, "
+ " \"renew-timer\": 1000, "
+ " \"valid-lifetime\": 4000, "
+ " \"subnet4\": [ {"
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], "
+ " \"subnet\": \"192.0.2.0/24\""
+ " } ], "
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"default-ip-ttl\", "
+ " \"data\": \"FF\", "
+ " \"csv-format\": false"
+ " }, "
+ " {"
+ " \"name\": \"ip-forwarding\", "
+ " \"data\": \"false\", "
+ " \"always-send\": true"
+ " }"
+ " ]"
+ "}",
+
+ // Configuration 3:
+ // - 1 subnet with never-send option
+ // - 2 global options (one forced with always-send)
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ] }, "
+ " \"rebind-timer\": 2000, "
+ " \"renew-timer\": 1000, "
+ " \"valid-lifetime\": 4000, "
+ " \"subnet4\": [ {"
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], "
+ " \"subnet\": \"192.0.2.0/24\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"ip-forwarding\", "
+ " \"never-send\": true"
+ " }"
+ " ]"
+ " } ], "
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"default-ip-ttl\", "
+ " \"data\": \"FF\", "
+ " \"csv-format\": false"
+ " }, "
+ " {"
+ " \"name\": \"ip-forwarding\", "
+ " \"data\": \"false\", "
+ " \"always-send\": true"
+ " }"
+ " ]"
+ "}",
+
+ // Configuration 4:
+ // - one subnet, with one pool
+ // - user-contexts defined in both subnet and pool
+ "{"
+ " \"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.254.226.0/25\","
+ " \"user-context\": { \"value\": 42 } } ],"
+ " \"subnet\": \"10.254.226.0/24\", "
+ " \"user-context\": {"
+ " \"secure\": false"
+ " }"
+ " } ],"
+ "\"valid-lifetime\": 4000 }",
+};
+
+// Convenience function for comparing option buffer to an expected string value
+// @param exp_string expected string value
+// @param buffer OptionBuffer whose contents are to be tested
+void checkStringInBuffer( const std::string& exp_string, const OptionBuffer& buffer) {
+ std::string buffer_string(buffer.begin(), buffer.end());
+ EXPECT_EQ(exp_string, std::string(buffer_string.c_str()));
+}
+
+// This test verifies that the destination address of the response
+// message is set to giaddr, when giaddr is set to non-zero address
+// in the received message.
+TEST_F(Dhcpv4SrvTest, adjustIfaceDataRelay) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Create the instance of the incoming packet.
+ boost::shared_ptr<Pkt4> req(new Pkt4(DHCPDISCOVER, 1234));
+ // Set the giaddr to non-zero address and hops to non-zero value
+ // as if it was relayed.
+ req->setGiaddr(IOAddress("192.0.1.1"));
+ req->setHops(2);
+ // Set ciaddr to zero. This simulates the client which applies
+ // for the new lease.
+ req->setCiaddr(IOAddress("0.0.0.0"));
+ // Clear broadcast flag.
+ req->setFlags(0x0000);
+
+ // Set local address, port and interface.
+ req->setLocalAddr(IOAddress("192.0.2.5"));
+ req->setLocalPort(1001);
+ req->setIface("eth1");
+ req->setIndex(ETH1_INDEX);
+
+ // Set remote port (it will be used in the next test).
+ req->setRemotePort(1234);
+
+ // Create the exchange using the req.
+ Dhcpv4Exchange ex = createExchange(req);
+
+ Pkt4Ptr resp = ex.getResponse();
+ resp->setYiaddr(IOAddress("192.0.1.100"));
+ // Clear the remote address.
+ resp->setRemoteAddr(IOAddress("0.0.0.0"));
+ // Set hops value for the response.
+ resp->setHops(req->getHops());
+
+ // This function never throws.
+ ASSERT_NO_THROW(srv_.adjustIfaceData(ex));
+
+ // Now the destination address should be relay's address.
+ EXPECT_EQ("192.0.1.1", resp->getRemoteAddr().toText());
+ // The query has been relayed, so the response must be sent to the port 67.
+ EXPECT_EQ(DHCP4_SERVER_PORT, resp->getRemotePort());
+ // Local address should be the address assigned to interface eth1.
+ EXPECT_EQ("192.0.2.5", resp->getLocalAddr().toText());
+ // The local port is always DHCPv4 server port 67.
+ EXPECT_EQ(DHCP4_SERVER_PORT, resp->getLocalPort());
+ // We will send response over the same interface which was used to receive
+ // query.
+ EXPECT_EQ("eth1", resp->getIface());
+ EXPECT_EQ(ETH1_INDEX, resp->getIndex());
+
+ // Let's do another test and set other fields: ciaddr and
+ // flags. By doing it, we want to make sure that the relay
+ // address will take precedence.
+ req->setGiaddr(IOAddress("192.0.1.50"));
+ req->setCiaddr(IOAddress("192.0.1.11"));
+ req->setFlags(Pkt4::FLAG_BROADCAST_MASK);
+
+ resp->setYiaddr(IOAddress("192.0.1.100"));
+ // Clear remote address.
+ resp->setRemoteAddr(IOAddress("0.0.0.0"));
+
+ // Set the client and server ports.
+ srv_.client_port_ = 1234;
+ srv_.server_port_ = 2345;
+
+ ASSERT_NO_THROW(srv_.adjustIfaceData(ex));
+
+ // Response should be sent back to the relay address.
+ EXPECT_EQ("192.0.1.50", resp->getRemoteAddr().toText());
+
+ // Remote port was enforced to the client port.
+ EXPECT_EQ(srv_.client_port_, resp->getRemotePort());
+
+ // Local port was enforced to the server port.
+ EXPECT_EQ(srv_.server_port_, resp->getLocalPort());
+}
+
+// This test verifies that the remote port is adjusted when
+// the query carries a relay port RAI sub-option.
+TEST_F(Dhcpv4SrvTest, adjustIfaceDataRelayPort) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Create the instance of the incoming packet.
+ boost::shared_ptr<Pkt4> req(new Pkt4(DHCPDISCOVER, 1234));
+ // Set the giaddr to non-zero address and hops to non-zero value
+ // as if it was relayed.
+ req->setGiaddr(IOAddress("192.0.1.1"));
+ req->setHops(2);
+ // Set ciaddr to zero. This simulates the client which applies
+ // for the new lease.
+ req->setCiaddr(IOAddress("0.0.0.0"));
+ // Clear broadcast flag.
+ req->setFlags(0x0000);
+
+ // Set local address, port and interface.
+ req->setLocalAddr(IOAddress("192.0.2.5"));
+ req->setLocalPort(1001);
+ req->setIface("eth1");
+ req->setIndex(ETH1_INDEX);
+
+ // Set remote port.
+ req->setRemotePort(1234);
+
+ // Add a RAI relay-port sub-option (the only difference with the previous test).
+ OptionDefinitionPtr rai_def =
+ LibDHCP::getOptionDef(DHCP4_OPTION_SPACE, DHO_DHCP_AGENT_OPTIONS);
+ ASSERT_TRUE(rai_def);
+ OptionCustomPtr rai(new OptionCustom(*rai_def, Option::V4));
+ ASSERT_TRUE(rai);
+ req->addOption(rai);
+ OptionPtr relay_port(new Option(Option::V4, RAI_OPTION_RELAY_PORT));
+ ASSERT_TRUE(relay_port);
+ rai->addOption(relay_port);
+
+ // Create the exchange using the req.
+ Dhcpv4Exchange ex = createExchange(req);
+
+ Pkt4Ptr resp = ex.getResponse();
+ resp->setYiaddr(IOAddress("192.0.1.100"));
+ // Clear the remote address.
+ resp->setRemoteAddr(IOAddress("0.0.0.0"));
+ // Set hops value for the response.
+ resp->setHops(req->getHops());
+
+ // Set the remote port to 67 as we know it will be updated.
+ resp->setRemotePort(67);
+
+ // This function never throws.
+ ASSERT_NO_THROW(srv_.adjustIfaceData(ex));
+
+ // Now the destination address should be relay's address.
+ EXPECT_EQ("192.0.1.1", resp->getRemoteAddr().toText());
+ // The query has been relayed, so the response should be sent to the
+ // port 67, but here there is a relay port RAI so another value is used.
+ EXPECT_EQ(1234, resp->getRemotePort());
+ // Local address should be the address assigned to interface eth1.
+ EXPECT_EQ("192.0.2.5", resp->getLocalAddr().toText());
+ // The local port is always DHCPv4 server port 67.
+ EXPECT_EQ(DHCP4_SERVER_PORT, resp->getLocalPort());
+ // We will send response over the same interface which was used to receive
+ // query.
+ EXPECT_EQ("eth1", resp->getIface());
+ EXPECT_EQ(ETH1_INDEX, resp->getIndex());
+}
+
+// This test verifies that it is possible to configure the server to use
+// routing information to determine the right outbound interface to sent
+// responses to a relayed client.
+TEST_F(Dhcpv4SrvTest, adjustIfaceDataUseRouting) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Create configuration for interfaces. It includes the outbound-interface
+ // setting which indicates that the responses aren't necessarily sent
+ // over the same interface via which a request has been received, but routing
+ // information is used to determine this interface.
+ CfgMgr::instance().clear();
+ CfgIfacePtr cfg_iface = CfgMgr::instance().getStagingCfg()->getCfgIface();
+ cfg_iface->useSocketType(AF_INET, CfgIface::SOCKET_UDP);
+ cfg_iface->use(AF_INET, "eth0");
+ cfg_iface->use(AF_INET, "eth1");
+ cfg_iface->setOutboundIface(CfgIface::USE_ROUTING);
+ CfgMgr::instance().commit();;
+
+ // Create the instance of the incoming packet.
+ boost::shared_ptr<Pkt4> req(new Pkt4(DHCPDISCOVER, 1234));
+ // Set the giaddr to non-zero address and hops to non-zero value
+ // as if it was relayed.
+ req->setGiaddr(IOAddress("192.0.1.1"));
+ req->setHops(2);
+ // Set ciaddr to zero. This simulates the client which applies
+ // for the new lease.
+ req->setCiaddr(IOAddress("0.0.0.0"));
+ // Clear broadcast flag.
+ req->setFlags(0x0000);
+
+ // Set local address, port and interface.
+ req->setLocalAddr(IOAddress("192.0.2.5"));
+ req->setLocalPort(1001);
+ req->setIface("eth1");
+ req->setIndex(ETH1_INDEX);
+
+ // Create the exchange using the req.
+ Dhcpv4Exchange ex = createExchange(req);
+
+ Pkt4Ptr resp = ex.getResponse();
+ resp->setYiaddr(IOAddress("192.0.1.100"));
+ // Clear the remote address.
+ resp->setRemoteAddr(IOAddress("0.0.0.0"));
+ // Set hops value for the response.
+ resp->setHops(req->getHops());
+
+ // This function never throws.
+ ASSERT_NO_THROW(srv_.adjustIfaceData(ex));
+
+ // Now the destination address should be relay's address.
+ EXPECT_EQ("192.0.1.1", resp->getRemoteAddr().toText());
+ // The query has been relayed, so the response must be sent to the port 67.
+ EXPECT_EQ(DHCP4_SERVER_PORT, resp->getRemotePort());
+
+ // The local port is always DHCPv4 server port 67.
+ EXPECT_EQ(DHCP4_SERVER_PORT, resp->getLocalPort());
+
+ // No specific interface is selected as outbound interface and no specific
+ // local address is provided. The IfaceMgr will figure out which interface to use.
+ EXPECT_TRUE(resp->getLocalAddr().isV4Zero());
+ EXPECT_FALSE(resp->indexSet());
+
+ // Fixed in #5515 so now the interface name is never empty.
+ EXPECT_FALSE(resp->getIface().empty());
+
+ // Another test verifies that setting outbound interface to same as inbound will
+ // cause the server to set interface and local address as expected.
+
+ cfg_iface = CfgMgr::instance().getStagingCfg()->getCfgIface();
+ cfg_iface->useSocketType(AF_INET, CfgIface::SOCKET_UDP);
+ cfg_iface->use(AF_INET, "eth0");
+ cfg_iface->use(AF_INET, "eth1");
+ cfg_iface->setOutboundIface(CfgIface::SAME_AS_INBOUND);
+ CfgMgr::instance().commit();
+
+ ASSERT_NO_THROW(srv_.adjustIfaceData(ex));
+
+ EXPECT_EQ("192.0.2.5", resp->getLocalAddr().toText());
+ EXPECT_EQ("eth1", resp->getIface());
+ EXPECT_EQ(ETH1_INDEX, resp->getIndex());
+}
+
+// This test verifies that the destination address of the response
+// message is set to source address when the testing mode is enabled.
+// Relayed message: not testing mode was tested in adjustIfaceDataRelay.
+TEST_F(Dhcpv4SrvTest, adjustRemoteAddressRelaySendToSourceTestingModeEnabled) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Create the instance of the incoming packet.
+ boost::shared_ptr<Pkt4> req(new Pkt4(DHCPDISCOVER, 1234));
+ // Set the giaddr to non-zero address and hops to non-zero value
+ // as if it was relayed.
+ req->setGiaddr(IOAddress("192.0.1.1"));
+ req->setHops(2);
+ // Set ciaddr to zero. This simulates the client which applies
+ // for the new lease.
+ req->setCiaddr(IOAddress("0.0.0.0"));
+ // Clear broadcast flag.
+ req->setFlags(0x0000);
+
+ // Set local address, port and interface.
+ req->setLocalAddr(IOAddress("192.0.2.5"));
+ req->setLocalPort(1001);
+ req->setIface("eth1");
+ req->setIndex(ETH1_INDEX);
+
+ // Set remote address and port.
+ req->setRemoteAddr(IOAddress("192.0.2.1"));
+ req->setRemotePort(1234);
+
+ // Create the exchange using the req.
+ Dhcpv4Exchange ex = createExchange(req);
+
+ Pkt4Ptr resp = ex.getResponse();
+ resp->setYiaddr(IOAddress("192.0.1.100"));
+ // Clear the remote address.
+ resp->setRemoteAddr(IOAddress("0.0.0.0"));
+ // Set hops value for the response.
+ resp->setHops(req->getHops());
+
+ // Set the testing mode.
+ srv_.setSendResponsesToSource(true);
+
+ // This function never throws.
+ ASSERT_NO_THROW(srv_.adjustIfaceData(ex));
+
+ // Now the destination address should be source address.
+ EXPECT_EQ("192.0.2.1", resp->getRemoteAddr().toText());
+}
+
+// This test verifies that the destination address of the response message
+// is set to ciaddr when giaddr is set to zero and the ciaddr is set to
+// non-zero address in the received message. This is the case when the
+// client is in Renew or Rebind state.
+TEST_F(Dhcpv4SrvTest, adjustIfaceDataRenew) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Create instance of the incoming packet.
+ boost::shared_ptr<Pkt4> req(new Pkt4(DHCPDISCOVER, 1234));
+
+ // Clear giaddr to simulate direct packet.
+ req->setGiaddr(IOAddress("0.0.0.0"));
+ // Set ciaddr to non-zero address. The response should be sent to this
+ // address as the client is in renewing or rebinding state (it is fully
+ // configured).
+ req->setCiaddr(IOAddress("192.0.1.15"));
+ // Let's configure broadcast flag. It should be ignored because
+ // we are responding directly to the client having an address
+ // and trying to extend his lease. Broadcast flag is only used
+ // when new lease is acquired and server must make a decision
+ // whether to unicast the response to the acquired address or
+ // broadcast it.
+ req->setFlags(Pkt4::FLAG_BROADCAST_MASK);
+ // This is a direct message, so the hops should be cleared.
+ req->setHops(0);
+ // Set local unicast address as if we are renewing a lease.
+ req->setLocalAddr(IOAddress("192.0.2.1"));
+ // Request is received on the DHCPv4 server port.
+ req->setLocalPort(DHCP4_SERVER_PORT);
+ // Set the interface. The response should be sent over the same interface.
+ req->setIface("eth1");
+ req->setIndex(ETH1_INDEX);
+
+ // Create the exchange using the req.
+ Dhcpv4Exchange ex = createExchange(req);
+ Pkt4Ptr resp = ex.getResponse();
+
+ // Let's extend the lease for the client in such a way that
+ // it will actually get different address. The response
+ // should not be sent to this address but rather to ciaddr
+ // as client still have ciaddr configured.
+ resp->setYiaddr(IOAddress("192.0.1.13"));
+ // Clear the remote address.
+ resp->setRemoteAddr(IOAddress("0.0.0.0"));
+ // Copy hops value from the query.
+ resp->setHops(req->getHops());
+
+ ASSERT_NO_THROW(srv_.adjustIfaceData(ex));
+
+ // Check that server responds to ciaddr
+ EXPECT_EQ("192.0.1.15", resp->getRemoteAddr().toText());
+ // The query was non-relayed, so the response should be sent to a DHCPv4
+ // client port 68.
+ EXPECT_EQ(DHCP4_CLIENT_PORT, resp->getRemotePort());
+ // The response should be sent from the unicast address on which the
+ // query has been received.
+ EXPECT_EQ("192.0.2.1", resp->getLocalAddr().toText());
+ // The response should be sent from the DHCPv4 server port.
+ EXPECT_EQ(DHCP4_SERVER_PORT, resp->getLocalPort());
+ // The interface data should match the data in the query.
+ EXPECT_EQ("eth1", resp->getIface());
+ EXPECT_EQ(ETH1_INDEX, resp->getIndex());
+}
+
+// This test verifies that the destination address of the response message
+// is set to source address when the testing mode is enabled.
+// Renew: not testing mode was tested in adjustIfaceDataRenew.
+TEST_F(Dhcpv4SrvTest, adjustRemoteAddressRenewSendToSourceTestingModeEnabled) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Create instance of the incoming packet.
+ boost::shared_ptr<Pkt4> req(new Pkt4(DHCPDISCOVER, 1234));
+
+ // Clear giaddr to simulate direct packet.
+ req->setGiaddr(IOAddress("0.0.0.0"));
+ // Set ciaddr to non-zero address. The response should be sent to this
+ // address as the client is in renewing or rebinding state (it is fully
+ // configured).
+ req->setCiaddr(IOAddress("192.0.1.15"));
+ // Let's configure broadcast flag. It should be ignored because
+ // we are responding directly to the client having an address
+ // and trying to extend his lease. Broadcast flag is only used
+ // when new lease is acquired and server must make a decision
+ // whether to unicast the response to the acquired address or
+ // broadcast it.
+ req->setFlags(Pkt4::FLAG_BROADCAST_MASK);
+ // This is a direct message, so the hops should be cleared.
+ req->setHops(0);
+ // Set local unicast address as if we are renewing a lease.
+ req->setLocalAddr(IOAddress("192.0.2.1"));
+ // Request is received on the DHCPv4 server port.
+ req->setLocalPort(DHCP4_SERVER_PORT);
+ // Set the interface. The response should be sent over the same interface.
+ req->setIface("eth1");
+ req->setIndex(ETH1_INDEX);
+ // Set remote address.
+ req->setRemoteAddr(IOAddress("192.0.2.1"));
+
+ // Create the exchange using the req.
+ Dhcpv4Exchange ex = createExchange(req);
+ Pkt4Ptr resp = ex.getResponse();
+
+ // Let's extend the lease for the client in such a way that
+ // it will actually get different address. The response
+ // should not be sent to this address but rather to ciaddr
+ // as client still have ciaddr configured.
+ resp->setYiaddr(IOAddress("192.0.1.13"));
+ // Clear the remote address.
+ resp->setRemoteAddr(IOAddress("0.0.0.0"));
+ // Copy hops value from the query.
+ resp->setHops(req->getHops());
+
+ // Set the testing mode.
+ srv_.setSendResponsesToSource(true);
+
+ ASSERT_NO_THROW(srv_.adjustIfaceData(ex));
+
+ // Check that server responds to source address.
+ EXPECT_EQ("192.0.2.1", resp->getRemoteAddr().toText());
+}
+
+// This test verifies that the destination address of the response message
+// is set correctly when giaddr and ciaddr is zeroed in the received message
+// and the new lease is acquired. The lease address is carried in the
+// response message in the yiaddr field. In this case destination address
+// of the response should be set to yiaddr if server supports direct responses
+// to the client which doesn't have an address yet or broadcast if the server
+// doesn't support direct responses.
+TEST_F(Dhcpv4SrvTest, adjustIfaceDataSelect) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Create instance of the incoming packet.
+ boost::shared_ptr<Pkt4> req(new Pkt4(DHCPDISCOVER, 1234));
+
+ // Clear giaddr to simulate direct packet.
+ req->setGiaddr(IOAddress("0.0.0.0"));
+ // Clear client address as it hasn't got any address configured yet.
+ req->setCiaddr(IOAddress("0.0.0.0"));
+
+ // Let's clear the broadcast flag.
+ req->setFlags(0);
+
+ // This is a non-relayed message, so let's clear hops count.
+ req->setHops(0);
+ // The query is sent to the broadcast address in the Select state.
+ req->setLocalAddr(IOAddress("255.255.255.255"));
+ // The query has been received on the DHCPv4 server port 67.
+ req->setLocalPort(DHCP4_SERVER_PORT);
+ // Set the interface. The response should be sent via the same interface.
+ req->setIface("eth1");
+ req->setIndex(ETH1_INDEX);
+
+ // Create the exchange using the req.
+ Dhcpv4Exchange ex = createExchange(req);
+ Pkt4Ptr resp = ex.getResponse();
+ // Assign some new address for this client.
+ resp->setYiaddr(IOAddress("192.0.1.13"));
+ // Clear the remote address.
+ resp->setRemoteAddr(IOAddress("0.0.0.0"));
+ // Copy hops count.
+ resp->setHops(req->getHops());
+
+ // We want to test the case, when the server (packet filter) doesn't support
+ // direct responses to the client which doesn't have an address yet. In
+ // case, the server should send its response to the broadcast address.
+ // We can control whether the current packet filter returns that its support
+ // direct responses or not.
+ test_config.setDirectResponse(false);
+
+ // When running unit tests, the IfaceMgr is using the default Packet
+ // Filtering class, PktFilterInet. This class does not support direct
+ // responses to clients without address assigned. When giaddr and ciaddr
+ // are zero and client has just got new lease, the assigned address is
+ // carried in yiaddr. In order to send this address to the client,
+ // server must broadcast its response.
+ ASSERT_NO_THROW(srv_.adjustIfaceData(ex));
+
+ // Check that the response is sent to broadcast address as the
+ // server doesn't have capability to respond directly.
+ EXPECT_EQ("255.255.255.255", resp->getRemoteAddr().toText());
+
+ // Although the query has been sent to the broadcast address, the
+ // server should select a unicast address on the particular interface
+ // as a source address for the response.
+ EXPECT_EQ("192.0.2.3", resp->getLocalAddr().toText());
+
+ // The response should be sent from the DHCPv4 server port.
+ EXPECT_EQ(DHCP4_SERVER_PORT, resp->getLocalPort());
+
+ // The response should be sent via the same interface through which
+ // query has been received.
+ EXPECT_EQ("eth1", resp->getIface());
+ EXPECT_EQ(ETH1_INDEX, resp->getIndex());
+
+ // We also want to test the case when the server has capability to
+ // respond directly to the client which is not configured. Server
+ // makes decision whether it responds directly or broadcast its
+ // response based on the capability reported by IfaceMgr. We can
+ // control whether the current packet filter returns that it supports
+ // direct responses or not.
+ test_config.setDirectResponse(true);
+
+ // Now we expect that the server will send its response to the
+ // address assigned for the client.
+ ASSERT_NO_THROW(srv_.adjustIfaceData(ex));
+
+ EXPECT_EQ("192.0.1.13", resp->getRemoteAddr().toText());
+}
+
+// This test verifies that the destination address of the response message
+// is set to source address when the testing mode is enabled.
+// Select cases: not testing mode were tested in adjustIfaceDataSelect.
+TEST_F(Dhcpv4SrvTest, adjustRemoteAddressSelectSendToSourceTestingModeEnabled) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Create instance of the incoming packet.
+ boost::shared_ptr<Pkt4> req(new Pkt4(DHCPDISCOVER, 1234));
+
+ // Clear giaddr to simulate direct packet.
+ req->setGiaddr(IOAddress("0.0.0.0"));
+ // Clear client address as it hasn't got any address configured yet.
+ req->setCiaddr(IOAddress("0.0.0.0"));
+
+ // Let's clear the broadcast flag.
+ req->setFlags(0);
+
+ // This is a non-relayed message, so let's clear hops count.
+ req->setHops(0);
+ // The query is sent to the broadcast address in the Select state.
+ req->setLocalAddr(IOAddress("255.255.255.255"));
+ // The query has been received on the DHCPv4 server port 67.
+ req->setLocalPort(DHCP4_SERVER_PORT);
+ // Set the interface. The response should be sent via the same interface.
+ req->setIface("eth1");
+ req->setIndex(ETH1_INDEX);
+ // Set remote address.
+ req->setRemoteAddr(IOAddress("192.0.2.1"));
+
+ // Create the exchange using the req.
+ Dhcpv4Exchange ex = createExchange(req);
+ Pkt4Ptr resp = ex.getResponse();
+ // Assign some new address for this client.
+ resp->setYiaddr(IOAddress("192.0.1.13"));
+ // Clear the remote address.
+ resp->setRemoteAddr(IOAddress("0.0.0.0"));
+ // Copy hops count.
+ resp->setHops(req->getHops());
+
+ // Disable direct responses.
+ test_config.setDirectResponse(false);
+
+ // Set the testing mode.
+ srv_.setSendResponsesToSource(true);
+
+ ASSERT_NO_THROW(srv_.adjustIfaceData(ex));
+
+ // Check that server responds to source address.
+ EXPECT_EQ("192.0.2.1", resp->getRemoteAddr().toText());
+
+ // Enable direct responses.
+ test_config.setDirectResponse(true);
+
+ // Clear the remote address.
+ resp->setRemoteAddr(IOAddress("0.0.0.0"));
+
+ ASSERT_NO_THROW(srv_.adjustIfaceData(ex));
+
+ // Check that server still responds to source address.
+ EXPECT_EQ("192.0.2.1", resp->getRemoteAddr().toText());
+}
+
+// This test verifies that the destination address of the response message
+// is set to broadcast address when client set broadcast flag in its
+// query. Client sets this flag to indicate that it can't receive direct
+// responses from the server when it doesn't have its interface configured.
+// Server must respect broadcast flag.
+TEST_F(Dhcpv4SrvTest, adjustIfaceDataBroadcast) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Create instance of the incoming packet.
+ boost::shared_ptr<Pkt4> req(new Pkt4(DHCPDISCOVER, 1234));
+
+ // Clear giaddr to simulate direct packet.
+ req->setGiaddr(IOAddress("0.0.0.0"));
+ // Clear client address as it hasn't got any address configured yet.
+ req->setCiaddr(IOAddress("0.0.0.0"));
+ // The query is sent to the broadcast address in the Select state.
+ req->setLocalAddr(IOAddress("255.255.255.255"));
+ // The query has been received on the DHCPv4 server port 67.
+ req->setLocalPort(DHCP4_SERVER_PORT);
+ // Set the interface. The response should be sent via the same interface.
+ req->setIface("eth1");
+ req->setIndex(ETH1_INDEX);
+
+ // Let's set the broadcast flag.
+ req->setFlags(Pkt4::FLAG_BROADCAST_MASK);
+
+ // Create the exchange using the req.
+ Dhcpv4Exchange ex = createExchange(req);
+ Pkt4Ptr resp = ex.getResponse();
+
+ // Assign some new address for this client.
+ resp->setYiaddr(IOAddress("192.0.1.13"));
+
+ // Clear the remote address.
+ resp->setRemoteAddr(IOAddress("0.0.0.0"));
+
+ ASSERT_NO_THROW(srv_.adjustIfaceData(ex));
+
+ // Server must respond to broadcast address when client desired that
+ // by setting the broadcast flag in its request.
+ EXPECT_EQ("255.255.255.255", resp->getRemoteAddr().toText());
+
+ // Although the query has been sent to the broadcast address, the
+ // server should select a unicast address on the particular interface
+ // as a source address for the response.
+ EXPECT_EQ("192.0.2.3", resp->getLocalAddr().toText());
+
+ // The response should be sent from the DHCPv4 server port.
+ EXPECT_EQ(DHCP4_SERVER_PORT, resp->getLocalPort());
+
+ // The response should be sent via the same interface through which
+ // query has been received.
+ EXPECT_EQ("eth1", resp->getIface());
+ EXPECT_EQ(ETH1_INDEX, resp->getIndex());
+}
+
+// This test verifies that the destination address of the response message
+// is set to source address when the testing mode is enabled.
+// Broadcast case: not testing mode was tested in adjustIfaceDataBroadcast.
+TEST_F(Dhcpv4SrvTest, adjustRemoteAddressBroadcastSendToSourceTestingModeEnabled) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Create instance of the incoming packet.
+ boost::shared_ptr<Pkt4> req(new Pkt4(DHCPDISCOVER, 1234));
+
+ // Clear giaddr to simulate direct packet.
+ req->setGiaddr(IOAddress("0.0.0.0"));
+ // Clear client address as it hasn't got any address configured yet.
+ req->setCiaddr(IOAddress("0.0.0.0"));
+ // The query is sent to the broadcast address in the Select state.
+ req->setLocalAddr(IOAddress("255.255.255.255"));
+ // The query has been received on the DHCPv4 server port 67.
+ req->setLocalPort(DHCP4_SERVER_PORT);
+ // Set the interface. The response should be sent via the same interface.
+ req->setIface("eth1");
+ req->setIndex(ETH1_INDEX);
+ // Set remote address.
+ req->setRemoteAddr(IOAddress("192.0.2.1"));
+
+ // Let's set the broadcast flag.
+ req->setFlags(Pkt4::FLAG_BROADCAST_MASK);
+
+ // Create the exchange using the req.
+ Dhcpv4Exchange ex = createExchange(req);
+ Pkt4Ptr resp = ex.getResponse();
+
+ // Assign some new address for this client.
+ resp->setYiaddr(IOAddress("192.0.1.13"));
+
+ // Clear the remote address.
+ resp->setRemoteAddr(IOAddress("0.0.0.0"));
+
+ // Set the testing mode.
+ srv_.setSendResponsesToSource(true);
+
+ ASSERT_NO_THROW(srv_.adjustIfaceData(ex));
+
+ // Check that server responds to source address.
+ EXPECT_EQ("192.0.2.1", resp->getRemoteAddr().toText());
+}
+
+// This test verifies that the mandatory to copy fields and options
+// are really copied into the response.
+TEST_F(Dhcpv4SrvTest, initResponse) {
+ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
+
+ // Set fields which must be copied
+ query->setIface("foo");
+ query->setIndex(111);
+ query->setHops(5);
+ const HWAddr& hw = HWAddr::fromText("11:22:33:44:55:66:77:88", 10);
+ HWAddrPtr hw_addr(new HWAddr(hw));
+ query->setHWAddr(hw_addr);
+ query->setGiaddr(IOAddress("10.10.10.10"));
+ const HWAddr& src_hw = HWAddr::fromText("e4:ce:8f:12:34:56");
+ HWAddrPtr src_hw_addr(new HWAddr(src_hw));
+ query->setLocalHWAddr(src_hw_addr);
+ const HWAddr& dst_hw = HWAddr::fromText("e8:ab:cd:78:9a:bc");
+ HWAddrPtr dst_hw_addr(new HWAddr(dst_hw));
+ query->setRemoteHWAddr(dst_hw_addr);
+ query->setFlags(BOOTP_BROADCAST);
+
+ // Add options which must be copied
+ // client-id echo is optional
+ // rai echo is done in relayAgentInfoEcho
+ // Do subnet selection option
+ OptionDefinitionPtr sbnsel_def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE,
+ DHO_SUBNET_SELECTION);
+ ASSERT_TRUE(sbnsel_def);
+ OptionCustomPtr sbnsel(new OptionCustom(*sbnsel_def, Option::V4));
+ ASSERT_TRUE(sbnsel);
+ sbnsel->writeAddress(IOAddress("192.0.2.3"));
+ query->addOption(sbnsel);
+
+ // Create exchange and get Response
+ Dhcpv4Exchange ex = createExchange(query);
+ Pkt4Ptr response = ex.getResponse();
+ ASSERT_TRUE(response);
+
+ // Check fields
+ EXPECT_EQ("foo", response->getIface());
+ EXPECT_EQ(111, response->getIndex());
+ EXPECT_TRUE(response->getSiaddr().isV4Zero());
+ EXPECT_TRUE(response->getCiaddr().isV4Zero());
+ EXPECT_EQ(5, response->getHops());
+ EXPECT_TRUE(hw == *response->getHWAddr());
+ EXPECT_EQ(IOAddress("10.10.10.10"), response->getGiaddr());
+ EXPECT_TRUE(src_hw == *response->getLocalHWAddr());
+ EXPECT_TRUE(dst_hw == *response->getRemoteHWAddr());
+ EXPECT_TRUE(BOOTP_BROADCAST == response->getFlags());
+
+ // Check options (i.e., subnet selection option)
+ OptionPtr resp_sbnsel = response->getOption(DHO_SUBNET_SELECTION);
+ ASSERT_TRUE(resp_sbnsel);
+ OptionCustomPtr resp_custom =
+ boost::dynamic_pointer_cast<OptionCustom>(resp_sbnsel);
+ ASSERT_TRUE(resp_custom);
+ IOAddress subnet_addr("0.0.0.0");
+ ASSERT_NO_THROW(subnet_addr = resp_custom->readAddress());
+ EXPECT_EQ(IOAddress("192.0.2.3"), subnet_addr);
+}
+
+// This test verifies that the server identifier option is appended to
+// a specified DHCPv4 message and the server identifier is correct.
+TEST_F(Dhcpv4SrvTest, appendServerID) {
+ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
+ Dhcpv4Exchange ex = createExchange(query);
+ Pkt4Ptr response = ex.getResponse();
+
+ // Set a local address. It is required by the function under test
+ // to create the Server Identifier option.
+ query->setLocalAddr(IOAddress("192.0.3.1"));
+
+ // Append the Server Identifier.
+ ASSERT_NO_THROW(NakedDhcpv4Srv::appendServerID(ex));
+
+ // Make sure that the option has been added.
+ OptionPtr opt = response->getOption(DHO_DHCP_SERVER_IDENTIFIER);
+ ASSERT_TRUE(opt);
+ Option4AddrLstPtr opt_server_id =
+ boost::dynamic_pointer_cast<Option4AddrLst>(opt);
+ ASSERT_TRUE(opt_server_id);
+
+ // The option is represented as a list of IPv4 addresses but with
+ // only one address added.
+ Option4AddrLst::AddressContainer addrs = opt_server_id->getAddresses();
+ ASSERT_EQ(1, addrs.size());
+ // This address should match the local address of the packet.
+ EXPECT_EQ("192.0.3.1", addrs[0].toText());
+}
+
+// Sanity check. Verifies that both Dhcpv4Srv and its derived
+// class NakedDhcpv4Srv can be instantiated and destroyed.
+TEST_F(Dhcpv4SrvTest, basic) {
+
+ // Check that the base class can be instantiated
+ boost::scoped_ptr<Dhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new Dhcpv4Srv(DHCP4_SERVER_PORT + 10000, false,
+ false)));
+ srv.reset();
+ // We have to close open sockets because further in this test we will
+ // call the Dhcpv4Srv constructor again. This constructor will try to
+ // set the appropriate packet filter class for IfaceMgr. This requires
+ // that all sockets are closed.
+ IfaceMgr::instance().closeSockets();
+
+ // Check that the derived class can be instantiated
+ boost::scoped_ptr<NakedDhcpv4Srv> naked_srv;
+ ASSERT_NO_THROW(
+ naked_srv.reset(new NakedDhcpv4Srv(DHCP4_SERVER_PORT + 10000)));
+ // Close sockets again for the next test.
+ IfaceMgr::instance().closeSockets();
+
+ ASSERT_NO_THROW(naked_srv.reset(new NakedDhcpv4Srv(0)));
+}
+
+// This test verifies the test_send_responses_to_source_ is false by default
+// and sets by the KEA_TEST_SEND_RESPONSES_TO_SOURCE environment variable.
+TEST_F(Dhcpv4SrvTest, testSendResponsesToSource) {
+
+ ASSERT_FALSE(std::getenv("KEA_TEST_SEND_RESPONSES_TO_SOURCE"));
+ boost::scoped_ptr<NakedDhcpv4Srv> naked_srv;
+ ASSERT_NO_THROW(
+ naked_srv.reset(new NakedDhcpv4Srv(DHCP4_SERVER_PORT + 10000)));
+ EXPECT_FALSE(naked_srv->getSendResponsesToSource());
+ ::setenv("KEA_TEST_SEND_RESPONSES_TO_SOURCE", "ENABLED", 1);
+ // Do not use ASSERT as we want unsetenv to be always called.
+ EXPECT_NO_THROW(
+ naked_srv.reset(new NakedDhcpv4Srv(DHCP4_SERVER_PORT + 10000)));
+ EXPECT_TRUE(naked_srv->getSendResponsesToSource());
+ ::unsetenv("KEA_TEST_SEND_RESPONSES_TO_SOURCE");
+}
+
+// Verifies that DISCOVER message can be processed correctly,
+// that the OFFER message generated in response is valid and
+// contains necessary options.
+//
+// Note: this test focuses on the packet correctness. There
+// are other tests that verify correctness of the allocation
+// engine. See DiscoverBasic, DiscoverHint, DiscoverNoClientId
+// and DiscoverInvalidHint.
+TEST_F(Dhcpv4SrvTest, processDiscover) {
+ testDiscoverRequest(DHCPDISCOVER);
+}
+
+// Verifies that REQUEST message can be processed correctly,
+// that the OFFER message generated in response is valid and
+// contains necessary options.
+//
+// Note: this test focuses on the packet correctness. There
+// are other tests that verify correctness of the allocation
+// engine. See DiscoverBasic, DiscoverHint, DiscoverNoClientId
+// and DiscoverInvalidHint.
+TEST_F(Dhcpv4SrvTest, processRequest) {
+ testDiscoverRequest(DHCPREQUEST);
+}
+
+// Verifies that DHCPDISCOVERs are sanity checked correctly.
+// 1. They must have either hardware address or client id
+// 2. They must not have server id
+TEST_F(Dhcpv4SrvTest, sanityCheckDiscover) {
+ NakedDhcpv4Srv srv;
+ Pkt4Ptr pkt(new Pkt4(DHCPDISCOVER, 1234));
+
+ // Should throw, no hardware address or client id
+ ASSERT_THROW_MSG(srv.processDiscover(pkt), RFCViolation,
+ "Missing or useless client-id and no HW address"
+ " provided in message DHCPDISCOVER");
+
+ // Add a hardware address. This should not throw.
+ std::vector<uint8_t> data = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe};
+ HWAddrPtr hwaddr(new HWAddr(data, HTYPE_ETHER));
+ pkt->setHWAddr(hwaddr);
+ ASSERT_NO_THROW(srv.processDiscover(pkt));
+
+ // Now let's make a new pkt with client-id only, it should not throw.
+ pkt.reset(new Pkt4(DHCPDISCOVER, 1234));
+ pkt->addOption(generateClientId());
+ ASSERT_NO_THROW(srv.processDiscover(pkt));
+
+ // Now let's add a server-id. This should throw.
+ OptionDefinitionPtr server_id_def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE,
+ DHO_DHCP_SERVER_IDENTIFIER);
+ ASSERT_TRUE(server_id_def);
+
+ OptionCustomPtr server_id(new OptionCustom(*server_id_def, Option::V4));
+ server_id->writeAddress(IOAddress("192.0.2.3"));
+ pkt->addOption(server_id);
+ EXPECT_THROW_MSG(srv.processDiscover(pkt), RFCViolation,
+ "Server-id option was not expected,"
+ " but received in message DHCPDISCOVER");
+}
+
+// Verifies that DHCPREQEUSTs are sanity checked correctly.
+// 1. They must have either hardware address or client id
+// 2. They must have a requested address
+// 3. They may or may not have a server id
+TEST_F(Dhcpv4SrvTest, sanityCheckRequest) {
+ NakedDhcpv4Srv srv;
+ Pkt4Ptr pkt(new Pkt4(DHCPREQUEST, 1234));
+
+ // Should throw, no hardware address or client id
+ ASSERT_THROW_MSG(srv.processRequest(pkt), RFCViolation,
+ "Missing or useless client-id and no HW address"
+ " provided in message DHCPREQUEST");
+
+ // Add a hardware address. Should not throw.
+ std::vector<uint8_t> data = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe};
+ HWAddrPtr hwaddr(new HWAddr(data, HTYPE_ETHER));
+ pkt->setHWAddr(hwaddr);
+ EXPECT_NO_THROW(srv.processRequest(pkt));
+
+ // Now let's add a requested address. This should not throw.
+ OptionDefinitionPtr req_addr_def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE,
+ DHO_DHCP_REQUESTED_ADDRESS);
+ ASSERT_TRUE(req_addr_def);
+ OptionCustomPtr req_addr(new OptionCustom(*req_addr_def, Option::V4));
+ req_addr->writeAddress(IOAddress("192.0.2.3"));
+ pkt->addOption(req_addr);
+ ASSERT_NO_THROW(srv.processRequest(pkt));
+
+ // Now let's make a new pkt with client-id only and an address, it should not throw.
+ pkt.reset(new Pkt4(DHCPREQUEST, 1234));
+ pkt->addOption(generateClientId());
+ pkt->addOption(req_addr);
+ ASSERT_NO_THROW(srv.processRequest(pkt));
+
+ // Now let's add a server-id. This should not throw.
+ OptionDefinitionPtr server_id_def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE,
+ DHO_DHCP_SERVER_IDENTIFIER);
+ ASSERT_TRUE(server_id_def);
+
+ OptionCustomPtr server_id(new OptionCustom(*server_id_def, Option::V4));
+ server_id->writeAddress(IOAddress("192.0.2.3"));
+ pkt->addOption(server_id);
+ EXPECT_NO_THROW(srv.processRequest(pkt));
+}
+
+// Verifies that DHCPDECLINEs are sanity checked correctly.
+// 1. They must have either hardware address or client id
+// 2. They must have a requested address
+// 3. They may or may not have a server id
+TEST_F(Dhcpv4SrvTest, sanityCheckDecline) {
+ NakedDhcpv4Srv srv;
+ Pkt4Ptr pkt(new Pkt4(DHCPDECLINE, 1234));
+
+ // Should throw, no hardware address or client id
+ ASSERT_THROW_MSG(srv.processDecline(pkt), RFCViolation,
+ "Missing or useless client-id and no HW address"
+ " provided in message DHCPDECLINE");
+
+ // Add a hardware address. Should throw because of missing address.
+ std::vector<uint8_t> data = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe};
+ HWAddrPtr hwaddr(new HWAddr(data, HTYPE_ETHER));
+ pkt->setHWAddr(hwaddr);
+ ASSERT_THROW_MSG(srv.processDecline(pkt), RFCViolation,
+ "Mandatory 'Requested IP address' option missing in DHCPDECLINE"
+ " sent from [hwtype=1 00:fe:fe:fe:fe:fe], cid=[no info], tid=0x4d2");
+
+ // Now let's add a requested address. This should not throw.
+ OptionDefinitionPtr req_addr_def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE,
+ DHO_DHCP_REQUESTED_ADDRESS);
+ ASSERT_TRUE(req_addr_def);
+ OptionCustomPtr req_addr(new OptionCustom(*req_addr_def, Option::V4));
+ req_addr->writeAddress(IOAddress("192.0.2.3"));
+ pkt->addOption(req_addr);
+ ASSERT_NO_THROW(srv.processDecline(pkt));
+
+ // Now let's make a new pkt with client-id only and an address, it should not throw.
+ pkt.reset(new Pkt4(DHCPDECLINE, 1234));
+ pkt->addOption(generateClientId());
+ pkt->addOption(req_addr);
+ ASSERT_NO_THROW(srv.processDecline(pkt));
+
+ // Now let's add a server-id. This should not throw.
+ OptionDefinitionPtr server_id_def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE,
+ DHO_DHCP_SERVER_IDENTIFIER);
+ ASSERT_TRUE(server_id_def);
+
+ OptionCustomPtr server_id(new OptionCustom(*server_id_def, Option::V4));
+ server_id->writeAddress(IOAddress("192.0.2.3"));
+ pkt->addOption(server_id);
+ EXPECT_NO_THROW(srv.processDecline(pkt));
+}
+
+// Verifies that DHCPRELEASEs are sanity checked correctly.
+// 1. They must have either hardware address or client id
+// 2. They may or may not have a server id
+TEST_F(Dhcpv4SrvTest, sanityCheckRelease) {
+ NakedDhcpv4Srv srv;
+ Pkt4Ptr pkt(new Pkt4(DHCPRELEASE, 1234));
+
+ // Should throw, no hardware address or client id
+ ASSERT_THROW_MSG(srv.processRelease(pkt), RFCViolation,
+ "Missing or useless client-id and no HW address"
+ " provided in message DHCPRELEASE");
+
+ // Add a hardware address. Should not throw.
+ std::vector<uint8_t> data = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe};
+ HWAddrPtr hwaddr(new HWAddr(data, HTYPE_ETHER));
+ pkt->setHWAddr(hwaddr);
+ EXPECT_NO_THROW(srv.processRelease(pkt));
+
+ // Make a new pkt with client-id only. Should not throw.
+ pkt.reset(new Pkt4(DHCPRELEASE, 1234));
+ pkt->addOption(generateClientId());
+ ASSERT_NO_THROW(srv.processRelease(pkt));
+
+ // Now let's add a server-id. This should not throw.
+ OptionDefinitionPtr server_id_def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE,
+ DHO_DHCP_SERVER_IDENTIFIER);
+ ASSERT_TRUE(server_id_def);
+
+ OptionCustomPtr server_id(new OptionCustom(*server_id_def, Option::V4));
+ server_id->writeAddress(IOAddress("192.0.2.3"));
+ pkt->addOption(server_id);
+ EXPECT_NO_THROW(srv.processRelease(pkt));
+}
+
+// Verifies that DHCPINFORMs are sanity checked correctly.
+// 1. They must have either hardware address or client id
+// 2. They may or may not have requested address
+// 3. They may or may not have a server id
+TEST_F(Dhcpv4SrvTest, sanityCheckInform) {
+ NakedDhcpv4Srv srv;
+ Pkt4Ptr pkt(new Pkt4(DHCPINFORM, 1234));
+
+ // Should throw, no hardware address or client id
+ ASSERT_THROW_MSG(srv.processInform(pkt), RFCViolation,
+ "Missing or useless client-id and no HW address"
+ " provided in message DHCPINFORM");
+
+ // Add a hardware address. Should not throw.
+ std::vector<uint8_t> data = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe};
+ HWAddrPtr hwaddr(new HWAddr(data, HTYPE_ETHER));
+ pkt->setHWAddr(hwaddr);
+ ASSERT_NO_THROW(srv.processInform(pkt));
+
+ // Now let's add a requested address. This should not throw.
+ OptionDefinitionPtr req_addr_def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE,
+ DHO_DHCP_REQUESTED_ADDRESS);
+ ASSERT_TRUE(req_addr_def);
+ OptionCustomPtr req_addr(new OptionCustom(*req_addr_def, Option::V4));
+ req_addr->writeAddress(IOAddress("192.0.2.3"));
+ pkt->addOption(req_addr);
+ ASSERT_NO_THROW(srv.processInform(pkt));
+
+ // Now let's make a new pkt with client-id only and an address, it should not throw.
+ pkt.reset(new Pkt4(DHCPINFORM, 1234));
+ pkt->addOption(generateClientId());
+ pkt->addOption(req_addr);
+ ASSERT_NO_THROW(srv.processInform(pkt));
+
+ // Now let's add a server-id. This should not throw.
+ OptionDefinitionPtr server_id_def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE,
+ DHO_DHCP_SERVER_IDENTIFIER);
+ ASSERT_TRUE(server_id_def);
+
+ OptionCustomPtr server_id(new OptionCustom(*server_id_def, Option::V4));
+ server_id->writeAddress(IOAddress("192.0.2.3"));
+ pkt->addOption(server_id);
+ EXPECT_NO_THROW(srv.processInform(pkt));
+}
+
+// This test verifies that incoming DISCOVER can be handled properly, that an
+// OFFER is generated, that the response has an address and that address
+// really belongs to the configured pool.
+//
+// constructed very simple DISCOVER message with:
+// - client-id option
+//
+// expected returned OFFER message:
+// - copy of client-id
+// - server-id
+// - offered address
+TEST_F(Dhcpv4SrvTest, DiscoverBasic) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ boost::scoped_ptr<NakedDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
+
+ Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ dis->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+ dis->setIface("eth1");
+ dis->setIndex(ETH1_INDEX);
+
+ // Pass it to the server and get an offer
+ Pkt4Ptr offer = srv->processDiscover(dis);
+
+ // Check if we get response at all
+ checkResponse(offer, DHCPOFFER, 1234);
+
+ // Check that address was returned from proper range, that its lease
+ // lifetime is correct, that T1 and T2 are returned properly
+ checkAddressParams(offer, subnet_, true, true);
+
+ // Check identifiers
+ checkServerId(offer, srv->getServerID());
+ checkClientId(offer, clientid);
+}
+
+// This test verifies that OFFERs return expected valid lifetimes.
+TEST_F(Dhcpv4SrvTest, DiscoverValidLifetime) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ boost::scoped_ptr<NakedDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
+
+ // Recreate subnet
+ Triplet<uint32_t> unspecified;
+ Triplet<uint32_t> valid_lft(500, 1000, 1500);
+ subnet_ = Subnet4::create(IOAddress("192.0.2.0"), 24,
+ unspecified,
+ unspecified,
+ valid_lft,
+ subnet_->getID());
+
+ pool_ = Pool4Ptr(new Pool4(IOAddress("192.0.2.100"),
+ IOAddress("192.0.2.110")));
+ subnet_->addPool(pool_);
+ CfgMgr::instance().clear();
+ CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->add(subnet_);
+ CfgMgr::instance().commit();
+
+ // Struct for describing an individual lifetime test scenario
+ struct LifetimeTest {
+ // logged test description
+ std::string description_;
+ // lifetime hint (0 means not send dhcp-lease-time option)
+ uint32_t hint;
+ // expected returned value
+ uint32_t expected;
+ };
+
+ // Test scenarios
+ std::vector<LifetimeTest> tests = {
+ { "default valid lifetime", 0, 1000 },
+ { "specified valid lifetime", 1001, 1001 },
+ { "too small valid lifetime", 100, 500 },
+ { "too large valid lifetime", 2000, 1500 }
+ };
+
+ // Iterate over the test scenarios.
+ for (auto test : tests) {
+ SCOPED_TRACE(test.description_);
+
+ // Create a discover packet to use
+ Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ dis->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+ dis->setIface("eth1");
+ dis->setIndex(ETH1_INDEX);
+
+ // Add dhcp-lease-time option.
+ if (test.hint) {
+ OptionUint32Ptr opt(new OptionUint32(Option::V4,
+ DHO_DHCP_LEASE_TIME,
+ test.hint));
+ dis->addOption(opt);
+ }
+
+ // Pass it to the server and get an offer
+ Pkt4Ptr offer = srv->processDiscover(dis);
+
+ // Check if we get response at all
+ checkResponse(offer, DHCPOFFER, 1234);
+
+ // Check that address was returned from proper range, that its lease
+ // lifetime is correct and has the expected value.
+ checkAddressParams(offer, subnet_, false, false, test.expected);
+
+ // Check identifiers
+ checkServerId(offer, srv->getServerID());
+ checkClientId(offer, clientid);
+ }
+}
+
+// Check that option 58 and 59 are only included if they were specified
+// (and calculate-tee-times = false) and the values are sane:
+// T2 is less than valid lft; T1 is less than T2 (if given) or valid
+// lft if T2 is not given.
+TEST_F(Dhcpv4SrvTest, DiscoverTimers) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ boost::scoped_ptr<NakedDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
+
+ // Recreate subnet
+ Triplet<uint32_t> unspecified;
+ Triplet<uint32_t> valid_lft(1000);
+ subnet_ = Subnet4::create(IOAddress("192.0.2.0"), 24,
+ unspecified,
+ unspecified,
+ valid_lft,
+ subnet_->getID());
+
+ pool_ = Pool4Ptr(new Pool4(IOAddress("192.0.2.100"),
+ IOAddress("192.0.2.110")));
+ subnet_->addPool(pool_);
+ CfgMgr::instance().clear();
+ CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->add(subnet_);
+ CfgMgr::instance().commit();
+
+ // Struct for describing an individual timer test scenario
+ struct TimerTest {
+ // logged test description
+ std::string description_;
+ // configured value for subnet's T1
+ Triplet<uint32_t> cfg_t1_;
+ // configured value for subnet's T1
+ Triplet<uint32_t> cfg_t2_;
+ // True if Offer should contain Subnet's T1 value
+ bool exp_t1_;
+ // True if Offer should contain Subnet's T2 value
+ bool exp_t2_;
+ };
+
+ // Convenience constants
+ bool T1 = true;
+ bool T2 = true;
+
+ // Test scenarios
+ std::vector<TimerTest> tests = {
+ {
+ "T1:unspecified, T2:unspecified",
+ unspecified, unspecified,
+ // Client should neither.
+ !T1, !T2
+ },
+ {
+ "T1 unspecified, T2 < VALID",
+ unspecified, valid_lft - 1,
+ // Client should only get T2.
+ !T1, T2
+ },
+ {
+ "T1:unspecified, T2 = VALID",
+ unspecified, valid_lft,
+ // Client should get neither.
+ !T1, !T2
+ },
+ {
+ "T1:unspecified, T2 > VALID",
+ unspecified, valid_lft + 1,
+ // Client should get neither.
+ !T1, !T2
+ },
+
+ {
+ "T1 < VALID, T2:unspecified",
+ valid_lft - 1, unspecified,
+ // Client should only get T1.
+ T1, !T2
+ },
+ {
+ "T1 = VALID, T2:unspecified",
+ valid_lft, unspecified,
+ // Client should get neither.
+ !T1, !T2
+ },
+ {
+ "T1 > VALID, T2:unspecified",
+ valid_lft + 1, unspecified,
+ // Client should get neither.
+ !T1, !T2
+ },
+ {
+ "T1 < T2 < VALID",
+ valid_lft - 2, valid_lft - 1,
+ // Client should get both.
+ T1, T2
+ },
+ {
+ "T1 = T2 < VALID",
+ valid_lft - 1, valid_lft - 1,
+ // Client should only get T2.
+ !T1, T2
+ },
+ {
+ "T1 > T2 < VALID",
+ valid_lft - 1, valid_lft - 2,
+ // Client should only get T2.
+ !T1, T2
+ },
+ {
+ "T1 = T2 = VALID",
+ valid_lft, valid_lft,
+ // Client should get neither.
+ !T1, !T2
+ },
+ {
+ "T1 > VALID < T2, T2 > VALID",
+ valid_lft + 1, valid_lft + 2,
+ // Client should get neither.
+ !T1, !T2
+ }
+ };
+
+ // Create a discover packet to use
+ Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ dis->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+ dis->setIface("eth1");
+ dis->setIndex(ETH1_INDEX);
+
+ // Iterate over the test scenarios.
+ for (auto test = tests.begin(); test != tests.end(); ++test) {
+ {
+ SCOPED_TRACE((*test).description_);
+ // Configure subnet's timer values
+ subnet_->setT1((*test).cfg_t1_);
+ subnet_->setT2((*test).cfg_t2_);
+
+ // Discover/Offer exchange with the server
+ Pkt4Ptr offer = srv->processDiscover(dis);
+
+ // Verify we have an offer
+ checkResponse(offer, DHCPOFFER, 1234);
+
+ // Verify the timers are as expected.
+ checkAddressParams(offer, subnet_,
+ (*test).exp_t1_, (*test).exp_t2_);
+ }
+ }
+}
+
+// Check that option 58 and 59 are included when calculate-tee-times
+// is enabled, but only when they are not explicitly specified via
+// renew-timer and rebinding-timer. This test does not check whether
+// the subnet's for t1-percent and t2-percent are valid, as this is
+// enforced by parsing and tested elsewhere.
+TEST_F(Dhcpv4SrvTest, calculateTeeTimers) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ boost::scoped_ptr<NakedDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
+
+ // Recreate subnet
+ Triplet<uint32_t> unspecified;
+ Triplet<uint32_t> valid_lft(1000);
+ subnet_ = Subnet4::create(IOAddress("192.0.2.0"), 24,
+ unspecified,
+ unspecified,
+ valid_lft,
+ subnet_->getID());
+
+ pool_ = Pool4Ptr(new Pool4(IOAddress("192.0.2.100"),
+ IOAddress("192.0.2.110")));
+ subnet_->addPool(pool_);
+ CfgMgr::instance().clear();
+ CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->add(subnet_);
+ CfgMgr::instance().commit();
+
+ // Struct for describing an individual timer test scenario
+ struct TimerTest {
+ // logged test description
+ std::string description_;
+ // configured value for subnet's T1
+ Triplet<uint32_t> cfg_t1_;
+ // configured value for subnet's T1
+ Triplet<uint32_t> cfg_t2_;
+ // configured value for subnet's t1_percent.
+ double t1_percent_;
+ // configured value for subnet's t2_percent.
+ double t2_percent_;
+ // expected value for T1 in server response.
+ // A value of 0 means server should not have sent T1.
+ uint32_t t1_exp_value_;
+ // expected value for T2 in server response.
+ // A value of 0 means server should not have sent T2.
+ uint32_t t2_exp_value_;
+ };
+
+ // Convenience constant
+ uint32_t not_expected = 0;
+
+ // Test scenarios
+ std::vector<TimerTest> tests = {
+ {
+ "T1 and T2 calculated",
+ unspecified, unspecified,
+ 0.4, 0.8,
+ 400, 800
+ },
+ {
+ "T1 and T2 specified insane",
+ valid_lft + 1, valid_lft + 2,
+ 0.4, 0.8,
+ not_expected, not_expected
+ },
+ {
+ "T1 should be calculated, T2 specified",
+ unspecified, valid_lft - 1,
+ 0.4, 0.8,
+ 400, valid_lft - 1
+ },
+ {
+ "T1 specified, T2 should be calculated",
+ 299, unspecified,
+ 0.4, 0.8,
+ 299, 800
+ },
+ {
+ "T1 specified > T2, T2 should be calculated",
+ valid_lft - 1, unspecified,
+ 0.4, 0.8,
+ not_expected, 800
+ }
+ };
+
+ // Calculation is enabled for all the scenarios.
+ subnet_->setCalculateTeeTimes(true);
+
+ // Create a discover packet to use
+ Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ dis->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+ dis->setIface("eth1");
+ dis->setIndex(ETH1_INDEX);
+
+ // Iterate over the test scenarios.
+ for (auto test = tests.begin(); test != tests.end(); ++test) {
+ {
+ SCOPED_TRACE((*test).description_);
+ // Configure subnet's timer values
+ subnet_->setT1((*test).cfg_t1_);
+ subnet_->setT2((*test).cfg_t2_);
+
+ subnet_->setT1Percent((*test).t1_percent_);
+ subnet_->setT2Percent((*test).t2_percent_);
+
+ // Discover/Offer exchange with the server
+ Pkt4Ptr offer = srv->processDiscover(dis);
+
+ // Verify we have an offer
+ checkResponse(offer, DHCPOFFER, 1234);
+
+ // Check T1 timer
+ OptionUint32Ptr opt = boost::dynamic_pointer_cast
+ <OptionUint32> (offer->getOption(DHO_DHCP_RENEWAL_TIME));
+
+ if ((*test).t1_exp_value_ == not_expected) {
+ EXPECT_FALSE(opt) << "T1 present and shouldn't be";
+ } else {
+ ASSERT_TRUE(opt) << "Required T1 option missing or it has"
+ " an unexpected type";
+ EXPECT_EQ(opt->getValue(), (*test).t1_exp_value_);
+ }
+
+ // Check T2 timer
+ opt = boost::dynamic_pointer_cast
+ <OptionUint32>(offer->getOption(DHO_DHCP_REBINDING_TIME));
+
+ if ((*test).t2_exp_value_ == not_expected) {
+ EXPECT_FALSE(opt) << "T2 present and shouldn't be";
+ } else {
+ ASSERT_TRUE(opt) << "Required T2 option missing or it has"
+ " an unexpected type";
+ EXPECT_EQ(opt->getValue(), (*test).t2_exp_value_);
+ }
+ }
+ }
+}
+
+// This test verifies that incoming DISCOVER can be handled properly, that an
+// OFFER is generated, that the response has an address and that address
+// really belongs to the configured pool.
+//
+// constructed very simple DISCOVER message with:
+// - client-id option
+// - address set to specific value as hint, but that hint is invalid
+//
+// expected returned OFFER message:
+// - copy of client-id
+// - server-id
+// - offered address (!= hint)
+TEST_F(Dhcpv4SrvTest, DiscoverInvalidHint) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ boost::scoped_ptr<NakedDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
+ IOAddress hint("10.1.2.3");
+
+ Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ dis->setRemoteAddr(IOAddress("192.0.2.107"));
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+ dis->setYiaddr(hint);
+ dis->setIface("eth1");
+ dis->setIndex(ETH1_INDEX);
+
+ // Pass it to the server and get an offer
+ Pkt4Ptr offer = srv->processDiscover(dis);
+
+ // Check if we get response at all
+ checkResponse(offer, DHCPOFFER, 1234);
+
+ // Check that address was returned from proper range, that its lease
+ // lifetime is correct, that T1 and T2 are returned properly
+ checkAddressParams(offer, subnet_, true, true);
+
+ EXPECT_NE(offer->getYiaddr(), hint);
+
+ // Check identifiers
+ checkServerId(offer, srv->getServerID());
+ checkClientId(offer, clientid);
+}
+
+/// @todo: Add a test that client sends hint that is in pool, but currently
+/// being used by a different client.
+
+// This test checks that the server is offering different addresses to different
+// clients in OFFERs. Please note that OFFER is not a guarantee that such
+// an address will be assigned. Had the pool was very small and contained only
+// 2 addresses, the third client would get the same offer as the first one
+// and this is a correct behavior. It is REQUEST that will fail for the third
+// client. OFFER is basically saying "if you send me a request, you will
+// probably get an address like this" (there are no guarantees).
+TEST_F(Dhcpv4SrvTest, ManyDiscovers) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ boost::scoped_ptr<NakedDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
+
+ Pkt4Ptr dis1 = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ Pkt4Ptr dis2 = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 2345));
+ Pkt4Ptr dis3 = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 3456));
+
+ dis1->setRemoteAddr(IOAddress("192.0.2.1"));
+ dis2->setRemoteAddr(IOAddress("192.0.2.2"));
+ dis3->setRemoteAddr(IOAddress("192.0.2.3"));
+
+ // Assign interfaces
+ dis1->setIface("eth1");
+ dis1->setIndex(ETH1_INDEX);
+ dis2->setIface("eth1");
+ dis2->setIndex(ETH1_INDEX);
+ dis3->setIface("eth1");
+ dis3->setIndex(ETH1_INDEX);
+
+ // Different client-id sizes
+ OptionPtr clientid1 = generateClientId(4); // length 4
+ OptionPtr clientid2 = generateClientId(5); // length 5
+ OptionPtr clientid3 = generateClientId(6); // length 6
+
+ dis1->addOption(clientid1);
+ dis2->addOption(clientid2);
+ dis3->addOption(clientid3);
+
+ // Pass it to the server and get an offer
+ Pkt4Ptr offer1 = srv->processDiscover(dis1);
+ Pkt4Ptr offer2 = srv->processDiscover(dis2);
+ Pkt4Ptr offer3 = srv->processDiscover(dis3);
+
+ // Check if we get response at all
+ checkResponse(offer1, DHCPOFFER, 1234);
+ checkResponse(offer2, DHCPOFFER, 2345);
+ checkResponse(offer3, DHCPOFFER, 3456);
+
+ IOAddress addr1 = offer1->getYiaddr();
+ IOAddress addr2 = offer2->getYiaddr();
+ IOAddress addr3 = offer3->getYiaddr();
+
+ // Check that the assigned address is indeed from the configured pool
+ checkAddressParams(offer1, subnet_, true, true);
+ checkAddressParams(offer2, subnet_, true, true);
+ checkAddressParams(offer3, subnet_, true, true);
+
+ // Check server-ids
+ checkServerId(offer1, srv->getServerID());
+ checkServerId(offer2, srv->getServerID());
+ checkServerId(offer3, srv->getServerID());
+ checkClientId(offer1, clientid1);
+ checkClientId(offer2, clientid2);
+ checkClientId(offer3, clientid3);
+
+ // Finally check that the addresses offered are different
+ EXPECT_NE(addr1, addr2);
+ EXPECT_NE(addr2, addr3);
+ EXPECT_NE(addr3, addr1);
+ cout << "Offered address to client1=" << addr1 << endl;
+ cout << "Offered address to client2=" << addr2 << endl;
+ cout << "Offered address to client3=" << addr3 << endl;
+}
+
+// Checks whether echoing back client-id is controllable, i.e.
+// whether the server obeys echo-client-id and sends (or not)
+// client-id
+TEST_F(Dhcpv4SrvTest, discoverEchoClientId) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ NakedDhcpv4Srv srv(0);
+
+ Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ dis->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+ dis->setIface("eth1");
+ dis->setIndex(ETH1_INDEX);
+
+ // Pass it to the server and get an offer
+ Pkt4Ptr offer = srv.processDiscover(dis);
+
+ // Check if we get response at all
+ checkResponse(offer, DHCPOFFER, 1234);
+ checkClientId(offer, clientid);
+
+ ConstSrvConfigPtr cfg = CfgMgr::instance().getCurrentCfg();
+ const Subnet4Collection* subnets = cfg->getCfgSubnets4()->getAll();
+ ASSERT_EQ(1, subnets->size());
+ CfgMgr::instance().clear();
+ CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->add(*subnets->begin());
+ CfgMgr::instance().getStagingCfg()->setEchoClientId(false);
+ CfgMgr::instance().commit();
+
+ offer = srv.processDiscover(dis);
+
+ // Check if we get response at all
+ checkResponse(offer, DHCPOFFER, 1234);
+ checkClientId(offer, clientid);
+}
+
+// This test verifies that incoming DISCOVER can reuse an existing lease.
+TEST_F(Dhcpv4SrvTest, DiscoverCache) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ boost::scoped_ptr<NakedDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
+
+ // Enable lease reuse.
+ subnet_->setCacheThreshold(.1);
+
+ const IOAddress addr("192.0.2.106");
+ const uint32_t temp_valid = subnet_->getValid();
+ const int delta = 100;
+ const time_t temp_timestamp = time(NULL) - delta;
+
+ // Generate client-id also sets client_id_ member
+ OptionPtr clientid = generateClientId();
+
+ // Check that the address we are about to use is indeed in pool
+ ASSERT_TRUE(subnet_->inPool(Lease::TYPE_V4, addr));
+
+ // let's create a lease and put it in the LeaseMgr
+ uint8_t hwaddr2_data[] = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe};
+ HWAddrPtr hwaddr2(new HWAddr(hwaddr2_data, sizeof(hwaddr2_data), HTYPE_ETHER));
+ Lease4Ptr used(new Lease4(IOAddress("192.0.2.106"), hwaddr2,
+ &client_id_->getClientId()[0], client_id_->getClientId().size(),
+ temp_valid, temp_timestamp, subnet_->getID()));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(used));
+
+ // Check that the lease is really in the database
+ Lease4Ptr l = LeaseMgrFactory::instance().getLease4(addr);
+ ASSERT_TRUE(l);
+
+ // Check that preferred, valid and cltt really set.
+ // Constructed lease looks as if it was assigned 100 seconds ago
+ EXPECT_EQ(l->valid_lft_, temp_valid);
+ EXPECT_EQ(l->cltt_, temp_timestamp);
+
+ // Let's create a DISCOVER
+ Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ dis->setRemoteAddr(IOAddress(addr));
+ dis->addOption(clientid);
+ dis->setIface("eth0");
+ dis->setIndex(ETH0_INDEX);
+ dis->setHWAddr(hwaddr2);
+
+ // Pass it to the server and get an offer
+ Pkt4Ptr offer = srv->processDiscover(dis);
+
+ // Check if we get response at all
+ checkResponse(offer, DHCPOFFER, 1234);
+
+ // Check valid lifetime (temp_valid - age)
+ OptionUint32Ptr opt = boost::dynamic_pointer_cast<
+ OptionUint32>(offer->getOption(DHO_DHCP_LEASE_TIME));
+ ASSERT_TRUE(opt);
+ EXPECT_GE(subnet_->getValid() - delta, opt->getValue());
+ EXPECT_LE(subnet_->getValid() - delta - 10, opt->getValue());
+
+ // Check address
+ EXPECT_EQ(addr, offer->getYiaddr());
+
+ // Check T1
+ opt = boost::dynamic_pointer_cast<
+ OptionUint32>(offer->getOption(DHO_DHCP_RENEWAL_TIME));
+ ASSERT_TRUE(opt);
+ EXPECT_EQ(opt->getValue(), subnet_->getT1());
+
+ // Check T2
+ opt = boost::dynamic_pointer_cast<
+ OptionUint32>(offer->getOption(DHO_DHCP_REBINDING_TIME));
+ ASSERT_TRUE(opt);
+ EXPECT_EQ(opt->getValue(), subnet_->getT2());
+
+ // Check identifiers
+ checkServerId(offer, srv->getServerID());
+ checkClientId(offer, clientid);
+}
+
+// Check that option 58 and 59 are not included if they are not specified.
+TEST_F(Dhcpv4SrvTest, RequestNoTimers) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ boost::scoped_ptr<NakedDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
+
+ Pkt4Ptr req = Pkt4Ptr(new Pkt4(DHCPREQUEST, 1234));
+ req->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ req->addOption(clientid);
+ req->setIface("eth1");
+ req->setIndex(ETH1_INDEX);
+
+ // Recreate a subnet but set T1 and T2 to "unspecified".
+ subnet_ = Subnet4::create(IOAddress("192.0.2.0"), 24,
+ Triplet<uint32_t>(),
+ Triplet<uint32_t>(),
+ 3000,
+ subnet_->getID());
+ pool_ = Pool4Ptr(new Pool4(IOAddress("192.0.2.100"),
+ IOAddress("192.0.2.110")));
+ subnet_->addPool(pool_);
+ CfgMgr::instance().clear();
+ CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->add(subnet_);
+ CfgMgr::instance().commit();
+
+ // Pass it to the server and get an ACK.
+ Pkt4Ptr ack = srv->processRequest(req);
+
+ // Check if we get response at all
+ checkResponse(ack, DHCPACK, 1234);
+
+ // T1 and T2 timers must not be present.
+ checkAddressParams(ack, subnet_, false, false);
+
+ // Check identifiers
+ checkServerId(ack, srv->getServerID());
+ checkClientId(ack, clientid);
+}
+
+// Checks whether echoing back client-id is controllable
+TEST_F(Dhcpv4SrvTest, requestEchoClientId) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ NakedDhcpv4Srv srv(0);
+
+ Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPREQUEST, 1234));
+ dis->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+ dis->setIface("eth1");
+ dis->setIndex(ETH1_INDEX);
+
+ // Pass it to the server and get ACK
+ Pkt4Ptr ack = srv.processRequest(dis);
+
+ // Check if we get response at all
+ checkResponse(ack, DHCPACK, 1234);
+ checkClientId(ack, clientid);
+
+ ConstSrvConfigPtr cfg = CfgMgr::instance().getCurrentCfg();
+ const Subnet4Collection* subnets = cfg->getCfgSubnets4()->getAll();
+ ASSERT_EQ(1, subnets->size());
+ CfgMgr::instance().clear();
+ CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->add(*subnets->begin());
+ CfgMgr::instance().getStagingCfg()->setEchoClientId(false);
+ CfgMgr::instance().commit();
+
+ ack = srv.processRequest(dis);
+
+ // Check if we get response at all
+ checkResponse(ack, DHCPACK, 1234);
+ checkClientId(ack, clientid);
+}
+
+// This test verifies that incoming (positive) REQUEST/Renewing can be handled properly, that a
+// REPLY is generated, that the response has an address and that address
+// really belongs to the configured pool and that lease is actually renewed.
+//
+// expected:
+// - returned REPLY message has copy of client-id
+// - returned REPLY message has server-id
+// - returned REPLY message has IA that includes IAADDR
+// - lease is actually renewed in LeaseMgr
+TEST_F(Dhcpv4SrvTest, RenewBasic) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ boost::scoped_ptr<NakedDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
+
+ const IOAddress addr("192.0.2.106");
+ const uint32_t temp_valid = 100;
+ const time_t temp_timestamp = time(NULL) - 10;
+
+ // Generate client-id also sets client_id_ member
+ OptionPtr clientid = generateClientId();
+
+ // Check that the address we are about to use is indeed in pool
+ ASSERT_TRUE(subnet_->inPool(Lease::TYPE_V4, addr));
+
+ // let's create a lease and put it in the LeaseMgr
+ uint8_t hwaddr2_data[] = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe};
+ HWAddrPtr hwaddr2(new HWAddr(hwaddr2_data, sizeof(hwaddr2_data), HTYPE_ETHER));
+ Lease4Ptr used(new Lease4(IOAddress("192.0.2.106"), hwaddr2,
+ &client_id_->getClientId()[0], client_id_->getClientId().size(),
+ temp_valid, temp_timestamp, subnet_->getID()));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(used));
+
+ // Check that the lease is really in the database
+ Lease4Ptr l = LeaseMgrFactory::instance().getLease4(addr);
+ ASSERT_TRUE(l);
+
+ // Check that preferred, valid and cltt really set.
+ // Constructed lease looks as if it was assigned 10 seconds ago
+ EXPECT_EQ(l->valid_lft_, temp_valid);
+ EXPECT_EQ(l->cltt_, temp_timestamp);
+
+ // Let's create a RENEW
+ Pkt4Ptr req = Pkt4Ptr(new Pkt4(DHCPREQUEST, 1234));
+ req->setRemoteAddr(IOAddress(addr));
+ req->setYiaddr(addr);
+ req->setCiaddr(addr); // client's address
+ req->setIface("eth0");
+ req->setIndex(ETH0_INDEX);
+ req->setHWAddr(hwaddr2);
+
+ req->addOption(clientid);
+ req->addOption(srv->getServerID());
+
+ // Pass it to the server and hope for a REPLY
+ Pkt4Ptr ack = srv->processRequest(req);
+
+ // Check if we get response at all
+ checkResponse(ack, DHCPACK, 1234);
+ EXPECT_EQ(addr, ack->getYiaddr());
+
+ // Check that address was returned from proper range, that its lease
+ // lifetime is correct, that T1 and T2 are returned properly
+ checkAddressParams(ack, subnet_, true, true);
+
+ // Check identifiers
+ checkServerId(ack, srv->getServerID());
+ checkClientId(ack, clientid);
+
+ // Check that the lease is really in the database
+ l = checkLease(ack, clientid, req->getHWAddr(), addr);
+ ASSERT_TRUE(l);
+
+ // Check that preferred, valid and cltt were really updated
+ EXPECT_EQ(l->valid_lft_, subnet_->getValid());
+
+ // Checking for CLTT is a bit tricky if we want to avoid off by 1 errors
+ int32_t cltt = static_cast<int32_t>(l->cltt_);
+ int32_t expected = static_cast<int32_t>(time(NULL));
+ // Equality or difference by 1 between cltt and expected is ok.
+ EXPECT_GE(1, abs(cltt - expected));
+
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(addr);
+ EXPECT_TRUE(LeaseMgrFactory::instance().deleteLease(lease));
+}
+
+// Renew*Lifetime common code.
+namespace {
+
+struct ctx {
+ Dhcpv4SrvTest* test;
+ NakedDhcpv4Srv* srv;
+ const IOAddress& addr;
+ const uint32_t temp_valid;
+ const time_t temp_timestamp;
+ OptionPtr clientid;
+ HWAddrPtr hwaddr;
+ Lease4Ptr used;
+ Lease4Ptr l;
+ OptionPtr opt;
+ Pkt4Ptr req;
+ Pkt4Ptr ack;
+};
+
+void prepare(struct ctx& c) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Check that the address we are about to use is indeed in pool
+ ASSERT_TRUE(c.test->subnet_->inPool(Lease::TYPE_V4, c.addr));
+
+ // let's create a lease and put it in the LeaseMgr
+ uint8_t hwaddr_data[] = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe};
+ c.hwaddr.reset(new HWAddr(hwaddr_data, sizeof(hwaddr_data), HTYPE_ETHER));
+
+ c.used.reset(new Lease4(c.addr, c.hwaddr,
+ &c.test->client_id_->getClientId()[0],
+ c.test->client_id_->getClientId().size(),
+ c.temp_valid, c.temp_timestamp,
+ c.test->subnet_->getID()));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(c.used));
+
+ // Check that the lease is really in the database
+ c.l = LeaseMgrFactory::instance().getLease4(c.addr);
+ ASSERT_TRUE(c.l);
+
+ // Check that valid and cltt really set.
+ // Constructed lease looks as if it was assigned 10 seconds ago
+ EXPECT_EQ(c.l->valid_lft_, c.temp_valid);
+ EXPECT_EQ(c.l->cltt_, c.temp_timestamp);
+
+ // Set the valid lifetime interval.
+ c.test->subnet_->setValid(Triplet<uint32_t>(2000, 3000, 4000));
+
+ // Let's create a RENEW
+ c.req.reset(new Pkt4(DHCPREQUEST, 1234));
+ c.req->setRemoteAddr(IOAddress(c.addr));
+ c.req->setYiaddr(c.addr);
+ c.req->setCiaddr(c.addr); // client's address
+ c.req->setIface("eth0");
+ c.req->setIndex(ETH0_INDEX);
+ c.req->setHWAddr(c.hwaddr);
+
+ c.req->addOption(c.clientid);
+ c.req->addOption(c.srv->getServerID());
+
+ if (c.opt) {
+ c.req->addOption(c.opt);
+ }
+
+ // Pass it to the server and hope for a REPLY
+ c.ack = c.srv->processRequest(c.req);
+
+ // Check if we get response at all
+ c.test->checkResponse(c.ack, DHCPACK, 1234);
+ EXPECT_EQ(c.addr, c.ack->getYiaddr());
+
+ // Check identifiers
+ c.test->checkServerId(c.ack, c.srv->getServerID());
+ c.test->checkClientId(c.ack, c.clientid);
+
+ // Check that the lease is really in the database
+ c.l = c.test->checkLease(c.ack, c.clientid, c.req->getHWAddr(), c.addr);
+ ASSERT_TRUE(c.l);
+}
+
+// This test verifies that renewal returns the default valid lifetime
+// when the client does not specify a value.
+TEST_F(Dhcpv4SrvTest, RenewDefaultLifetime) {
+ boost::scoped_ptr<NakedDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
+
+ struct ctx c = {
+ this, // test
+ srv.get(), // srv
+ IOAddress("192.0.2.106"), // addr
+ 100, // temp_valid
+ time(NULL) - 10, // temp_timestamp
+ // Generate client-id also sets client_id_ member
+ generateClientId(), // clientid
+ HWAddrPtr(), // hwaddr
+ Lease4Ptr(), // used
+ Lease4Ptr(), // l
+ OptionPtr(), // opt
+ Pkt4Ptr(), // req
+ Pkt4Ptr() // acka
+ };
+
+ prepare(c);
+
+ // There is no valid lifetime hint so the default will be returned.
+
+ // Check that address was returned from proper range, that its lease
+ // lifetime is correct, that T1 and T2 are returned properly
+ checkAddressParams(c.ack, subnet_, true, true, subnet_->getValid());
+
+ // Check that valid and cltt were really updated
+ EXPECT_EQ(c.l->valid_lft_, subnet_->getValid());
+
+ // Checking for CLTT is a bit tricky if we want to avoid off by 1 errors
+ int32_t cltt = static_cast<int32_t>(c.l->cltt_);
+ int32_t expected = static_cast<int32_t>(time(NULL));
+ // Equality or difference by 1 between cltt and expected is ok.
+ EXPECT_GE(1, abs(cltt - expected));
+
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(c.addr);
+ EXPECT_TRUE(LeaseMgrFactory::instance().deleteLease(lease));
+}
+
+// This test verifies that renewal returns the specified valid lifetime
+// when the client adds an in-bound hint in the DISCOVER.
+TEST_F(Dhcpv4SrvTest, RenewHintLifetime) {
+ boost::scoped_ptr<NakedDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
+
+ struct ctx c = {
+ this, // test
+ srv.get(), // srv
+ IOAddress("192.0.2.106"), // addr
+ 100, // temp_valid
+ time(NULL) - 10, // temp_timestamp
+ // Generate client-id also sets client_id_ member
+ generateClientId(), // clientid
+ HWAddrPtr(), // hwaddr
+ Lease4Ptr(), // used
+ Lease4Ptr(), // l
+ OptionPtr(), // opt
+ Pkt4Ptr(), // req
+ Pkt4Ptr() // acka
+ };
+
+ // Add a dhcp-lease-time with an in-bound valid lifetime hint
+ // which will be returned in the OFFER.
+ uint32_t hint = 3001;
+ c.opt.reset(new OptionUint32(Option::V4, DHO_DHCP_LEASE_TIME, hint));
+
+ prepare(c);
+
+ // Check that address was returned from proper range, that its lease
+ // lifetime is correct, that T1 and T2 are returned properly
+ checkAddressParams(c.ack, subnet_, true, true, hint);
+
+ // Check that valid and cltt were really updated
+ EXPECT_EQ(c.l->valid_lft_, hint);
+
+ // Checking for CLTT is a bit tricky if we want to avoid off by 1 errors
+ int32_t cltt = static_cast<int32_t>(c.l->cltt_);
+ int32_t expected = static_cast<int32_t>(time(NULL));
+ // Equality or difference by 1 between cltt and expected is ok.
+ EXPECT_GE(1, abs(cltt - expected));
+
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(c.addr);
+ EXPECT_TRUE(LeaseMgrFactory::instance().deleteLease(lease));
+}
+
+// This test verifies that renewal returns the min valid lifetime
+// when the client adds a too small hint in the DISCOVER.
+TEST_F(Dhcpv4SrvTest, RenewMinLifetime) {
+ boost::scoped_ptr<NakedDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
+
+ struct ctx c = {
+ this, // test
+ srv.get(), // srv
+ IOAddress("192.0.2.106"), // addr
+ 100, // temp_valid
+ time(NULL) - 10, // temp_timestamp
+ // Generate client-id also sets client_id_ member
+ generateClientId(), // clientid
+ HWAddrPtr(), // hwaddr
+ Lease4Ptr(), // used
+ Lease4Ptr(), // l
+ OptionPtr(), // opt
+ Pkt4Ptr(), // req
+ Pkt4Ptr() // acka
+ };
+
+ // Add a dhcp-lease-time with too small valid lifetime hint.
+ // The min valid lifetime will be returned in the OFFER.
+ c.opt.reset(new OptionUint32(Option::V4, DHO_DHCP_LEASE_TIME, 1000));
+
+ prepare(c);
+
+ // Check that address was returned from proper range, that its lease
+ // lifetime is correct, that T1 and T2 are returned properly
+ // Note that T2 should be false for a reason which does not matter...
+ checkAddressParams(c.ack, subnet_, true, false, subnet_->getValid().getMin());
+
+ // Check that valid and cltt were really updated
+ EXPECT_EQ(c.l->valid_lft_, subnet_->getValid().getMin());
+
+ // Checking for CLTT is a bit tricky if we want to avoid off by 1 errors
+ int32_t cltt = static_cast<int32_t>(c.l->cltt_);
+ int32_t expected = static_cast<int32_t>(time(NULL));
+ // Equality or difference by 1 between cltt and expected is ok.
+ EXPECT_GE(1, abs(cltt - expected));
+
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(c.addr);
+ EXPECT_TRUE(LeaseMgrFactory::instance().deleteLease(lease));
+}
+
+// This test verifies that renewal returns the max valid lifetime
+// when the client adds a too large hint in the DISCOVER.
+TEST_F(Dhcpv4SrvTest, RenewMaxLifetime) {
+ boost::scoped_ptr<NakedDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
+
+ struct ctx c = {
+ this, // test
+ srv.get(), // srv
+ IOAddress("192.0.2.106"), // addr
+ 100, // temp_valid
+ time(NULL) - 10, // temp_timestamp
+ // Generate client-id also sets client_id_ member
+ generateClientId(), // clientid
+ HWAddrPtr(), // hwaddr
+ Lease4Ptr(), // used
+ Lease4Ptr(), // l
+ OptionPtr(), // opt
+ Pkt4Ptr(), // req
+ Pkt4Ptr() // acka
+ };
+
+ // Add a dhcp-lease-time with too large valid lifetime hint.
+ // The max valid lifetime will be returned in the OFFER.
+ c.opt.reset(new OptionUint32(Option::V4, DHO_DHCP_LEASE_TIME, 5000));
+
+ prepare(c);
+
+ // Check that address was returned from proper range, that its lease
+ // lifetime is correct, that T1 and T2 are returned properly
+ checkAddressParams(c.ack, subnet_, true, true, subnet_->getValid().getMax());
+
+ // Check that valid and cltt were really updated
+ EXPECT_EQ(c.l->valid_lft_, subnet_->getValid().getMax());
+
+ // Checking for CLTT is a bit tricky if we want to avoid off by 1 errors
+ int32_t cltt = static_cast<int32_t>(c.l->cltt_);
+ int32_t expected = static_cast<int32_t>(time(NULL));
+ // Equality or difference by 1 between cltt and expected is ok.
+ EXPECT_GE(1, abs(cltt - expected));
+
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(c.addr);
+ EXPECT_TRUE(LeaseMgrFactory::instance().deleteLease(lease));
+}
+
+} // end of Renew*Lifetime
+
+// This test verifies that incoming RENEW can reuse an existing lease.
+TEST_F(Dhcpv4SrvTest, RenewCache) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ boost::scoped_ptr<NakedDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
+
+ // Enable lease reuse.
+ subnet_->setCacheThreshold(.1);
+
+ const IOAddress addr("192.0.2.106");
+ const uint32_t temp_valid = subnet_->getValid();
+ const int delta = 100;
+ const time_t temp_timestamp = time(NULL) - delta;
+
+ // Generate client-id also sets client_id_ member
+ OptionPtr clientid = generateClientId();
+
+ // Check that the address we are about to use is indeed in pool
+ ASSERT_TRUE(subnet_->inPool(Lease::TYPE_V4, addr));
+
+ // let's create a lease and put it in the LeaseMgr
+ uint8_t hwaddr2_data[] = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe};
+ HWAddrPtr hwaddr2(new HWAddr(hwaddr2_data, sizeof(hwaddr2_data), HTYPE_ETHER));
+ Lease4Ptr used(new Lease4(IOAddress("192.0.2.106"), hwaddr2,
+ &client_id_->getClientId()[0], client_id_->getClientId().size(),
+ temp_valid, temp_timestamp, subnet_->getID()));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(used));
+
+ // Check that the lease is really in the database
+ Lease4Ptr l = LeaseMgrFactory::instance().getLease4(addr);
+ ASSERT_TRUE(l);
+
+ // Check that preferred, valid and cltt really set.
+ // Constructed lease looks as if it was assigned 100 seconds ago
+ EXPECT_EQ(l->valid_lft_, temp_valid);
+ EXPECT_EQ(l->cltt_, temp_timestamp);
+
+ // Let's create a RENEW
+ Pkt4Ptr req = Pkt4Ptr(new Pkt4(DHCPREQUEST, 1234));
+ req->setRemoteAddr(IOAddress(addr));
+ req->setYiaddr(addr);
+ req->setCiaddr(addr); // client's address
+ req->setIface("eth0");
+ req->setIndex(ETH0_INDEX);
+
+ req->addOption(clientid);
+ req->setHWAddr(hwaddr2);
+
+ // Pass it to the server and hope for a REPLY
+ Pkt4Ptr ack = srv->processRequest(req);
+
+ // Check if we get response at all
+ checkResponse(ack, DHCPACK, 1234);
+
+ // Check valid lifetime (temp_valid - age)
+ OptionUint32Ptr opt = boost::dynamic_pointer_cast<
+ OptionUint32>(ack->getOption(DHO_DHCP_LEASE_TIME));
+ ASSERT_TRUE(opt);
+ EXPECT_GE(subnet_->getValid() - delta, opt->getValue());
+ EXPECT_LE(subnet_->getValid() - delta - 10, opt->getValue());
+
+ // Check address
+ EXPECT_EQ(addr, ack->getYiaddr());
+
+ // Check T1
+ opt = boost::dynamic_pointer_cast<
+ OptionUint32>(ack->getOption(DHO_DHCP_RENEWAL_TIME));
+ ASSERT_TRUE(opt);
+ EXPECT_EQ(opt->getValue(), subnet_->getT1());
+
+ // Check T2
+ opt = boost::dynamic_pointer_cast<
+ OptionUint32>(ack->getOption(DHO_DHCP_REBINDING_TIME));
+ ASSERT_TRUE(opt);
+ EXPECT_EQ(opt->getValue(), subnet_->getT2());
+
+ // Check identifiers
+ checkServerId(ack, srv->getServerID());
+ checkClientId(ack, clientid);
+
+ // Check that the lease is really in the database
+ Lease4Ptr lease = checkLease(ack, clientid, req->getHWAddr(), addr);
+ ASSERT_TRUE(lease);
+
+ // Check that the lease was not updated
+ EXPECT_EQ(temp_timestamp, lease->cltt_);
+}
+
+// Exercises Dhcpv4Srv::buildCfgOptionList().
+TEST_F(Dhcpv4SrvTest, buildCfgOptionsList) {
+ configureServerIdentifier();
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ Pkt4Ptr query(new Pkt4(DHCPREQUEST, 1234));
+ query->addOption(generateClientId());
+ query->setHWAddr(generateHWAddr(6));
+ query->setIface("eth0");
+ query->setIndex(ETH0_INDEX);
+
+ {
+ SCOPED_TRACE("Pool value");
+
+ // Server id should come from subnet2's first pool.
+ buildCfgOptionTest(IOAddress("192.0.2.254"), query, IOAddress("192.0.2.101"), IOAddress("192.0.2.254"));
+ }
+
+ {
+ SCOPED_TRACE("Subnet value");
+
+ // Server id should come from subnet3.
+ buildCfgOptionTest(IOAddress("192.0.3.254"), query, IOAddress("192.0.3.101"), IOAddress("192.0.3.254"));
+ }
+
+ {
+ SCOPED_TRACE("Shared-network value");
+
+ // Server id should come from subnet4's shared-network.
+ buildCfgOptionTest(IOAddress("192.0.4.254"), query, IOAddress("192.0.4.101"), IOAddress("192.0.4.254"));
+ }
+
+ {
+ SCOPED_TRACE("Client-class value");
+
+ Pkt4Ptr query_with_classes(new Pkt4(DHCPREQUEST, 1234));
+ query_with_classes->addOption(generateClientId());
+ query_with_classes->setHWAddr(generateHWAddr(6));
+ query_with_classes->setIface("eth0");
+ query_with_classes->setIndex(ETH0_INDEX);
+ query_with_classes->addClass("foo");
+
+ // Server id should come from subnet5's client-class value.
+ buildCfgOptionTest(IOAddress("192.0.5.254"), query_with_classes, IOAddress("192.0.5.101"), IOAddress("192.0.5.254"));
+ }
+
+ {
+ SCOPED_TRACE("Global value if client class does not define it");
+
+ Pkt4Ptr query_with_classes(new Pkt4(DHCPREQUEST, 1234));
+ query_with_classes->addOption(generateClientId());
+ query_with_classes->setHWAddr(generateHWAddr(6));
+ query_with_classes->setIface("eth0");
+ query_with_classes->setIndex(ETH0_INDEX);
+ query_with_classes->addClass("bar");
+
+ // Server id should be global value as subnet6's client-class does not define it.
+ buildCfgOptionTest(IOAddress("10.0.0.254"), query_with_classes, IOAddress("192.0.6.101"), IOAddress("192.0.6.100"));
+ }
+
+ {
+ SCOPED_TRACE("Global value if client class does not define any option");
+
+ Pkt4Ptr query_with_classes(new Pkt4(DHCPREQUEST, 1234));
+ query_with_classes->addOption(generateClientId());
+ query_with_classes->setHWAddr(generateHWAddr(6));
+ query_with_classes->setIface("eth0");
+ query_with_classes->setIndex(ETH0_INDEX);
+ query_with_classes->addClass("xyz");
+
+ // Server id should be global value as subnet7's client-class does not define any option.
+ buildCfgOptionTest(IOAddress("10.0.0.254"), query_with_classes, IOAddress("192.0.7.101"), IOAddress("192.0.7.100"));
+ }
+
+ {
+ SCOPED_TRACE("Global value");
+
+ // Server id should be global value as lease is from subnet2's second pool.
+ buildCfgOptionTest(IOAddress("10.0.0.254"), query, IOAddress("192.0.2.201"), IOAddress("10.0.0.254"));
+ }
+}
+
+// This test verifies that the logic which matches server identifier in the
+// received message with server identifiers used by a server works correctly:
+// - a message with no server identifier is accepted,
+// - a message with a server identifier which matches one of the server
+// identifiers used by a server is accepted,
+// - a message with a server identifier which doesn't match any server
+// identifier used by a server, is not accepted.
+// - a message with a server identifier which doesn't match any server
+// identifier used by a server is accepted when the DHCP Server Identifier
+// option is configured to be ignored.
+TEST_F(Dhcpv4SrvTest, acceptServerId) {
+ configureServerIdentifier();
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ NakedDhcpv4Srv srv(0);
+
+ Pkt4Ptr pkt(new Pkt4(DHCPREQUEST, 1234));
+ // If no server identifier option is present, the message is always
+ // accepted.
+ EXPECT_TRUE(srv.acceptServerId(pkt));
+
+ // Create definition of the server identifier option.
+ OptionDefinition def("server-identifier", DHO_DHCP_SERVER_IDENTIFIER,
+ DHCP4_OPTION_SPACE, "ipv4-address", false);
+
+ // Add a server identifier option which doesn't match server ids being
+ // used by the server. The accepted server ids are the IPv4 addresses
+ // configured on the interfaces. The 10.1.2.3 is not configured on
+ // any interfaces.
+ OptionCustomPtr other_serverid(new OptionCustom(def, Option::V4));
+ other_serverid->writeAddress(IOAddress("10.1.2.3"));
+ pkt->addOption(other_serverid);
+ EXPECT_FALSE(srv.acceptServerId(pkt));
+
+ // Configure the DHCP Server Identifier to be ignored.
+ ASSERT_FALSE(CfgMgr::instance().getCurrentCfg()->getIgnoreServerIdentifier());
+ CfgMgr::instance().getCurrentCfg()->setIgnoreServerIdentifier(true);
+ EXPECT_TRUE(srv.acceptServerId(pkt));
+
+ // Restore the ignore-dhcp-server-identifier compatibility flag.
+ CfgMgr::instance().getCurrentCfg()->setIgnoreServerIdentifier(false);
+ EXPECT_FALSE(srv.acceptServerId(pkt));
+
+ // Remove the server identifier.
+ ASSERT_NO_THROW(pkt->delOption(DHO_DHCP_SERVER_IDENTIFIER));
+
+ // Add a server id being an IPv4 address configured on eth1 interface.
+ // A DHCPv4 message holding this server identifier should be accepted.
+ OptionCustomPtr eth1_serverid(new OptionCustom(def, Option::V4));
+ eth1_serverid->writeAddress(IOAddress("192.0.2.3"));
+ ASSERT_NO_THROW(pkt->addOption(eth1_serverid));
+ EXPECT_TRUE(srv.acceptServerId(pkt));
+
+ // Remove the server identifier.
+ ASSERT_NO_THROW(pkt->delOption(DHO_DHCP_SERVER_IDENTIFIER));
+
+ // Add a server id being an IPv4 address configured on eth0 interface.
+ // A DHCPv4 message holding this server identifier should be accepted.
+ OptionCustomPtr eth0_serverid(new OptionCustom(def, Option::V4));
+ eth0_serverid->writeAddress(IOAddress("10.0.0.1"));
+ ASSERT_NO_THROW(pkt->addOption(eth0_serverid));
+ EXPECT_TRUE(srv.acceptServerId(pkt));
+
+ // Remove the server identifier.
+ ASSERT_NO_THROW(pkt->delOption(DHO_DHCP_SERVER_IDENTIFIER));
+
+ // Add a server id being an IPv4 address configured on subnet3.
+ // A DHCPv4 message holding this server identifier should be accepted.
+ OptionCustomPtr subnet_serverid(new OptionCustom(def, Option::V4));
+ subnet_serverid->writeAddress(IOAddress("192.0.3.254"));
+ ASSERT_NO_THROW(pkt->addOption(subnet_serverid));
+ EXPECT_TRUE(srv.acceptServerId(pkt));
+
+ // Remove the server identifier.
+ ASSERT_NO_THROW(pkt->delOption(DHO_DHCP_SERVER_IDENTIFIER));
+
+ // Add a server id being an IPv4 address configured on shared network1.
+ // A DHCPv4 message holding this server identifier should be accepted.
+ OptionCustomPtr network_serverid(new OptionCustom(def, Option::V4));
+ network_serverid->writeAddress(IOAddress("192.0.4.254"));
+ ASSERT_NO_THROW(pkt->addOption(network_serverid));
+ EXPECT_TRUE(srv.acceptServerId(pkt));
+
+ // Remove the server identifier.
+ ASSERT_NO_THROW(pkt->delOption(DHO_DHCP_SERVER_IDENTIFIER));
+
+ // Add a server id being an IPv4 address configured on client class.
+ // A DHCPv4 message holding this server identifier should be accepted.
+ Pkt4Ptr pkt_with_classes(new Pkt4(DHCPREQUEST, 1234));
+ OptionCustomPtr class_serverid(new OptionCustom(def, Option::V4));
+ class_serverid->writeAddress(IOAddress("192.0.5.254"));
+ ASSERT_NO_THROW(pkt_with_classes->addOption(class_serverid));
+ pkt_with_classes->addClass("foo");
+ EXPECT_TRUE(srv.acceptServerId(pkt_with_classes));
+
+ // Remove the server identifier.
+ ASSERT_NO_THROW(pkt_with_classes->delOption(DHO_DHCP_SERVER_IDENTIFIER));
+
+ // Add a server id being an IPv4 address configured on global level.
+ // The configured class does not define the server id option.
+ // A DHCPv4 message holding this server identifier should be accepted.
+ Pkt4Ptr pkt_with_classes_option_not_defined(new Pkt4(DHCPREQUEST, 1234));
+ OptionCustomPtr global_serverid(new OptionCustom(def, Option::V4));
+ global_serverid->writeAddress(IOAddress("10.0.0.254"));
+ ASSERT_NO_THROW(pkt_with_classes_option_not_defined->addOption(global_serverid));
+ pkt_with_classes_option_not_defined->addClass("bar");
+ EXPECT_TRUE(srv.acceptServerId(pkt_with_classes_option_not_defined));
+
+ // Remove the server identifier.
+ ASSERT_NO_THROW(pkt_with_classes_option_not_defined->delOption(DHO_DHCP_SERVER_IDENTIFIER));
+
+ // Add a server id being an IPv4 address configured on global level.
+ // The configured class does not define any option.
+ // A DHCPv4 message holding this server identifier should be accepted.
+ Pkt4Ptr pkt_with_classes_no_options(new Pkt4(DHCPREQUEST, 1234));
+ ASSERT_NO_THROW(pkt_with_classes_no_options->addOption(global_serverid));
+ pkt_with_classes_no_options->addClass("xyz");
+ EXPECT_TRUE(srv.acceptServerId(pkt_with_classes_no_options));
+
+ // Remove the server identifier.
+ ASSERT_NO_THROW(pkt_with_classes_no_options->delOption(DHO_DHCP_SERVER_IDENTIFIER));
+
+ // Add a server id being an IPv4 address configured on global level.
+ // A DHCPv4 message holding this server identifier should be accepted.
+ ASSERT_NO_THROW(pkt->addOption(global_serverid));
+ EXPECT_TRUE(srv.acceptServerId(pkt));
+
+ // Remove the server identifier.
+ ASSERT_NO_THROW(pkt->delOption(DHO_DHCP_SERVER_IDENTIFIER));
+
+ OptionDefinitionPtr rai_def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE,
+ DHO_DHCP_AGENT_OPTIONS);
+
+ OptionBuffer override_server_id_buf(IOAddress("10.0.0.128").toBytes());
+
+ // Create RAI option.
+ OptionCustomPtr rai(new OptionCustom(*rai_def, Option::V4));
+ OptionPtr rai_override_server_id(new Option(Option::V4,
+ RAI_OPTION_SERVER_ID_OVERRIDE,
+ override_server_id_buf));
+ rai->addOption(rai_override_server_id);
+
+ // Add a server id being an IPv4 address matching RAI sub-option 11
+ // (RAI_OPTION_SERVER_ID_OVERRIDE).
+ // A DHCPv4 message holding this server identifier should be accepted.
+ Pkt4Ptr pkt_with_override_server_id(new Pkt4(DHCPREQUEST, 1234));
+ OptionCustomPtr override_serverid(new OptionCustom(def, Option::V4));
+ override_serverid->writeAddress(IOAddress("10.0.0.128"));
+
+ ASSERT_NO_THROW(pkt_with_override_server_id->addOption(override_serverid));
+ ASSERT_NO_THROW(pkt_with_override_server_id->addOption(rai));
+ EXPECT_TRUE(srv.acceptServerId(pkt_with_override_server_id));
+
+ // Remove the server identifier.
+ ASSERT_NO_THROW(pkt_with_override_server_id->delOption(DHO_DHCP_SERVER_IDENTIFIER));
+}
+
+// @todo: Implement tests for rejecting renewals
+
+// This test verifies if the sanityCheck() really checks options presence.
+TEST_F(Dhcpv4SrvTest, sanityCheck) {
+ boost::scoped_ptr<NakedDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
+
+ Pkt4Ptr pkt = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ pkt->setHWAddr(generateHWAddr(6));
+
+ // Server-id is optional for information-request, so
+ EXPECT_NO_THROW(NakedDhcpv4Srv::sanityCheck(pkt, Dhcpv4Srv::OPTIONAL));
+
+ // Empty packet, no server-id
+ EXPECT_THROW(NakedDhcpv4Srv::sanityCheck(pkt, Dhcpv4Srv::MANDATORY),
+ RFCViolation);
+
+ pkt->addOption(srv->getServerID());
+
+ // Server-id is mandatory and present = no exception
+ EXPECT_NO_THROW(NakedDhcpv4Srv::sanityCheck(pkt, Dhcpv4Srv::MANDATORY));
+
+ // Server-id is forbidden, but present => exception
+ EXPECT_THROW(NakedDhcpv4Srv::sanityCheck(pkt, Dhcpv4Srv::FORBIDDEN),
+ RFCViolation);
+
+ // There's no client-id and no HWADDR. Server needs something to
+ // identify the client
+ pkt->setHWAddr(generateHWAddr(0));
+ EXPECT_THROW(NakedDhcpv4Srv::sanityCheck(pkt, Dhcpv4Srv::MANDATORY),
+ RFCViolation);
+}
+
+} // end of anonymous namespace
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+void
+Dhcpv4SrvTest::relayAgentInfoEcho() {
+ IfaceMgrTestConfig test_config(true);
+ NakedDhcpv4Srv srv(0);
+
+ // Use of the captured DHCPDISCOVER packet requires that
+ // subnet 10.254.226.0/24 is in use, because this packet
+ // contains the giaddr which belongs to this subnet and
+ // this giaddr is used to select the subnet
+ configure(CONFIGS[0]);
+
+ // Let's create a relayed DISCOVER. This particular relayed DISCOVER has
+ // added option 82 (relay agent info) with 3 suboptions. The server
+ // is supposed to echo it back in its response.
+ Pkt4Ptr dis;
+ ASSERT_NO_THROW(dis = PktCaptures::captureRelayedDiscover());
+
+ // Simulate that we have received that traffic
+ srv.fakeReceive(dis);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered buffer4_receive callback.
+ srv.run();
+
+ // Check that the server did send a response
+ ASSERT_EQ(1, srv.fake_sent_.size());
+
+ // Make sure that we received a response
+ Pkt4Ptr offer = srv.fake_sent_.front();
+ ASSERT_TRUE(offer);
+
+ // Get Relay Agent Info from query...
+ OptionPtr rai_query = dis->getOption(DHO_DHCP_AGENT_OPTIONS);
+ ASSERT_TRUE(rai_query);
+
+ // Get Relay Agent Info from response...
+ OptionPtr rai_response = offer->getOption(DHO_DHCP_AGENT_OPTIONS);
+ ASSERT_TRUE(rai_response);
+
+ EXPECT_TRUE(rai_response->equals(rai_query));
+}
+
+void
+Dhcpv4SrvTest::badRelayAgentInfoEcho() {
+ IfaceMgrTestConfig test_config(true);
+ NakedDhcpv4Srv srv(0);
+
+ // Use of the captured DHCPDISCOVER packet requires that
+ // subnet 10.254.226.0/24 is in use, because this packet
+ // contains the giaddr which belongs to this subnet and
+ // this giaddr is used to select the subnet
+ configure(CONFIGS[0]);
+
+ // Let's create a relayed DISCOVER. This particular relayed DISCOVER has
+ // added option 82 (relay agent info) with a sub-option which does not
+ // fit in the option. Unpacking it gave an empty option which is
+ // supposed to not be echoed back in its response.
+ Pkt4Ptr dis;
+ ASSERT_NO_THROW(dis = PktCaptures::captureBadRelayedDiscover());
+
+ // Simulate that we have received that traffic
+ srv.fakeReceive(dis);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered buffer4_receive callback.
+ srv.run();
+
+ // Check that the server did send a response
+ ASSERT_EQ(1, srv.fake_sent_.size());
+
+ // Make sure that we received a response
+ Pkt4Ptr offer = srv.fake_sent_.front();
+ ASSERT_TRUE(offer);
+
+ // Get Relay Agent Info from query...
+ OptionPtr rai_query = dis->getOption(DHO_DHCP_AGENT_OPTIONS);
+ ASSERT_TRUE(rai_query);
+ ASSERT_EQ(2, rai_query->len());
+
+ // Get Relay Agent Info from response...
+ OptionPtr rai_response = offer->getOption(DHO_DHCP_AGENT_OPTIONS);
+ ASSERT_FALSE(rai_response);
+}
+
+void
+Dhcpv4SrvTest::portsClientPort() {
+ IfaceMgrTestConfig test_config(true);
+ NakedDhcpv4Srv srv(0);
+
+ // By default te client port is supposed to be zero.
+ EXPECT_EQ(0, srv.client_port_);
+
+ // Use of the captured DHCPDISCOVER packet requires that
+ // subnet 10.254.226.0/24 is in use, because this packet
+ // contains the giaddr which belongs to this subnet and
+ // this giaddr is used to select the subnet
+ configure(CONFIGS[0]);
+ srv.client_port_ = 1234;
+
+ // Let's create a relayed DISCOVER. This particular relayed DISCOVER has
+ // added option 82 (relay agent info) with 3 suboptions. The server
+ // is supposed to echo it back in its response.
+ Pkt4Ptr dis;
+ ASSERT_NO_THROW(dis = PktCaptures::captureRelayedDiscover());
+
+ // Simulate that we have received that traffic
+ srv.fakeReceive(dis);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered buffer4_receive callback.
+ srv.run();
+
+ // Check that the server did send a response
+ ASSERT_EQ(1, srv.fake_sent_.size());
+
+ // Make sure that we received a response
+ Pkt4Ptr offer = srv.fake_sent_.front();
+ ASSERT_TRUE(offer);
+
+ // Get Relay Agent Info from query...
+ EXPECT_EQ(srv.client_port_, offer->getRemotePort());
+}
+
+void
+Dhcpv4SrvTest::portsServerPort() {
+ IfaceMgrTestConfig test_config(true);
+
+ // Do not use DHCP4_SERVER_PORT here as 0 means don't open sockets.
+ NakedDhcpv4Srv srv(0);
+ EXPECT_EQ(0, srv.server_port_);
+
+ // Use of the captured DHCPDISCOVER packet requires that
+ // subnet 10.254.226.0/24 is in use, because this packet
+ // contains the giaddr which belongs to this subnet and
+ // this giaddr is used to select the subnet
+ configure(CONFIGS[0]);
+ srv.server_port_ = 1234;
+
+ // Let's create a relayed DISCOVER. This particular relayed DISCOVER has
+ // added option 82 (relay agent info) with 3 suboptions. The server
+ // is supposed to echo it back in its response.
+ Pkt4Ptr dis;
+ ASSERT_NO_THROW(dis = PktCaptures::captureRelayedDiscover());
+
+ // Simulate that we have received that traffic
+ srv.fakeReceive(dis);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered buffer4_receive callback.
+ srv.run();
+
+ // Check that the server did send a response
+ ASSERT_EQ(1, srv.fake_sent_.size());
+
+ // Make sure that we received a response
+ Pkt4Ptr offer = srv.fake_sent_.front();
+ ASSERT_TRUE(offer);
+
+ // Get Relay Agent Info from query...
+ EXPECT_EQ(srv.server_port_, offer->getLocalPort());
+}
+
+/// @brief Remove TLS parameters from configuration element.
+void removeTlsParameters(ConstElementPtr elem) {
+ if (elem) {
+ ElementPtr mutable_elem = boost::const_pointer_cast<Element>(elem);
+ std::vector<std::string> tls_parameters= {
+ "trust-anchor",
+ "cert-file",
+ "key-file",
+ "cipher-list"
+ };
+ for (auto const& parameter : tls_parameters) {
+ mutable_elem->remove(parameter);
+ }
+ }
+}
+
+void
+Dhcpv4SrvTest::loadConfigFile(const string& path) {
+ CfgMgr::instance().clear();
+
+ LibDHCP::clearRuntimeOptionDefs();
+
+ IfaceMgrTestConfig test_config(true);
+
+ // Do not use DHCP4_SERVER_PORT here as 0 means don't open sockets.
+ NakedDhcpv4Srv srv(0);
+ EXPECT_EQ(0, srv.server_port_);
+
+ ConfigBackendDHCPv4Mgr::instance().registerBackendFactory("mysql",
+ [](const db::DatabaseConnection::ParameterMap&) -> ConfigBackendDHCPv4Ptr {
+ return (ConfigBackendDHCPv4Ptr());
+ });
+
+ ConfigBackendDHCPv4Mgr::instance().registerBackendFactory("postgresql",
+ [](const db::DatabaseConnection::ParameterMap&) -> ConfigBackendDHCPv4Ptr {
+ return (ConfigBackendDHCPv4Ptr());
+ });
+
+ // TimerMgr uses IO service to run asynchronous timers.
+ TimerMgr::instance()->setIOService(srv.getIOService());
+
+ // CommandMgr uses IO service to run asynchronous socket operations.
+ CommandMgr::instance().setIOService(srv.getIOService());
+
+ // LeaseMgr uses IO service to run asynchronous timers.
+ LeaseMgr::setIOService(srv.getIOService());
+
+ // HostMgr uses IO service to run asynchronous timers.
+ HostMgr::setIOService(srv.getIOService());
+
+ Parser4Context parser;
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parser.parseFile(path, Parser4Context::PARSER_DHCP4));
+ ASSERT_TRUE(json);
+
+ // Check the logic next.
+ ConstElementPtr dhcp4 = json->get("Dhcp4");
+ ASSERT_TRUE(dhcp4);
+ ElementPtr mutable_config = boost::const_pointer_cast<Element>(dhcp4);
+ mutable_config->set(string("hooks-libraries"), Element::createList());
+ // Remove TLS parameters
+ ConstElementPtr hosts = dhcp4->get("hosts-database");
+ removeTlsParameters(hosts);
+ hosts = dhcp4->get("hosts-databases");
+ if (hosts) {
+ for (auto& host : hosts->listValue()) {
+ removeTlsParameters(host);
+ }
+ }
+ ASSERT_NO_THROW(Dhcpv4SrvTest::configure(dhcp4->str(), true, true, true, true));
+
+ LeaseMgrFactory::destroy();
+ HostMgr::create();
+
+ TimerMgr::instance()->unregisterTimers();
+
+ // Close the command socket (if it exists).
+ CommandMgr::instance().closeCommandSocket();
+
+ // Reset CommandMgr IO service.
+ CommandMgr::instance().setIOService(IOServicePtr());
+
+ // Reset LeaseMgr IO service.
+ LeaseMgr::setIOService(IOServicePtr());
+
+ // Reset HostMgr IO service.
+ HostMgr::setIOService(IOServicePtr());
+}
+
+/// @brief Class which handles initialization of database
+/// backend for testing configurations.
+class DBInitializer {
+ public:
+ /// @brief Constructor.
+ ///
+ /// Created database schema.
+ DBInitializer() {
+#if defined (HAVE_MYSQL)
+ db::test::createMySQLSchema();
+#endif
+#if defined (HAVE_PGSQL)
+ db::test::createPgSQLSchema();
+#endif
+ }
+
+ /// @brief Destructor.
+ ///
+ /// Destroys database schema.
+ ~DBInitializer() {
+#if defined (HAVE_MYSQL)
+ db::test::destroyMySQLSchema();
+#endif
+#if defined (HAVE_PGSQL)
+ db::test::destroyPgSQLSchema();
+#endif
+ }
+};
+
+void
+Dhcpv4SrvTest::checkConfigFiles() {
+ DBInitializer dbi;
+ IfaceMgrTestConfig test_config(true);
+ string path = CFG_EXAMPLES;
+ vector<string> examples = {
+ "advanced.json",
+#if defined (HAVE_MYSQL) && defined (HAVE_PGSQL)
+ "all-keys.json",
+ "all-keys-netconf.json",
+ "all-options.json",
+#endif
+ "backends.json",
+ "classify.json",
+ "classify2.json",
+ "comments.json",
+#if defined (HAVE_MYSQL)
+ "config-backend.json",
+#endif
+ "dhcpv4-over-dhcpv6.json",
+ "dnr.json",
+ "global-reservations.json",
+ "ha-load-balancing-server1-mt-with-tls.json",
+ "ha-load-balancing-server2-mt.json",
+ "hooks.json",
+ "hooks-radius.json",
+ "leases-expiration.json",
+ "multiple-options.json",
+#if defined (HAVE_MYSQL)
+ "mysql-reservations.json",
+#endif
+#if defined (HAVE_PGSQL)
+ "pgsql-reservations.json",
+#endif
+ "reservations.json",
+ "several-subnets.json",
+ "shared-network.json",
+ "single-subnet.json",
+ "vendor-specific.json",
+ "vivso.json",
+ "with-ddns.json",
+ };
+ vector<string> files;
+ for (string example : examples) {
+ string file = path + "/" + example;
+ files.push_back(file);
+ }
+ for (const auto& file: files) {
+ string label("Checking configuration from file: ");
+ label += file;
+ SCOPED_TRACE(label);
+ loadConfigFile(file);
+ }
+}
+
+} // end of isc::dhcp::test namespace
+} // end of isc::dhcp namespace
+} // end of isc namespace
+
+namespace {
+
+TEST_F(Dhcpv4SrvTest, relayAgentInfoEcho) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ relayAgentInfoEcho();
+}
+
+TEST_F(Dhcpv4SrvTest, relayAgentInfoEchoMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ relayAgentInfoEcho();
+}
+
+TEST_F(Dhcpv4SrvTest, badRelayAgentInfoEcho) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ badRelayAgentInfoEcho();
+}
+
+TEST_F(Dhcpv4SrvTest, badRelayAgentInfoEchoMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ badRelayAgentInfoEcho();
+}
+
+TEST_F(Dhcpv4SrvTest, portsClientPort) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ portsClientPort();
+}
+
+TEST_F(Dhcpv4SrvTest, portsClientPortMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ portsClientPort();
+}
+
+TEST_F(Dhcpv4SrvTest, portsServerPort) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ portsServerPort();
+}
+
+TEST_F(Dhcpv4SrvTest, portsServerPortMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ portsServerPort();
+}
+
+/// @brief Check that example files from documentation are valid (can be parsed
+/// and loaded).
+TEST_F(Dhcpv4SrvTest, checkConfigFiles) {
+ checkConfigFiles();
+}
+
+/// @todo Implement tests for subnetSelect See tests in dhcp6_srv_unittest.cc:
+/// selectSubnetAddr, selectSubnetIface, selectSubnetRelayLinkaddr,
+/// selectSubnetRelayInterfaceId. Note that the concept of interface-id is not
+/// present in the DHCPv4, so not everything is applicable directly.
+/// See ticket #3057
+
+// Checks whether the server uses default (0.0.0.0) siaddr value, unless
+// explicitly specified
+TEST_F(Dhcpv4SrvTest, siaddrDefault) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ boost::scoped_ptr<NakedDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
+ IOAddress hint("192.0.2.107");
+
+ Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ dis->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+ dis->setYiaddr(hint);
+ dis->setIface("eth1");
+ dis->setIndex(ETH1_INDEX);
+
+ // Pass it to the server and get an offer
+ Pkt4Ptr offer = srv->processDiscover(dis);
+ ASSERT_TRUE(offer);
+
+ // Check if we get response at all
+ checkResponse(offer, DHCPOFFER, 1234);
+
+ // Verify that it is 0.0.0.0
+ EXPECT_EQ("0.0.0.0", offer->getSiaddr().toText());
+}
+
+// Checks whether the server uses specified siaddr value
+TEST_F(Dhcpv4SrvTest, siaddr) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ boost::scoped_ptr<NakedDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
+ subnet_->setSiaddr(IOAddress("192.0.2.123"));
+
+ Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ dis->setRemoteAddr(IOAddress("192.0.2.1"));
+ dis->setIface("eth1");
+ dis->setIndex(ETH1_INDEX);
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+
+ // Pass it to the server and get an offer
+ Pkt4Ptr offer = srv->processDiscover(dis);
+ ASSERT_TRUE(offer);
+
+ // Check if we get response at all
+ checkResponse(offer, DHCPOFFER, 1234);
+
+ // Verify that its value is proper
+ EXPECT_EQ("192.0.2.123", offer->getSiaddr().toText());
+}
+
+// Checks if the next-server defined as global value is overridden by subnet
+// specific value and returned in server messages. There's also similar test for
+// checking parser only configuration, see Dhcp4ParserTest.nextServerOverride in
+// config_parser_unittest.cc. This test was extended to other BOOTP fixed fields.
+TEST_F(Dhcpv4SrvTest, nextServerOverride) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ NakedDhcpv4Srv srv(0);
+
+ ConstElementPtr status;
+
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"next-server\": \"192.0.0.1\", "
+ "\"server-hostname\": \"nohost\", "
+ "\"boot-file-name\": \"nofile\", "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"next-server\": \"1.2.3.4\", "
+ " \"server-hostname\": \"some-name.example.org\", "
+ " \"boot-file-name\": \"bootfile.efi\", "
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config, true));
+
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(srv, json));
+
+ CfgMgr::instance().commit();
+
+ // check if returned status is OK
+ ASSERT_TRUE(status);
+ comment_ = config::parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ dis->setRemoteAddr(IOAddress("192.0.2.1"));
+ dis->setIface("eth1");
+ dis->setIndex(ETH1_INDEX);
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+
+ // Pass it to the server and get an offer
+ Pkt4Ptr offer = srv.processDiscover(dis);
+ ASSERT_TRUE(offer);
+ EXPECT_EQ(DHCPOFFER, offer->getType());
+
+ EXPECT_EQ("1.2.3.4", offer->getSiaddr().toText());
+ std::string sname("some-name.example.org");
+ uint8_t sname_buf[Pkt4::MAX_SNAME_LEN];
+ std::memset(sname_buf, 0, Pkt4::MAX_SNAME_LEN);
+ std::memcpy(sname_buf, sname.c_str(), sname.size());
+ EXPECT_EQ(0, std::memcmp(sname_buf, &offer->getSname()[0], Pkt4::MAX_SNAME_LEN));
+ std::string filename("bootfile.efi");
+ uint8_t filename_buf[Pkt4::MAX_FILE_LEN];
+ std::memset(filename_buf, 0, Pkt4::MAX_FILE_LEN);
+ std::memcpy(filename_buf, filename.c_str(), filename.size());
+ EXPECT_EQ(0, std::memcmp(filename_buf, &offer->getFile()[0], Pkt4::MAX_FILE_LEN));
+}
+
+// Checks if the next-server defined as global value is used in responses
+// when there is no specific value defined in subnet and returned to the client
+// properly. There's also similar test for checking parser only configuration,
+// see Dhcp4ParserTest.nextServerGlobal in config_parser_unittest.cc.
+TEST_F(Dhcpv4SrvTest, nextServerGlobal) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ NakedDhcpv4Srv srv(0);
+
+ ConstElementPtr status;
+
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"next-server\": \"192.0.0.1\", "
+ "\"server-hostname\": \"some-name.example.org\", "
+ "\"boot-file-name\": \"bootfile.efi\", "
+ "\"subnet4\": [ { "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config, true));
+
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(srv, json));
+
+ CfgMgr::instance().commit();
+
+ // check if returned status is OK
+ ASSERT_TRUE(status);
+ comment_ = config::parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ dis->setRemoteAddr(IOAddress("192.0.2.1"));
+ dis->setIface("eth1");
+ dis->setIndex(ETH1_INDEX);
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+
+ // Pass it to the server and get an offer
+ Pkt4Ptr offer = srv.processDiscover(dis);
+ ASSERT_TRUE(offer);
+ EXPECT_EQ(DHCPOFFER, offer->getType());
+
+ EXPECT_EQ("192.0.0.1", offer->getSiaddr().toText());
+ std::string sname("some-name.example.org");
+ uint8_t sname_buf[Pkt4::MAX_SNAME_LEN];
+ std::memset(sname_buf, 0, Pkt4::MAX_SNAME_LEN);
+ std::memcpy(sname_buf, sname.c_str(), sname.size());
+ EXPECT_EQ(0, std::memcmp(sname_buf, &offer->getSname()[0], Pkt4::MAX_SNAME_LEN));
+ std::string filename("bootfile.efi");
+ uint8_t filename_buf[Pkt4::MAX_FILE_LEN];
+ std::memset(filename_buf, 0, Pkt4::MAX_FILE_LEN);
+ std::memcpy(filename_buf, filename.c_str(), filename.size());
+ EXPECT_EQ(0, std::memcmp(filename_buf, &offer->getFile()[0], Pkt4::MAX_FILE_LEN));
+}
+
+// Checks if client packets are classified properly using match expressions.
+TEST_F(Dhcpv4SrvTest, matchClassification) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ NakedDhcpv4Srv srv(0);
+
+ // The router class matches incoming packets with foo in a host-name
+ // option (code 12) and sets an ip-forwarding option in the response.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ] }, "
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"valid-lifetime\": 4000, "
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], "
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/24\" } ], "
+ "\"client-classes\": [ "
+ "{ \"name\": \"router\", "
+ " \"option-data\": ["
+ " { \"name\": \"ip-forwarding\", "
+ " \"data\": \"true\" } ], "
+ " \"test\": \"option[12].text == 'foo'\" },"
+ "{ \"name\": \"template-client-id\","
+ " \"template-test\": \"substring(option[61].hex,0,3)\" },"
+ "{ \"name\": \"SPAWN_template-hostname_foo\" },"
+ "{ \"name\": \"template-hostname\","
+ " \"template-test\": \"option[12].text\"} ] }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(srv, json));
+ ASSERT_TRUE(status);
+ comment_ = config::parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ CfgMgr::instance().commit();
+
+ // Create packets with enough to select the subnet
+ OptionPtr clientid = generateClientId();
+ Pkt4Ptr query1(new Pkt4(DHCPDISCOVER, 1234));
+ query1->setRemoteAddr(IOAddress("192.0.2.1"));
+ query1->addOption(clientid);
+ query1->setIface("eth1");
+ query1->setIndex(ETH1_INDEX);
+ Pkt4Ptr query2(new Pkt4(DHCPDISCOVER, 1234));
+ query2->setRemoteAddr(IOAddress("192.0.2.1"));
+ query2->addOption(clientid);
+ query2->setIface("eth1");
+ query2->setIndex(ETH1_INDEX);
+ Pkt4Ptr query3(new Pkt4(DHCPDISCOVER, 1234));
+ query3->setRemoteAddr(IOAddress("192.0.2.1"));
+ query3->addOption(clientid);
+ query3->setIface("eth1");
+ query3->setIndex(ETH1_INDEX);
+
+ // Create and add a PRL option to the first 2 queries
+ OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4,
+ DHO_DHCP_PARAMETER_REQUEST_LIST));
+ ASSERT_TRUE(prl);
+ prl->addValue(DHO_IP_FORWARDING);
+ query1->addOption(prl);
+ query2->addOption(prl);
+
+ // Create and add a host-name option to the first and last queries
+ OptionStringPtr hostname(new OptionString(Option::V4, 12, "foo"));
+ ASSERT_TRUE(hostname);
+ query1->addOption(hostname);
+ query3->addOption(hostname);
+
+ // Classify packets
+ srv.classifyPacket(query1);
+ srv.classifyPacket(query2);
+ srv.classifyPacket(query3);
+
+ EXPECT_EQ(query1->classes_.size(), 6);
+ EXPECT_EQ(query2->classes_.size(), 3);
+ EXPECT_EQ(query3->classes_.size(), 6);
+
+ EXPECT_TRUE(query1->inClass("ALL"));
+ EXPECT_TRUE(query2->inClass("ALL"));
+ EXPECT_TRUE(query3->inClass("ALL"));
+
+ // Packets with the exception of the second should be in the router class
+ EXPECT_TRUE(query1->inClass("router"));
+ EXPECT_FALSE(query2->inClass("router"));
+ EXPECT_TRUE(query3->inClass("router"));
+
+ EXPECT_TRUE(query1->inClass("template-hostname"));
+ EXPECT_FALSE(query2->inClass("template-hostname"));
+ EXPECT_TRUE(query3->inClass("template-hostname"));
+
+ EXPECT_TRUE(query1->inClass("SPAWN_template-hostname_foo"));
+ EXPECT_FALSE(query2->inClass("SPAWN_template-hostname_foo"));
+ EXPECT_TRUE(query3->inClass("SPAWN_template-hostname_foo"));
+
+ EXPECT_TRUE(query1->inClass("template-client-id"));
+ EXPECT_TRUE(query2->inClass("template-client-id"));
+ EXPECT_TRUE(query3->inClass("template-client-id"));
+
+ EXPECT_TRUE(query1->inClass("SPAWN_template-client-id_def"));
+ EXPECT_TRUE(query2->inClass("SPAWN_template-client-id_def"));
+ EXPECT_TRUE(query3->inClass("SPAWN_template-client-id_def"));
+
+ // Process queries
+ Pkt4Ptr response1 = srv.processDiscover(query1);
+ Pkt4Ptr response2 = srv.processDiscover(query2);
+ Pkt4Ptr response3 = srv.processDiscover(query3);
+
+ // Classification processing should add an ip-forwarding option
+ OptionPtr opt1 = response1->getOption(DHO_IP_FORWARDING);
+ EXPECT_TRUE(opt1);
+
+ // But only for the first query: second was not classified
+ OptionPtr opt2 = response2->getOption(DHO_IP_FORWARDING);
+ EXPECT_FALSE(opt2);
+
+ // But only for the first query: third has no PRL
+ OptionPtr opt3 = response3->getOption(DHO_IP_FORWARDING);
+ EXPECT_FALSE(opt3);
+}
+
+// Checks if client packets are classified properly using match expressions
+// using option names
+TEST_F(Dhcpv4SrvTest, matchClassificationOptionName) {
+ NakedDhcpv4Srv srv(0);
+
+ // The router class matches incoming packets with foo in a host-name
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ] }, "
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"valid-lifetime\": 4000, "
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], "
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/24\" } ], "
+ "\"client-classes\": [ "
+ "{ \"name\": \"router\", "
+ " \"test\": \"option[host-name].text == 'foo'\" } ] }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(srv, json));
+ ASSERT_TRUE(status);
+ comment_ = config::parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ CfgMgr::instance().commit();
+
+ // Create a packet with enough to select the subnet
+ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
+ query->setRemoteAddr(IOAddress("192.0.2.1"));
+
+ // Create and add a host-name option to the query
+ OptionStringPtr hostname(new OptionString(Option::V4, 12, "foo"));
+ ASSERT_TRUE(hostname);
+ query->addOption(hostname);
+
+ // Classify packets
+ srv.classifyPacket(query);
+
+ // The query should be in the router class
+ EXPECT_TRUE(query->inClass("router"));
+}
+
+// Checks if client packets are classified properly using match expressions
+// using option names and definitions
+TEST_F(Dhcpv4SrvTest, matchClassificationOptionDef) {
+ NakedDhcpv4Srv srv(0);
+
+ // The router class matches incoming packets with foo in a defined
+ // option
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ] }, "
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"valid-lifetime\": 4000, "
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], "
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/24\" } ], "
+ "\"client-classes\": [ "
+ "{ \"name\": \"router\", "
+ " \"test\": \"option[my-host-name].text == 'foo'\" } ], "
+ "\"option-def\": [ {"
+ " \"name\": \"my-host-name\", "
+ " \"code\": 250, "
+ " \"type\": \"string\" } ] }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(srv, json));
+ ASSERT_TRUE(status);
+ comment_ = config::parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ CfgMgr::instance().commit();
+
+ // Create a packet with enough to select the subnet
+ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
+ query->setRemoteAddr(IOAddress("192.0.2.1"));
+
+ // Create and add a my-host-name option to the query
+ OptionStringPtr hostname(new OptionString(Option::V4, 250, "foo"));
+ ASSERT_TRUE(hostname);
+ query->addOption(hostname);
+
+ // Classify packets
+ srv.classifyPacket(query);
+
+ // The query should be in the router class
+ EXPECT_TRUE(query->inClass("router"));
+}
+
+// Checks subnet options have the priority over class options
+TEST_F(Dhcpv4SrvTest, subnetClassPriority) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ NakedDhcpv4Srv srv(0);
+
+ // Subnet sets an ip-forwarding option in the response.
+ // The router class matches incoming packets with foo in a host-name
+ // option (code 12) and sets an ip-forwarding option in the response.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ] }, "
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"valid-lifetime\": 4000, "
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], "
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"option-data\": ["
+ " { \"name\": \"ip-forwarding\", "
+ " \"data\": \"false\" } ] } ], "
+ "\"client-classes\": [ "
+ "{ \"name\": \"router\","
+ " \"option-data\": ["
+ " { \"name\": \"ip-forwarding\", "
+ " \"data\": \"true\" } ], "
+ " \"test\": \"option[12].text == 'foo'\" } ] }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(srv, json));
+ ASSERT_TRUE(status);
+ comment_ = config::parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ CfgMgr::instance().commit();
+
+ // Create a packet with enough to select the subnet and go through
+ // the DISCOVER processing
+ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
+ query->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ query->addOption(clientid);
+ query->setIface("eth1");
+ query->setIndex(ETH1_INDEX);
+
+ // Create and add a PRL option to the query
+ OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4,
+ DHO_DHCP_PARAMETER_REQUEST_LIST));
+ ASSERT_TRUE(prl);
+ prl->addValue(DHO_IP_FORWARDING);
+ query->addOption(prl);
+
+ // Create and add a host-name option to the query
+ OptionStringPtr hostname(new OptionString(Option::V4, 12, "foo"));
+ ASSERT_TRUE(hostname);
+ query->addOption(hostname);
+
+ // Classify the packet
+ srv.classifyPacket(query);
+
+ // The packet should be in the router class
+ EXPECT_TRUE(query->inClass("router"));
+
+ // Process the query
+ Pkt4Ptr response = srv.processDiscover(query);
+
+ // Processing should add an ip-forwarding option
+ OptionPtr opt = response->getOption(DHO_IP_FORWARDING);
+ ASSERT_TRUE(opt);
+ ASSERT_GT(opt->len(), opt->getHeaderLen());
+ // Classification sets the value to true/1, subnet to false/0
+ // Here subnet has the priority
+ EXPECT_EQ(0, opt->getUint8());
+}
+
+// Checks subnet options have the priority over global options
+TEST_F(Dhcpv4SrvTest, subnetGlobalPriority) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ NakedDhcpv4Srv srv(0);
+
+ // Subnet and global set an ip-forwarding option in the response.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ] }, "
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"valid-lifetime\": 4000, "
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], "
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"option-data\": ["
+ " { \"name\": \"ip-forwarding\", "
+ " \"data\": \"false\" } ] } ], "
+ "\"option-data\": ["
+ " { \"name\": \"ip-forwarding\", "
+ " \"data\": \"true\" } ] }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(srv, json));
+ ASSERT_TRUE(status);
+ comment_ = config::parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ CfgMgr::instance().commit();
+
+ // Create a packet with enough to select the subnet and go through
+ // the DISCOVER processing
+ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
+ query->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ query->addOption(clientid);
+ query->setIface("eth1");
+ query->setIndex(ETH1_INDEX);
+
+ // Create and add a PRL option to the query
+ OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4,
+ DHO_DHCP_PARAMETER_REQUEST_LIST));
+ ASSERT_TRUE(prl);
+ prl->addValue(DHO_IP_FORWARDING);
+ query->addOption(prl);
+
+ // Create and add a host-name option to the query
+ OptionStringPtr hostname(new OptionString(Option::V4, 12, "foo"));
+ ASSERT_TRUE(hostname);
+ query->addOption(hostname);
+
+ // Process the query
+ Pkt4Ptr response = srv.processDiscover(query);
+
+ // Processing should add an ip-forwarding option
+ OptionPtr opt = response->getOption(DHO_IP_FORWARDING);
+ ASSERT_TRUE(opt);
+ ASSERT_GT(opt->len(), opt->getHeaderLen());
+ // Global sets the value to true/1, subnet to false/0
+ // Here subnet has the priority
+ EXPECT_EQ(0, opt->getUint8());
+}
+
+// Checks class options have the priority over global options
+TEST_F(Dhcpv4SrvTest, classGlobalPriority) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ NakedDhcpv4Srv srv(0);
+
+ // A global ip-forwarding option is set in the response.
+ // The router class matches incoming packets with foo in a host-name
+ // option (code 12) and sets an ip-forwarding option in the response.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ] }, "
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"valid-lifetime\": 4000, "
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], "
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/24\" } ], "
+ "\"option-data\": ["
+ " { \"name\": \"ip-forwarding\", "
+ " \"data\": \"false\" } ], "
+ "\"client-classes\": [ "
+ "{ \"name\": \"router\","
+ " \"option-data\": ["
+ " { \"name\": \"ip-forwarding\", "
+ " \"data\": \"true\" } ], "
+ " \"test\": \"option[12].text == 'foo'\" } ] }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(srv, json));
+ ASSERT_TRUE(status);
+ comment_ = config::parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ CfgMgr::instance().commit();
+
+ // Create a packet with enough to select the subnet and go through
+ // the DISCOVER processing
+ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
+ query->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ query->addOption(clientid);
+ query->setIface("eth1");
+ query->setIndex(ETH1_INDEX);
+
+ // Create and add a PRL option to the query
+ OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4,
+ DHO_DHCP_PARAMETER_REQUEST_LIST));
+ ASSERT_TRUE(prl);
+ prl->addValue(DHO_IP_FORWARDING);
+ query->addOption(prl);
+
+ // Create and add a host-name option to the query
+ OptionStringPtr hostname(new OptionString(Option::V4, 12, "foo"));
+ ASSERT_TRUE(hostname);
+ query->addOption(hostname);
+
+ // Classify the packet
+ srv.classifyPacket(query);
+
+ // The packet should be in the router class
+ EXPECT_TRUE(query->inClass("router"));
+
+ // Process the query
+ Pkt4Ptr response = srv.processDiscover(query);
+
+ // Processing should add an ip-forwarding option
+ OptionPtr opt = response->getOption(DHO_IP_FORWARDING);
+ ASSERT_TRUE(opt);
+ ASSERT_GT(opt->len(), opt->getHeaderLen());
+ // Classification sets the value to true/1, global to false/0
+ // Here class has the priority
+ EXPECT_NE(0, opt->getUint8());
+}
+
+// Checks class options have the priority over global persistent options
+TEST_F(Dhcpv4SrvTest, classGlobalPersistency) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ NakedDhcpv4Srv srv(0);
+
+ // A global ip-forwarding option is set in the response.
+ // The router class matches incoming packets with foo in a host-name
+ // option (code 12) and sets an ip-forwarding option in the response.
+ // Note the persistency flag follows a "OR" semantic so to set
+ // it to false (or to leave the default) has no effect.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ] }, "
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"valid-lifetime\": 4000, "
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], "
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/24\" } ], "
+ "\"option-data\": ["
+ " { \"name\": \"ip-forwarding\", "
+ " \"data\": \"false\", "
+ " \"always-send\": true } ], "
+ "\"client-classes\": [ "
+ "{ \"name\": \"router\","
+ " \"option-data\": ["
+ " { \"name\": \"ip-forwarding\", "
+ " \"data\": \"true\", "
+ " \"always-send\": false } ], "
+ " \"test\": \"option[12].text == 'foo'\" } ] }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(srv, json));
+ ASSERT_TRUE(status);
+ comment_ = config::parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ CfgMgr::instance().commit();
+
+ // Create a packet with enough to select the subnet and go through
+ // the DISCOVER processing
+ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
+ query->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ query->addOption(clientid);
+ query->setIface("eth1");
+ query->setIndex(ETH1_INDEX);
+
+ // Do not add a PRL
+ OptionPtr prl = query->getOption(DHO_DHCP_PARAMETER_REQUEST_LIST);
+ EXPECT_FALSE(prl);
+
+ // Create and add a host-name option to the query
+ OptionStringPtr hostname(new OptionString(Option::V4, 12, "foo"));
+ ASSERT_TRUE(hostname);
+ query->addOption(hostname);
+
+ // Classify the packet
+ srv.classifyPacket(query);
+
+ // The packet should be in the router class
+ EXPECT_TRUE(query->inClass("router"));
+
+ // Process the query
+ Pkt4Ptr response = srv.processDiscover(query);
+
+ // Processing should add an ip-forwarding option
+ OptionPtr opt = response->getOption(DHO_IP_FORWARDING);
+ ASSERT_TRUE(opt);
+ ASSERT_GT(opt->len(), opt->getHeaderLen());
+ // Classification sets the value to true/1, global to false/0
+ // Here class has the priority
+ EXPECT_NE(0, opt->getUint8());
+}
+
+// Checks if the client-class field is indeed used for subnet selection.
+// Note that packet classification is already checked in Dhcpv4SrvTest
+// .*Classification above.
+TEST_F(Dhcpv4SrvTest, clientClassify) {
+
+ // 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
+ // subnet. That ugly hack will be removed in #3242, currently
+ // under review.
+
+ // The second subnet does not play any role here. The client's
+ // IP address belongs to the first subnet, so only that first
+ // subnet is being tested.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"client-class\": \"foo\", "
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/24\" }, "
+ "{ \"pools\": [ { \"pool\": \"192.0.3.1 - 192.0.3.100\" } ],"
+ " \"client-class\": \"xyzzy\", "
+ " \"id\": 2, "
+ " \"subnet\": \"192.0.3.0/24\" } "
+ "],"
+ "\"valid-lifetime\": 4000 }";
+
+ ASSERT_NO_THROW(configure(config, true, false));
+
+ // 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"));
+ dis->setIface("eth0");
+ dis->setIndex(ETH0_INDEX);
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+
+ // This discover does not belong to foo class, so it will not
+ // be serviced
+ bool drop = false;
+ EXPECT_FALSE(srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+
+ // 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, drop));
+ EXPECT_FALSE(drop);
+
+ // Let's add it to matching class.
+ dis->addClass("foo");
+
+ // This time it should work
+ EXPECT_TRUE(srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+}
+
+// Checks if the client-class field is indeed used for pool selection.
+TEST_F(Dhcpv4SrvTest, clientPoolClassify) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ NakedDhcpv4Srv srv(0);
+
+ // This test configures 2 pools.
+ // The second pool does not play any role here. The client's
+ // IP address belongs to the first pool, so only that first
+ // pool is being tested.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { "
+ " \"pool\": \"192.0.2.1 - 192.0.2.100\", "
+ " \"client-class\": \"foo\" }, "
+ " { \"pool\": \"192.0.3.1 - 192.0.3.100\", "
+ " \"client-class\": \"xyzzy\" } ], "
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.0.0/16\" } "
+ "],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config, true));
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(srv, json));
+
+ CfgMgr::instance().commit();
+
+ // check if returned status is OK
+ ASSERT_TRUE(status);
+ comment_ = config::parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ // 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"));
+ dis->setIface("eth0");
+ dis->setIndex(ETH0_INDEX);
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+
+ // This discover does not belong to foo class, so it will not
+ // be serviced
+ Pkt4Ptr offer = srv.processDiscover(dis);
+ EXPECT_FALSE(offer);
+
+ // Let's add the packet to bar class and try again.
+ dis->addClass("bar");
+
+ // Still not supported, because it belongs to wrong class.
+ offer = srv.processDiscover(dis);
+ EXPECT_FALSE(offer);
+
+ // Let's add it to matching class.
+ dis->addClass("foo");
+
+ // This time it should work
+ offer = srv.processDiscover(dis);
+ ASSERT_TRUE(offer);
+ EXPECT_EQ(DHCPOFFER, offer->getType());
+ EXPECT_FALSE(offer->getYiaddr().isV4Zero());
+}
+
+// Checks if the KNOWN built-in classes is indeed used for pool selection.
+TEST_F(Dhcpv4SrvTest, clientPoolClassifyKnown) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ NakedDhcpv4Srv srv(0);
+
+ // This test configures 2 pools.
+ // The first one requires reservation, the second does the opposite.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { "
+ " \"pool\": \"192.0.2.1 - 192.0.2.100\", "
+ " \"client-class\": \"KNOWN\" }, "
+ " { \"pool\": \"192.0.3.1 - 192.0.3.100\", "
+ " \"client-class\": \"UNKNOWN\" } ], "
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.0.0/16\" } "
+ "],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config, true));
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(srv, json));
+
+ CfgMgr::instance().commit();
+
+ // check if returned status is OK
+ ASSERT_TRUE(status);
+ comment_ = config::parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ // 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"));
+ dis->setIface("eth0");
+ dis->setIndex(ETH0_INDEX);
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+
+ // First pool requires reservation so the second will be used
+ Pkt4Ptr offer = srv.processDiscover(dis);
+ ASSERT_TRUE(offer);
+ EXPECT_EQ(DHCPOFFER, offer->getType());
+ EXPECT_EQ("192.0.3.1", offer->getYiaddr().toText());
+}
+
+// Checks if the UNKNOWN built-in classes is indeed used for pool selection.
+TEST_F(Dhcpv4SrvTest, clientPoolClassifyUnknown) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ NakedDhcpv4Srv srv(0);
+
+ // This test configures 2 pools.
+ // The first one requires no reservation, the second does the opposite.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { "
+ " \"pool\": \"192.0.2.1 - 192.0.2.100\", "
+ " \"client-class\": \"UNKNOWN\" }, "
+ " { \"pool\": \"192.0.3.1 - 192.0.3.100\", "
+ " \"client-class\": \"KNOWN\" } ], "
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.0.0/16\", "
+ " \"reservations\": [ { "
+ " \"hw-address\": \"00:00:00:11:22:33\", "
+ " \"hostname\": \"foo.bar\" } ] } "
+ "],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config, true));
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(srv, json));
+
+ CfgMgr::instance().commit();
+
+ // check if returned status is OK
+ ASSERT_TRUE(status);
+ comment_ = config::parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ // 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"));
+ dis->setIface("eth0");
+ dis->setIndex(ETH0_INDEX);
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+
+ // Set hardware address / identifier
+ const HWAddr& hw = HWAddr::fromText("00:00:00:11:22:33");
+ HWAddrPtr hw_addr(new HWAddr(hw));
+ dis->setHWAddr(hw_addr);
+
+ // First pool requires no reservation so the second will be used
+ Pkt4Ptr offer = srv.processDiscover(dis);
+ ASSERT_TRUE(offer);
+ EXPECT_EQ(DHCPOFFER, offer->getType());
+ EXPECT_EQ("192.0.3.1", offer->getYiaddr().toText());
+}
+
+// Verifies private option deferred processing
+TEST_F(Dhcpv4SrvTest, privateOption) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ NakedDhcpv4Srv srv(0);
+
+ // Same than option43Class but with private options
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ] }, "
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"valid-lifetime\": 4000, "
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], "
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"client-classes\": [ "
+ "{ \"name\": \"private\", "
+ " \"test\": \"option[234].exists\", "
+ " \"option-def\": [ "
+ " { \"code\": 245, "
+ " \"name\": \"privint\", "
+ " \"type\": \"uint32\" } ],"
+ " \"option-data\": [ "
+ " { \"code\": 234, "
+ " \"data\": \"01\" }, "
+ " { \"name\": \"privint\", "
+ " \"data\": \"12345678\" } ] } ] }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(srv, json));
+ ASSERT_TRUE(status);
+ comment_ = config::parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ CfgMgr::instance().commit();
+
+ // Create a packet with enough to select the subnet and go through
+ // the DISCOVER processing
+ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
+ query->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ query->addOption(clientid);
+ query->setIface("eth1");
+ query->setIndex(ETH1_INDEX);
+
+ // Create and add a private option with code 234
+ OptionBuffer buf;
+ buf.push_back(0x01);
+ OptionPtr opt1(new Option(Option::V4, 234, buf));
+ query->addOption(opt1);
+ query->getDeferredOptions().push_back(234);
+
+ // Create and add a private option with code 245
+ buf.clear();
+ buf.push_back(0x87);
+ buf.push_back(0x65);
+ buf.push_back(0x43);
+ buf.push_back(0x21);
+ OptionPtr opt2(new Option(Option::V4, 245, buf));
+ query->addOption(opt2);
+ query->getDeferredOptions().push_back(245);
+
+ // Create and add a PRL option to the query
+ OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4,
+ DHO_DHCP_PARAMETER_REQUEST_LIST));
+ ASSERT_TRUE(prl);
+ prl->addValue(234);
+ prl->addValue(245);
+ query->addOption(prl);
+
+ srv.classifyPacket(query);
+ ASSERT_NO_THROW(srv.deferredUnpack(query));
+
+ // Check if the option 245 was re-unpacked
+ opt2 = query->getOption(245);
+ OptionUint32Ptr opt32 = boost::dynamic_pointer_cast<OptionUint32>(opt2);
+ EXPECT_TRUE(opt32);
+
+ // Pass it to the server and get an offer
+ Pkt4Ptr offer = srv.processDiscover(query);
+
+ // Check if we get response at all
+ checkResponse(offer, DHCPOFFER, 1234);
+
+ // Processing should add an option with code 234
+ OptionPtr opt = offer->getOption(234);
+ EXPECT_TRUE(opt);
+
+ // And an option with code 245
+ opt = offer->getOption(245);
+ ASSERT_TRUE(opt);
+ // Verifies the content
+ opt32 = boost::dynamic_pointer_cast<OptionUint32>(opt);
+ ASSERT_TRUE(opt32);
+ EXPECT_EQ(12345678, opt32->getValue());
+}
+
+// Checks effect of persistency (aka always-send) flag on the PRL.
+TEST_F(Dhcpv4SrvTest, prlPersistency) {
+ IfaceMgrTestConfig test_config(true);
+
+ ASSERT_NO_THROW(configure(CONFIGS[2]));
+
+ // Create a packet with enough to select the subnet and go through
+ // the DISCOVER processing
+ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
+ query->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ query->addOption(clientid);
+ query->setIface("eth1");
+ query->setIndex(ETH1_INDEX);
+
+ // Create and add a PRL option for another option
+ OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4,
+ DHO_DHCP_PARAMETER_REQUEST_LIST));
+ ASSERT_TRUE(prl);
+ prl->addValue(DHO_ARP_CACHE_TIMEOUT);
+ query->addOption(prl);
+
+ // Create and add a host-name option to the query
+ OptionStringPtr hostname(new OptionString(Option::V4, 12, "foo"));
+ ASSERT_TRUE(hostname);
+ query->addOption(hostname);
+
+ // Let the server process it.
+ Pkt4Ptr response = srv_.processDiscover(query);
+
+ // Processing should add an ip-forwarding option
+ ASSERT_TRUE(response->getOption(DHO_IP_FORWARDING));
+ // But no default-ip-ttl
+ ASSERT_FALSE(response->getOption(DHO_DEFAULT_IP_TTL));
+ // Nor an arp-cache-timeout
+ ASSERT_FALSE(response->getOption(DHO_ARP_CACHE_TIMEOUT));
+
+ // Reset PRL adding default-ip-ttl
+ query->delOption(DHO_DHCP_PARAMETER_REQUEST_LIST);
+ prl->addValue(DHO_DEFAULT_IP_TTL);
+ query->addOption(prl);
+
+ // Let the server process it again.
+ response = srv_.processDiscover(query);
+
+ // Processing should add an ip-forwarding option
+ ASSERT_TRUE(response->getOption(DHO_IP_FORWARDING));
+ // and now a default-ip-ttl
+ ASSERT_TRUE(response->getOption(DHO_DEFAULT_IP_TTL));
+ // and still no arp-cache-timeout
+ ASSERT_FALSE(response->getOption(DHO_ARP_CACHE_TIMEOUT));
+}
+
+// Checks effect of cancellation (aka never-send) flag.
+TEST_F(Dhcpv4SrvTest, neverSend) {
+ IfaceMgrTestConfig test_config(true);
+
+ ASSERT_NO_THROW(configure(CONFIGS[3]));
+
+ // Create a packet with enough to select the subnet and go through
+ // the DISCOVER processing
+ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
+ query->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ query->addOption(clientid);
+ query->setIface("eth1");
+ query->setIndex(ETH1_INDEX);
+
+ // Create and add a PRL option for another option
+ OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4,
+ DHO_DHCP_PARAMETER_REQUEST_LIST));
+ ASSERT_TRUE(prl);
+ prl->addValue(DHO_ARP_CACHE_TIMEOUT);
+ query->addOption(prl);
+
+ // Create and add a host-name option to the query
+ OptionStringPtr hostname(new OptionString(Option::V4, 12, "foo"));
+ ASSERT_TRUE(hostname);
+ query->addOption(hostname);
+
+ // Let the server process it.
+ Pkt4Ptr response = srv_.processDiscover(query);
+
+ // Processing should not add an ip-forwarding option
+ ASSERT_FALSE(response->getOption(DHO_IP_FORWARDING));
+ // And no default-ip-ttl
+ ASSERT_FALSE(response->getOption(DHO_DEFAULT_IP_TTL));
+ // Nor an arp-cache-timeout
+ ASSERT_FALSE(response->getOption(DHO_ARP_CACHE_TIMEOUT));
+
+ // Reset PRL adding default-ip-ttl
+ query->delOption(DHO_DHCP_PARAMETER_REQUEST_LIST);
+ prl->addValue(DHO_DEFAULT_IP_TTL);
+ query->addOption(prl);
+
+ // Let the server process it again.
+ response = srv_.processDiscover(query);
+
+ // Processing should not add an ip-forwarding option
+ ASSERT_FALSE(response->getOption(DHO_IP_FORWARDING));
+ // And now a default-ip-ttl
+ ASSERT_TRUE(response->getOption(DHO_DEFAULT_IP_TTL));
+ // And still no arp-cache-timeout
+ ASSERT_FALSE(response->getOption(DHO_ARP_CACHE_TIMEOUT));
+}
+
+// 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-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { \"pool\": \"192.0.2.2 - 192.0.2.100\" } ],"
+ " \"relay\": { "
+ " \"ip-address\": \"192.0.5.1\""
+ " },"
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/24\" }, "
+ "{ \"pools\": [ { \"pool\": \"192.0.3.1 - 192.0.3.100\" } ],"
+ " \"relay\": { "
+ " \"ip-address\": \"192.0.5.2\""
+ " },"
+ " \"id\": 2, "
+ " \"subnet\": \"192.0.3.0/24\" } "
+ "],"
+ "\"valid-lifetime\": 4000 }";
+
+ // Use this config to set up the server
+ ASSERT_NO_THROW(configure(config, true, false));
+
+ // Let's get the subnet configuration objects
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ ASSERT_EQ(2, subnets->size());
+
+ // Let's get them for easy reference
+ Subnet4Ptr subnet1 = *subnets->begin();
+ Subnet4Ptr subnet2 = *std::next(subnets->begin());
+ 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->setIndex(ETH0_INDEX);
+ 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"));
+ bool drop = false;
+ EXPECT_TRUE(subnet1 == srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+
+ // Relay belongs to the second subnet, so it should be selected.
+ dis->setGiaddr(IOAddress("192.0.3.1"));
+ EXPECT_TRUE(subnet2 == srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+
+ // 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, drop));
+ EXPECT_FALSE(drop);
+
+ // The same check for the second subnet...
+ dis->setGiaddr(IOAddress("192.0.5.2"));
+ EXPECT_TRUE(subnet2 == srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+
+ // 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, drop));
+ EXPECT_FALSE(drop);
+
+ // 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, drop));
+ EXPECT_FALSE(drop);
+}
+
+// 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-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { \"pool\": \"192.0.2.2 - 192.0.2.100\" } ],"
+ " \"client-class\": \"foo\", "
+ " \"relay\": { "
+ " \"ip-address\": \"192.0.5.1\""
+ " },"
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/24\" }, "
+ "{ \"pools\": [ { \"pool\": \"192.0.3.1 - 192.0.3.100\" } ],"
+ " \"relay\": { "
+ " \"ip-address\": \"192.0.5.1\""
+ " },"
+ " \"id\": 2, "
+ " \"subnet\": \"192.0.3.0/24\" } "
+ "],"
+ "\"valid-lifetime\": 4000 }";
+
+ // Use this config to set up the server
+ ASSERT_NO_THROW(configure(config, true, false));
+
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ ASSERT_EQ(2, subnets->size());
+
+ // Let's get them for easy reference
+ Subnet4Ptr subnet1 = *subnets->begin();
+ Subnet4Ptr subnet2 = *std::next(subnets->begin());
+ 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->setIndex(ETH0_INDEX);
+ 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.
+ bool drop = false;
+ EXPECT_TRUE(subnet2 == srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+
+ // 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, drop));
+ EXPECT_FALSE(drop);
+}
+
+// Checks if a RAI link selection sub-option works as expected
+TEST_F(Dhcpv4SrvTest, relayLinkSelect) {
+
+ // We have 3 subnets defined.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { \"pool\": \"192.0.2.2 - 192.0.2.100\" } ],"
+ " \"relay\": { "
+ " \"ip-address\": \"192.0.5.1\""
+ " },"
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/24\" }, "
+ "{ \"pools\": [ { \"pool\": \"192.0.3.1 - 192.0.3.100\" } ],"
+ " \"id\": 2, "
+ " \"subnet\": \"192.0.3.0/24\" }, "
+ "{ \"pools\": [ { \"pool\": \"192.0.4.1 - 192.0.4.100\" } ],"
+ " \"client-class\": \"foo\", "
+ " \"id\": 3, "
+ " \"subnet\": \"192.0.4.0/24\" } "
+ "],"
+ "\"valid-lifetime\": 4000 }";
+
+ // Use this config to set up the server
+ ASSERT_NO_THROW(configure(config, true, false));
+
+ // Let's get the subnet configuration objects
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ ASSERT_EQ(3, subnets->size());
+
+ // Let's get them for easy reference
+ auto subnet_it = subnets->begin();
+ Subnet4Ptr subnet1 = *subnet_it;
+ ++subnet_it;
+ Subnet4Ptr subnet2 = *subnet_it;
+ ++subnet_it;
+ Subnet4Ptr subnet3 = *subnet_it;
+ ASSERT_TRUE(subnet1);
+ ASSERT_TRUE(subnet2);
+ ASSERT_TRUE(subnet3);
+
+ // Let's create a packet.
+ Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ dis->setRemoteAddr(IOAddress("192.0.2.1"));
+ dis->setIface("eth0");
+ dis->setIndex(ETH0_INDEX);
+ dis->setHops(1);
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+
+ // Let's create a Relay Agent Information option
+ OptionDefinitionPtr rai_def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE,
+ DHO_DHCP_AGENT_OPTIONS);
+ ASSERT_TRUE(rai_def);
+ OptionCustomPtr rai(new OptionCustom(*rai_def, Option::V4));
+ ASSERT_TRUE(rai);
+ IOAddress addr("192.0.3.2");
+ OptionPtr ols(new Option(Option::V4,
+ RAI_OPTION_LINK_SELECTION,
+ addr.toBytes()));
+ ASSERT_TRUE(ols);
+ rai->addOption(ols);
+
+ // This is just a sanity check, we're using regular method: ciaddr 192.0.3.1
+ // belongs to the second subnet, so it is selected
+ dis->setGiaddr(IOAddress("192.0.3.1"));
+ bool drop = false;
+ EXPECT_TRUE(subnet2 == srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+
+ // Setup a relay override for the first subnet as it has a high precedence
+ dis->setGiaddr(IOAddress("192.0.5.1"));
+ EXPECT_TRUE(subnet1 == srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+
+ // Put a RAI to select back the second subnet as it has
+ // the highest precedence
+ dis->addOption(rai);
+ EXPECT_TRUE(subnet2 == srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+
+ // Subnet select option has a lower precedence
+ OptionDefinitionPtr sbnsel_def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE,
+ DHO_SUBNET_SELECTION);
+ ASSERT_TRUE(sbnsel_def);
+ OptionCustomPtr sbnsel(new OptionCustom(*sbnsel_def, Option::V4));
+ ASSERT_TRUE(sbnsel);
+ sbnsel->writeAddress(IOAddress("192.0.2.3"));
+ dis->addOption(sbnsel);
+ EXPECT_TRUE(subnet2 == srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+
+ // But, when RAI exists without the link selection option, we should
+ // fall back to the subnet selection option.
+ rai->delOption(RAI_OPTION_LINK_SELECTION);
+ dis->delOption(DHO_DHCP_AGENT_OPTIONS);
+ dis->addOption(rai);
+ dis->setGiaddr(IOAddress("192.0.4.1"));
+ EXPECT_TRUE(subnet1 == srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+
+ // Check client-classification still applies
+ IOAddress addr_foo("192.0.4.2");
+ ols.reset(new Option(Option::V4, RAI_OPTION_LINK_SELECTION,
+ addr_foo.toBytes()));
+ dis->delOption(DHO_SUBNET_SELECTION);
+ dis->delOption(DHO_DHCP_AGENT_OPTIONS);
+ rai->addOption(ols);
+ dis->addOption(rai);
+
+ // Note it shall fail (vs. try the next criterion).
+ EXPECT_FALSE(srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+ // Add the packet to the class and check again: now it shall succeed
+ dis->addClass("foo");
+ EXPECT_TRUE(subnet3 == srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+
+ // Check it fails with a bad address in the sub-option
+ IOAddress addr_bad("10.0.0.1");
+ ols.reset(new Option(Option::V4, RAI_OPTION_LINK_SELECTION,
+ addr_bad.toBytes()));
+ rai->delOption(RAI_OPTION_LINK_SELECTION);
+ dis->delOption(DHO_DHCP_AGENT_OPTIONS);
+ rai->addOption(ols);
+ dis->addOption(rai);
+ EXPECT_FALSE(srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+}
+
+// Checks if a RAI link selection compatibility preferences work as expected
+TEST_F(Dhcpv4SrvTest, relayIgnoreLinkSelect) {
+
+ // We have 3 subnets defined.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"compatibility\": { \"ignore-rai-link-selection\": true },"
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { \"pool\": \"192.0.2.2 - 192.0.2.100\" } ],"
+ " \"relay\": { "
+ " \"ip-address\": \"192.0.5.1\""
+ " },"
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/24\" }, "
+ "{ \"pools\": [ { \"pool\": \"192.0.3.1 - 192.0.3.100\" } ],"
+ " \"id\": 2, "
+ " \"subnet\": \"192.0.3.0/24\" }, "
+ "{ \"pools\": [ { \"pool\": \"192.0.4.1 - 192.0.4.100\" } ],"
+ " \"client-class\": \"foo\", "
+ " \"id\": 3, "
+ " \"subnet\": \"192.0.4.0/24\" } "
+ "],"
+ "\"valid-lifetime\": 4000 }";
+
+ // Use this config to set up the server
+ ASSERT_NO_THROW(configure(config, true, false));
+
+ // Let's get the subnet configuration objects
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ ASSERT_EQ(3, subnets->size());
+
+ // Let's get them for easy reference
+ auto subnet_it = subnets->begin();
+ Subnet4Ptr subnet1 = *subnet_it;
+ ++subnet_it;
+ Subnet4Ptr subnet2 = *subnet_it;
+ ++subnet_it;
+ Subnet4Ptr subnet3 = *subnet_it;
+ ASSERT_TRUE(subnet1);
+ ASSERT_TRUE(subnet2);
+ ASSERT_TRUE(subnet3);
+
+ // Let's create a packet.
+ Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ dis->setRemoteAddr(IOAddress("192.0.2.1"));
+ dis->setIface("eth0");
+ dis->setIndex(ETH0_INDEX);
+ dis->setHops(1);
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+
+ // Let's create a Relay Agent Information option
+ OptionDefinitionPtr rai_def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE,
+ DHO_DHCP_AGENT_OPTIONS);
+ ASSERT_TRUE(rai_def);
+ OptionCustomPtr rai(new OptionCustom(*rai_def, Option::V4));
+ ASSERT_TRUE(rai);
+ IOAddress addr("192.0.3.2");
+ OptionPtr ols(new Option(Option::V4,
+ RAI_OPTION_LINK_SELECTION,
+ addr.toBytes()));
+ ASSERT_TRUE(ols);
+ rai->addOption(ols);
+
+ // This is just a sanity check, we're using regular method: ciaddr 192.0.3.1
+ // belongs to the second subnet, so it is selected
+ dis->setGiaddr(IOAddress("192.0.3.1"));
+ bool drop = false;
+ EXPECT_TRUE(subnet2 == srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+
+ // Setup a relay override for the first subnet as it has a high precedence
+ dis->setGiaddr(IOAddress("192.0.5.1"));
+ EXPECT_TRUE(subnet1 == srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+
+ // Put a RAI to select back the second subnet as it has
+ // the highest precedence, but it should be ignored due
+ // to the ignore-rai-link-selection compatibility config
+ dis->addOption(rai);
+ EXPECT_TRUE(subnet1 == srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+
+ // Subnet select option has a lower precedence, but will succeed
+ // because RAI link selection suboptions are being ignored
+ OptionDefinitionPtr sbnsel_def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE,
+ DHO_SUBNET_SELECTION);
+ ASSERT_TRUE(sbnsel_def);
+ OptionCustomPtr sbnsel(new OptionCustom(*sbnsel_def, Option::V4));
+ ASSERT_TRUE(sbnsel);
+ sbnsel->writeAddress(IOAddress("192.0.2.3"));
+ dis->addOption(sbnsel);
+ EXPECT_TRUE(subnet1 == srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+
+ // But, when RAI exists without the link selection option, we should
+ // fall back to the subnet selection option.
+ rai->delOption(RAI_OPTION_LINK_SELECTION);
+ dis->delOption(DHO_DHCP_AGENT_OPTIONS);
+ dis->addOption(rai);
+ dis->setGiaddr(IOAddress("192.0.4.1"));
+ EXPECT_TRUE(subnet1 == srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+
+ // Check client-classification still applies
+ IOAddress addr_foo("192.0.4.2");
+ ols.reset(new Option(Option::V4, RAI_OPTION_LINK_SELECTION,
+ addr_foo.toBytes()));
+ dis->delOption(DHO_SUBNET_SELECTION);
+ dis->delOption(DHO_DHCP_AGENT_OPTIONS);
+ rai->addOption(ols);
+ dis->addOption(rai);
+
+ // Note it shall fail (vs. try the next criterion).
+ EXPECT_FALSE(srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+ // Add the packet to the class and check again: now it shall succeed
+ dis->addClass("foo");
+ EXPECT_TRUE(subnet3 == srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+
+ // Check it succeeds even with a bad address in the sub-option
+ IOAddress addr_bad("10.0.0.1");
+ ols.reset(new Option(Option::V4, RAI_OPTION_LINK_SELECTION,
+ addr_bad.toBytes()));
+ rai->delOption(RAI_OPTION_LINK_SELECTION);
+ dis->delOption(DHO_DHCP_AGENT_OPTIONS);
+ rai->addOption(ols);
+ dis->addOption(rai);
+ EXPECT_TRUE(srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+}
+
+// Checks if a subnet selection option works as expected
+TEST_F(Dhcpv4SrvTest, subnetSelect) {
+
+ // We have 3 subnets defined.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { \"pool\": \"192.0.2.2 - 192.0.2.100\" } ],"
+ " \"relay\": { "
+ " \"ip-address\": \"192.0.5.1\""
+ " },"
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/24\" }, "
+ "{ \"pools\": [ { \"pool\": \"192.0.3.1 - 192.0.3.100\" } ],"
+ " \"id\": 2, "
+ " \"subnet\": \"192.0.3.0/24\" }, "
+ "{ \"pools\": [ { \"pool\": \"192.0.4.1 - 192.0.4.100\" } ],"
+ " \"client-class\": \"foo\", "
+ " \"id\": 3, "
+ " \"subnet\": \"192.0.4.0/24\" } "
+ "],"
+ "\"valid-lifetime\": 4000 }";
+
+ // Use this config to set up the server
+ ASSERT_NO_THROW(configure(config, true, false));
+
+ // Let's get the subnet configuration objects
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ ASSERT_EQ(3, subnets->size());
+
+ // Let's get them for easy reference
+ auto subnet_it = subnets->begin();
+ Subnet4Ptr subnet1 = *subnet_it;
+ ++subnet_it;
+ Subnet4Ptr subnet2 = *subnet_it;
+ ++subnet_it;
+ Subnet4Ptr subnet3 = *subnet_it;
+ ASSERT_TRUE(subnet1);
+ ASSERT_TRUE(subnet2);
+ ASSERT_TRUE(subnet3);
+
+ // Let's create a packet.
+ Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ dis->setRemoteAddr(IOAddress("192.0.2.1"));
+ dis->setIface("eth0");
+ dis->setIndex(ETH0_INDEX);
+ dis->setHops(1);
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+
+ // Let's create a Subnet Selection option
+ OptionDefinitionPtr sbnsel_def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE,
+ DHO_SUBNET_SELECTION);
+ ASSERT_TRUE(sbnsel_def);
+ OptionCustomPtr sbnsel(new OptionCustom(*sbnsel_def, Option::V4));
+ ASSERT_TRUE(sbnsel);
+ sbnsel->writeAddress(IOAddress("192.0.3.2"));
+
+ // This is just a sanity check, we're using regular method: ciaddr 192.0.3.1
+ // belongs to the second subnet, so it is selected
+ dis->setGiaddr(IOAddress("192.0.3.1"));
+ bool drop = false;
+ EXPECT_TRUE(subnet2 == srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+
+ // Setup a relay override for the first subnet as it has a high precedence
+ dis->setGiaddr(IOAddress("192.0.5.1"));
+ EXPECT_TRUE(subnet1 == srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+
+ // Put a subnet select option to select back the second subnet as
+ // it has the second highest precedence
+ dis->addOption(sbnsel);
+ EXPECT_TRUE(subnet2 == srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+
+ // Check client-classification still applies
+ sbnsel->writeAddress(IOAddress("192.0.4.2"));
+ // Note it shall fail (vs. try the next criterion).
+ EXPECT_FALSE(srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+ // Add the packet to the class and check again: now it shall succeed
+ dis->addClass("foo");
+ EXPECT_TRUE(subnet3 == srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+
+ // Check it fails with a bad address in the sub-option
+ sbnsel->writeAddress(IOAddress("10.0.0.1"));
+ EXPECT_FALSE(srv_.selectSubnet(dis, drop));
+ EXPECT_FALSE(drop);
+}
+
+// This test verifies that the direct message is dropped when it has been
+// received by the server via an interface for which there is no subnet
+// configured. It also checks that the message is not dropped (is processed)
+// when it is relayed or unicast.
+TEST_F(Dhcpv4SrvTest, acceptDirectRequest) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ NakedDhcpv4Srv srv(0);
+
+ Pkt4Ptr pkt(new Pkt4(DHCPDISCOVER, 1234));
+ // Set Giaddr and local server's unicast address, but don't set hops.
+ // Hops value should not matter. The server will treat the message
+ // with the hops value of 0 and non-zero giaddr as relayed.
+ pkt->setGiaddr(IOAddress("192.0.10.1"));
+ pkt->setRemoteAddr(IOAddress("0.0.0.0"));
+ pkt->setLocalAddr(IOAddress("192.0.2.3"));
+ pkt->setIface("eth1");
+ pkt->setIndex(ETH1_INDEX);
+ EXPECT_TRUE(srv.accept(pkt));
+
+ // Let's set hops and check that the message is still accepted as
+ // a relayed message.
+ pkt->setHops(1);
+ EXPECT_TRUE(srv.accept(pkt));
+
+ // Make it a direct message but keep unicast server's address. The
+ // messages sent to unicast address should be accepted as they are
+ // most likely to renew existing leases. The server should respond
+ // to renews so they have to be accepted and processed.
+ pkt->setHops(0);
+ pkt->setGiaddr(IOAddress("0.0.0.0"));
+ EXPECT_TRUE(srv.accept(pkt));
+
+ // Direct message is now sent to a broadcast address. The server
+ // should accept this message because it has been received via
+ // eth1 for which there is a subnet configured (see test fixture
+ // class constructor).
+ pkt->setLocalAddr(IOAddress("255.255.255.255"));
+ EXPECT_TRUE(srv.accept(pkt));
+
+ // For eth0, there is no subnet configured. Such message is expected
+ // to be silently dropped.
+ pkt->setIface("eth0");
+ pkt->setIndex(ETH0_INDEX);
+ EXPECT_FALSE(srv.accept(pkt));
+
+ // But, if the message is unicast it should be accepted, even though
+ // it has been received via eth0.
+ pkt->setLocalAddr(IOAddress("10.0.0.1"));
+ EXPECT_TRUE(srv.accept(pkt));
+
+ // For the DHCPINFORM the ciaddr should be set or at least the source
+ // address.
+ pkt->setType(DHCPINFORM);
+ pkt->setRemoteAddr(IOAddress("10.0.0.101"));
+ EXPECT_TRUE(srv.accept(pkt));
+
+ // When neither ciaddr nor source address is present, the packet should
+ // be dropped.
+ pkt->setRemoteAddr(IOAddress("0.0.0.0"));
+ EXPECT_FALSE(srv.accept(pkt));
+
+ // When ciaddr is set, the packet should be accepted.
+ pkt->setCiaddr(IOAddress("10.0.0.1"));
+ EXPECT_TRUE(srv.accept(pkt));
+}
+
+// This test checks that the server rejects a message with invalid type.
+TEST_F(Dhcpv4SrvTest, acceptMessageType) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ NakedDhcpv4Srv srv(0);
+
+ // Specify messages to be accepted by the server.
+ int allowed[] = {
+ DHCPDISCOVER,
+ DHCPREQUEST,
+ DHCPRELEASE,
+ DHCPDECLINE,
+ DHCPINFORM
+ };
+ size_t allowed_size = sizeof(allowed) / sizeof(allowed[0]);
+ // Check that the server actually accepts these message types.
+ for (size_t i = 0; i < allowed_size; ++i) {
+ EXPECT_TRUE(srv.acceptMessageType(Pkt4Ptr(new Pkt4(allowed[i], 1234))))
+ << "Test failed for message type " << i;
+ }
+ // Specify messages which server is supposed to drop.
+ int not_allowed[] = {
+ DHCPOFFER,
+ DHCPACK,
+ DHCPNAK,
+ DHCPLEASEQUERY,
+ DHCPLEASEUNASSIGNED,
+ DHCPLEASEUNKNOWN,
+ DHCPLEASEACTIVE,
+ DHCPBULKLEASEQUERY,
+ DHCPLEASEQUERYDONE,
+ };
+ size_t not_allowed_size = sizeof(not_allowed) / sizeof(not_allowed[0]);
+ // Actually check that the server will drop these messages.
+ for (size_t i = 0; i < not_allowed_size; ++i) {
+ EXPECT_FALSE(srv.acceptMessageType(Pkt4Ptr(new Pkt4(not_allowed[i],
+ 1234))))
+ << "Test failed for message type " << i;
+ }
+
+ // Verify that we drop packets with no option 53
+ // Make a BOOTP packet (i.e. no option 53)
+ std::vector<uint8_t> bin;
+ const char* bootp_txt =
+ "01010601002529b629b600000000000000000000000000000ace5001944452fe711700"
+ "0000000000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000000000"
+ "000000000000000000000000000000000000000000000000000063825363521b010400"
+ "020418020600237453fc48090b0000118b06010401020300ff00000000000000000000"
+ "0000000000000000000000000000000000000000";
+
+ isc::util::encode::decodeHex(bootp_txt, bin);
+ Pkt4Ptr pkt(new Pkt4(&bin[0], bin.size()));
+ pkt->unpack();
+ ASSERT_EQ(DHCP_NOTYPE, pkt->getType());
+ EXPECT_FALSE(srv.acceptMessageType(Pkt4Ptr(new Pkt4(&bin[0], bin.size()))));
+
+ // Verify that we drop packets with types >= DHCP_TYPES_EOF
+ // Make Discover with type changed to 0xff
+ std::vector<uint8_t> bin2;
+ const char* invalid_msg_type =
+ "010106015d05478d000000000000000000000000000000000afee20120e52ab8151400"
+ "0000000000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000638253633501ff3707"
+ "0102030407067d3c0a646f63736973332e303a7d7f0000118b7a010102057501010102"
+ "010303010104010105010106010107010f0801100901030a01010b01180c01010d0200"
+ "400e0200100f010110040000000211010014010015013f160101170101180104190104"
+ "1a01041b01201c01021d01081e01201f01102001102101022201012301002401002501"
+ "01260200ff2701012b59020345434d030b45434d3a45524f55544552040d3242523232"
+ "39553430303434430504312e3034060856312e33332e30330707322e332e3052320806"
+ "30303039354209094347333030304443520a074e657467656172fe01083d0fff2ab815"
+ "140003000120e52ab81514390205dc5219010420000002020620e52ab8151409090000"
+ "118b0401020300ff";
+
+ bin.clear();
+ isc::util::encode::decodeHex(invalid_msg_type, bin);
+ pkt.reset(new Pkt4(&bin[0], bin.size()));
+ pkt->unpack();
+ ASSERT_EQ(0xff, pkt->getType());
+ EXPECT_FALSE(srv.acceptMessageType(pkt));
+}
+
+// Test checks whether statistic is bumped up appropriately when Decline
+// message is received.
+TEST_F(Dhcpv4SrvTest, statisticsDecline) {
+ NakedDhcpv4Srv srv(0);
+
+ pretendReceivingPkt(srv, CONFIGS[0], DHCPDECLINE, "pkt4-decline-received");
+}
+
+// Test checks whether statistic is bumped up appropriately when Offer
+// message is received (this should never happen in a sane network).
+TEST_F(Dhcpv4SrvTest, statisticsOfferRcvd) {
+ NakedDhcpv4Srv srv(0);
+
+ pretendReceivingPkt(srv, CONFIGS[0], DHCPOFFER, "pkt4-offer-received");
+}
+
+// Test checks whether statistic is bumped up appropriately when Ack
+// message is received (this should never happen in a sane network).
+TEST_F(Dhcpv4SrvTest, statisticsAckRcvd) {
+ NakedDhcpv4Srv srv(0);
+
+ pretendReceivingPkt(srv, CONFIGS[0], DHCPACK, "pkt4-ack-received");
+}
+
+// Test checks whether statistic is bumped up appropriately when Nak
+// message is received (this should never happen in a sane network).
+TEST_F(Dhcpv4SrvTest, statisticsNakRcvd) {
+ NakedDhcpv4Srv srv(0);
+
+ pretendReceivingPkt(srv, CONFIGS[0], DHCPNAK, "pkt4-nak-received");
+}
+
+// Test checks whether statistic is bumped up appropriately when Release
+// message is received.
+TEST_F(Dhcpv4SrvTest, statisticsReleaseRcvd) {
+ NakedDhcpv4Srv srv(0);
+
+ pretendReceivingPkt(srv, CONFIGS[0], DHCPRELEASE, "pkt4-release-received");
+}
+
+// Test checks whether statistic is bumped up appropriately when unknown
+// message is received.
+TEST_F(Dhcpv4SrvTest, statisticsUnknownRcvd) {
+ NakedDhcpv4Srv srv(0);
+
+ pretendReceivingPkt(srv, CONFIGS[0], 200, "pkt4-unknown-received");
+
+ // There should also be pkt4-receive-drop stat bumped up
+ using namespace isc::stats;
+ StatsMgr& mgr = StatsMgr::instance();
+ ObservationPtr drop_stat = mgr.getObservation("pkt4-receive-drop");
+
+ // This statistic must be present and must be set to 1.
+ ASSERT_TRUE(drop_stat);
+ EXPECT_EQ(1, drop_stat->getInteger().first);
+}
+
+// This test verifies that the server is able to handle an empty client-id
+// in incoming client message.
+TEST_F(Dhcpv4SrvTest, emptyClientId) {
+ IfaceMgrTestConfig test_config(true);
+ Dhcp4Client client;
+
+ EXPECT_NO_THROW(configure(CONFIGS[0], *client.getServer()));
+
+ // Tell the client to not send client-id on its own.
+ client.includeClientId("");
+
+ // Instead, tell him to send this extra option, which happens to be
+ // an empty client-id.
+ OptionPtr empty_client_id(new Option(Option::V4, DHO_DHCP_CLIENT_IDENTIFIER));
+ client.addExtraOption(empty_client_id);
+
+ // Let's check whether the server is able to process this packet without
+ // throwing any exceptions. We don't care whether the server sent any
+ // responses or not. The goal is to check that the server didn't throw
+ // any exceptions.
+ EXPECT_NO_THROW(client.doDORA());
+}
+
+// This test verifies that the server is able to handle too long client-id
+// in incoming client message.
+TEST_F(Dhcpv4SrvTest, tooLongClientId) {
+ IfaceMgrTestConfig test_config(true);
+ Dhcp4Client client;
+
+ EXPECT_NO_THROW(configure(CONFIGS[0], *client.getServer()));
+
+ // Tell the client to not send client-id on its own.
+ client.includeClientId("");
+
+ // Instead, tell him to send this extra option, which happens to be
+ // an empty client-id.
+ std::vector<uint8_t> data(250, 250);
+ OptionPtr long_client_id(new Option(Option::V4, DHO_DHCP_CLIENT_IDENTIFIER,
+ data));
+ client.addExtraOption(long_client_id);
+
+ // Let's check whether the server is able to process this packet without
+ // throwing any exceptions. We don't care whether the server sent any
+ // responses or not. The goal is to check that the server didn't throw
+ // any exceptions.
+ EXPECT_NO_THROW(client.doDORA());
+}
+
+// Checks if user-contexts are parsed properly.
+TEST_F(Dhcpv4SrvTest, userContext) {
+
+ IfaceMgrTestConfig test_config(true);
+
+ NakedDhcpv4Srv srv(0);
+
+ // This config has one subnet with user-context with one
+ // pool (also with context). Make sure the configuration could be accepted.
+ EXPECT_NO_THROW(configure(CONFIGS[4]));
+
+ // Now make sure the data was not lost.
+ ConstSrvConfigPtr cfg = CfgMgr::instance().getCurrentCfg();
+ const Subnet4Collection* subnets = cfg->getCfgSubnets4()->getAll();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(1, subnets->size());
+
+ // Let's get the subnet and check its context.
+ Subnet4Ptr subnet1 = *subnets->begin();
+ ASSERT_TRUE(subnet1);
+ ASSERT_TRUE(subnet1->getContext());
+ EXPECT_EQ("{ \"secure\": false }", subnet1->getContext()->str());
+
+ // Ok, not get the sole pool in it and check its context, too.
+ PoolCollection pools = subnet1->getPools(Lease::TYPE_V4);
+ ASSERT_EQ(1, pools.size());
+ ASSERT_TRUE(pools[0]);
+ ASSERT_TRUE(pools[0]->getContext());
+ EXPECT_EQ("{ \"value\": 42 }", pools[0]->getContext()->str());
+}
+
+// Verify that fixed fields are set from classes in the same order
+// as class options.
+TEST_F(Dhcpv4SrvTest, fixedFieldsInClassOrder) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ NakedDhcpv4Srv srv(0);
+
+ std::string config = R"(
+ {
+ "interfaces-config": { "interfaces": [ "*" ] },
+ "client-classes": [
+ {
+ "name":"one",
+ "server-hostname": "server_one",
+ "next-server": "192.0.2.111",
+ "boot-file-name":"one.boot",
+ "option-data": [
+ {
+ "name": "domain-name",
+ "data": "one.example.com"
+ }]
+ },
+ {
+ "name":"two",
+ "server-hostname": "server_two",
+ "next-server":"192.0.2.222",
+ "boot-file-name":"two.boot",
+ "option-data": [
+ {
+ "name": "domain-name",
+ "data": "two.example.com"
+ }]
+ },
+ {
+ "name":"next-server-only",
+ "next-server":"192.0.2.100"
+ },
+ {
+ "name":"server-hostname-only",
+ "server-hostname": "server_only"
+ },
+ {
+ "name":"bootfile-only",
+ "boot-file-name": "only.boot"
+ }],
+
+ "subnet4": [
+ {
+ "id": 1,
+ "subnet": "192.0.2.0/24",
+ "pools": [ { "pool": "192.0.2.1 - 192.0.2.100" } ],
+ "reservations": [
+ {
+ "hw-address": "08:00:27:25:d3:01",
+ "client-classes": [ "one", "two" ]
+ },
+ {
+ "hw-address": "08:00:27:25:d3:02",
+ "client-classes": [ "two", "one" ]
+ },
+ {
+ "hw-address": "08:00:27:25:d3:03",
+ "client-classes": [ "server-hostname-only", "bootfile-only", "next-server-only" ]
+ }]
+ }]
+ }
+ )";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW_LOG(json = parseDHCP4(config));
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(srv, json));
+ ASSERT_TRUE(status);
+ comment_ = config::parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ CfgMgr::instance().commit();
+
+ struct Scenario {
+ std::string hw_str_;
+ std::string exp_classes_;
+ std::string exp_server_hostname_;
+ std::string exp_next_server_;
+ std::string exp_bootfile_;
+ std::string exp_domain_name_;
+ };
+
+ const std::vector<Scenario> scenarios = {
+ {
+ "08:00:27:25:d3:01",
+ "ALL, one, two, KNOWN",
+ "server_one",
+ "192.0.2.111",
+ "one.boot",
+ "one.example.com"
+ },
+ {
+ "08:00:27:25:d3:02",
+ "ALL, two, one, KNOWN",
+ "server_two",
+ "192.0.2.222",
+ "two.boot",
+ "two.example.com"
+ },
+ {
+ "08:00:27:25:d3:03",
+ "ALL, server-hostname-only, bootfile-only, next-server-only, KNOWN",
+ "server_only",
+ "192.0.2.100",
+ "only.boot",
+ ""
+ }
+ };
+
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE(scenario.hw_str_); {
+ // Build a DISCOVER
+ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
+ query->setRemoteAddr(IOAddress("192.0.2.1"));
+ query->setIface("eth1");
+
+ HWAddrPtr hw_addr(new HWAddr(HWAddr::fromText(scenario.hw_str_, 10)));
+ query->setHWAddr(hw_addr);
+
+ srv.classifyPacket(query);
+
+ // Process it.
+ Pkt4Ptr response = srv.processDiscover(query);
+
+ // Make sure class list is as expected.
+ ASSERT_EQ(scenario.exp_classes_, query->getClasses().toText());
+
+ // Now check the fixed fields.
+ checkStringInBuffer(scenario.exp_server_hostname_, response->getSname());
+ EXPECT_EQ(scenario.exp_next_server_, response->getSiaddr().toText());
+ checkStringInBuffer(scenario.exp_bootfile_, response->getFile());
+
+ // Check domain name option.
+ OptionPtr opt = response->getOption(DHO_DOMAIN_NAME);
+ if (scenario.exp_domain_name_.empty()) {
+ ASSERT_FALSE(opt);
+ } else {
+ ASSERT_TRUE(opt);
+ OptionStringPtr opstr = boost::dynamic_pointer_cast<OptionString>(opt);
+ ASSERT_TRUE(opstr);
+ EXPECT_EQ(scenario.exp_domain_name_, opstr->getValue());
+ }
+ }
+ }
+}
+
+/// @todo: Implement proper tests for MySQL lease/host database,
+/// see ticket #4214.
+
+} // end of anonymous namespace
diff --git a/src/bin/dhcp4/tests/dhcp4_test_utils.cc b/src/bin/dhcp4/tests/dhcp4_test_utils.cc
new file mode 100644
index 0000000..045fe6f
--- /dev/null
+++ b/src/bin/dhcp4/tests/dhcp4_test_utils.cc
@@ -0,0 +1,1011 @@
+// Copyright (C) 2013-2023 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <asiolink/io_address.h>
+#include <cc/data.h>
+#include <cc/command_interpreter.h>
+#include <dhcp4/json_config_parser.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <dhcp/libdhcp++.h>
+#include <dhcp/option4_addrlst.h>
+#include <dhcp/option_int.h>
+#include <dhcp/option_int_array.h>
+#include <dhcp/option_custom.h>
+#include <dhcp/iface_mgr.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcp/tests/pkt_captures.h>
+#include <dhcpsrv/cfg_db_access.h>
+#include <dhcpsrv/cfg_multi_threading.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/lease.h>
+#include <dhcpsrv/lease_mgr.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <log/logger_support.h>
+#include <stats/stats_mgr.h>
+#include <sstream>
+
+using namespace std;
+using namespace isc::asiolink;
+using namespace isc::data;
+using namespace isc::util;
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+BaseServerTest::BaseServerTest()
+ : original_datadir_(CfgMgr::instance().getDataDir()) {
+ CfgMgr::instance().setDataDir(TEST_DATA_BUILDDIR);
+}
+
+BaseServerTest::~BaseServerTest() {
+ // Remove default lease file.
+ std::ostringstream s2;
+ s2 << CfgMgr::instance().getDataDir() << "/kea-leases4.csv";
+ static_cast<void>(::remove(s2.str().c_str()));
+
+ // Revert to original data directory.
+ CfgMgr::instance().setDataDir(original_datadir_);
+
+ // Revert to unit test logging, in case the test reconfigured it.
+ isc::log::initLogger();
+}
+
+Dhcpv4SrvTest::Dhcpv4SrvTest()
+ : rcode_(-1), srv_(0), multi_threading_(false) {
+
+ // Wipe any existing statistics
+ isc::stats::StatsMgr::instance().removeAll();
+
+ subnet_ = Subnet4::create(IOAddress("192.0.2.0"),
+ 24, 1000, 2000, 3000, SubnetID(1));
+ pool_ = Pool4Ptr(new Pool4(IOAddress("192.0.2.100"), IOAddress("192.0.2.110")));
+ subnet_->addPool(pool_);
+
+ // Add Router option.
+ Option4AddrLstPtr opt_routers(new Option4AddrLst(DHO_ROUTERS));
+ opt_routers->setAddress(IOAddress("192.0.2.2"));
+ subnet_->getCfgOption()->add(opt_routers, false, false,
+ DHCP4_OPTION_SPACE);
+
+ CfgMgr::instance().clear();
+ CfgMgr::instance().setFamily(AF_INET);
+ CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->add(subnet_);
+ CfgMgr::instance().commit();
+
+ LibDHCP::clearRuntimeOptionDefs();
+
+ // Let's wipe all existing statistics.
+ isc::stats::StatsMgr::instance().removeAll();
+
+ // Reset the thread pool.
+ MultiThreadingMgr::instance().apply(false, 0, 0);
+}
+
+Dhcpv4SrvTest::~Dhcpv4SrvTest() {
+ // Make sure that we revert to default value
+ CfgMgr::instance().clear();
+
+ LibDHCP::clearRuntimeOptionDefs();
+
+ // Let's wipe all existing statistics.
+ isc::stats::StatsMgr::instance().removeAll();
+
+ // Reset the thread pool.
+ MultiThreadingMgr::instance().apply(false, 0, 0);
+}
+
+void Dhcpv4SrvTest::addPrlOption(Pkt4Ptr& pkt) {
+
+ OptionUint8ArrayPtr option_prl =
+ OptionUint8ArrayPtr(new OptionUint8Array(Option::V4,
+ DHO_DHCP_PARAMETER_REQUEST_LIST));
+
+ // Let's request options that have been configured for the subnet.
+ option_prl->addValue(DHO_DOMAIN_NAME_SERVERS);
+ option_prl->addValue(DHO_DOMAIN_NAME);
+ option_prl->addValue(DHO_LOG_SERVERS);
+ option_prl->addValue(DHO_COOKIE_SERVERS);
+ // Let's also request the option that hasn't been configured. In such
+ // case server should ignore request for this particular option.
+ option_prl->addValue(DHO_LPR_SERVERS);
+ // And add 'Parameter Request List' option into the DISCOVER packet.
+ pkt->addOption(option_prl);
+}
+
+ConstElementPtr
+Dhcpv4SrvTest::configure(Dhcpv4Srv& server, ConstElementPtr config) {
+ ConstElementPtr const status(configureDhcp4Server(server, config));
+
+ // Simulate the application of MT config such as in ControlledDhcpvXSrv::processConfig().
+ CfgMultiThreading::apply(CfgMgr::instance().getStagingCfg()->getDHCPMultiThreading());
+
+ return status;
+}
+
+void
+Dhcpv4SrvTest::configureRequestedOptions() {
+ // dns-servers
+ Option4AddrLstPtr
+ option_dns_servers(new Option4AddrLst(DHO_DOMAIN_NAME_SERVERS));
+ option_dns_servers->addAddress(IOAddress("192.0.2.1"));
+ option_dns_servers->addAddress(IOAddress("192.0.2.100"));
+ ASSERT_NO_THROW(subnet_->getCfgOption()->add(option_dns_servers, false,
+ false, DHCP4_OPTION_SPACE));
+
+ // domain-name
+ OptionDefinition def("domain-name", DHO_DOMAIN_NAME, DHCP4_OPTION_SPACE,
+ OPT_FQDN_TYPE);
+ OptionCustomPtr option_domain_name(new OptionCustom(def, Option::V4));
+ option_domain_name->writeFqdn("example.com");
+ subnet_->getCfgOption()->add(option_domain_name, false, false,
+ DHCP4_OPTION_SPACE);
+
+ // log-servers
+ Option4AddrLstPtr option_log_servers(new Option4AddrLst(DHO_LOG_SERVERS));
+ option_log_servers->addAddress(IOAddress("192.0.2.2"));
+ option_log_servers->addAddress(IOAddress("192.0.2.10"));
+ ASSERT_NO_THROW(subnet_->getCfgOption()->add(option_log_servers, false,
+ false, DHCP4_OPTION_SPACE));
+
+ // cookie-servers
+ Option4AddrLstPtr option_cookie_servers(new Option4AddrLst(DHO_COOKIE_SERVERS));
+ option_cookie_servers->addAddress(IOAddress("192.0.2.1"));
+ ASSERT_NO_THROW(subnet_->getCfgOption()->add(option_cookie_servers, false,
+ false, DHCP4_OPTION_SPACE));
+}
+
+void
+Dhcpv4SrvTest::configureServerIdentifier() {
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+ CfgSubnets4Ptr subnets = cfg_mgr.getStagingCfg()->getCfgSubnets4();
+
+ // Build and add subnet2.
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.0"), 24, 1200, 2400, 3600, 2));
+ Pool4Ptr pool(new Pool4(IOAddress("192.0.2.100"), IOAddress("192.0.2.200")));
+ // Add server identifier to the pool.
+ OptionCustomPtr server_id = makeServerIdOption(IOAddress("192.0.2.254"));
+ CfgOptionPtr cfg_option = pool->getCfgOption();
+ cfg_option->add(server_id, false, false, DHCP4_OPTION_SPACE);
+ subnet2->addPool(pool);
+
+ // Add a second pool.
+ pool.reset(new Pool4(IOAddress("192.0.2.201"), IOAddress("192.0.2.220")));
+ subnet2->addPool(pool);
+
+ subnets->add(subnet2);
+
+ // Build and add subnet3.
+ Triplet<uint32_t> unspec;
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.3.0"), 24, unspec, unspec, 3600, 3));
+ pool.reset(new Pool4(IOAddress("192.0.3.100"), IOAddress("192.0.3.200")));
+ subnet3->addPool(pool);
+ subnet3->setT1Percent(0.5);
+ subnet3->setT2Percent(0.75);
+ subnet3->setCalculateTeeTimes(true);
+
+ // Add server identifier.
+ server_id = makeServerIdOption(IOAddress("192.0.3.254"));
+ cfg_option = subnet3->getCfgOption();
+ cfg_option->add(server_id, false, false, DHCP4_OPTION_SPACE);
+
+ subnets->add(subnet3);
+
+ // Build and add subnet4.
+ Subnet4Ptr subnet4(new Subnet4(IOAddress("192.0.4.0"), 24, unspec, unspec, 3600, 4));
+ pool.reset(new Pool4(IOAddress("192.0.4.100"), IOAddress("192.0.4.200")));
+ subnet4->addPool(pool);
+ subnet4->setCalculateTeeTimes(false);
+
+ subnets->add(subnet4);
+
+ // Build and add subnet5.
+ Subnet4Ptr subnet5(new Subnet4(IOAddress("192.0.5.0"), 24, unspec, unspec, 3600, 5));
+ pool.reset(new Pool4(IOAddress("192.0.5.100"), IOAddress("192.0.5.200")));
+ subnet5->addPool(pool);
+ subnet5->setCalculateTeeTimes(false);
+
+ subnets->add(subnet5);
+
+ CfgOptionPtr options(new CfgOption());
+ OptionDescriptor desc(false, false);
+ desc.option_ = makeServerIdOption(IOAddress("192.0.5.254"));
+ options->add(desc, DHCP4_OPTION_SPACE);
+ CfgMgr::instance().getStagingCfg()->getClientClassDictionary()->addClass("foo", ExpressionPtr(), "", true, false, options);
+ subnet5->requireClientClass("foo");
+
+ // Build and add subnet6.
+ Subnet4Ptr subnet6(new Subnet4(IOAddress("192.0.6.0"), 24, unspec, unspec, 3600, 6));
+ pool.reset(new Pool4(IOAddress("192.0.6.100"), IOAddress("192.0.6.200")));
+ subnet6->addPool(pool);
+ subnet6->setCalculateTeeTimes(false);
+
+ subnets->add(subnet6);
+
+ options.reset(new CfgOption());
+ OptionDescriptor desc_other(false, false);
+ desc_other.option_ = makeFqdnListOption();
+ options->add(desc_other, DHCP4_OPTION_SPACE);
+ CfgMgr::instance().getStagingCfg()->getClientClassDictionary()->addClass("bar", ExpressionPtr(), "", true, false, options);
+ subnet6->requireClientClass("bar");
+
+ // Build and add subnet7.
+ Subnet4Ptr subnet7(new Subnet4(IOAddress("192.0.7.0"), 24, unspec, unspec, 3600, 7));
+ pool.reset(new Pool4(IOAddress("192.0.7.100"), IOAddress("192.0.7.200")));
+ subnet7->addPool(pool);
+ subnet7->setCalculateTeeTimes(false);
+
+ subnets->add(subnet7);
+
+ options.reset();
+ CfgMgr::instance().getStagingCfg()->getClientClassDictionary()->addClass("xyz", ExpressionPtr(), "", true, false, options);
+ subnet7->requireClientClass("xyz");
+
+ // Build and add a shared-network.
+ CfgSharedNetworks4Ptr networks = cfg_mgr.getStagingCfg()->getCfgSharedNetworks4();
+ SharedNetwork4Ptr network1(new SharedNetwork4("one"));
+ network1->add(subnet4);
+
+ // Add server identifier.
+ server_id = makeServerIdOption(IOAddress("192.0.4.254"));
+ cfg_option = network1->getCfgOption();
+ cfg_option->add(server_id, false, false, DHCP4_OPTION_SPACE);
+
+ networks->add(network1);
+
+ // Add a global server identifier.
+ cfg_option = cfg_mgr.getStagingCfg()->getCfgOption();
+ server_id = makeServerIdOption(IOAddress("10.0.0.254"));
+ cfg_option->add(server_id, false, false, DHCP4_OPTION_SPACE);
+
+ // Commit the config.
+ cfg_mgr.commit();
+}
+
+void
+Dhcpv4SrvTest::messageCheck(const Pkt4Ptr& q, const Pkt4Ptr& a) {
+ ASSERT_TRUE(q);
+ ASSERT_TRUE(a);
+
+ EXPECT_EQ(q->getHops(), a->getHops());
+ EXPECT_EQ(q->getIface(), a->getIface());
+ EXPECT_EQ(q->getIndex(), a->getIndex());
+ EXPECT_EQ(q->getGiaddr(), a->getGiaddr());
+ // When processing an incoming packet the remote address
+ // is copied as a src address, and the source address is
+ // copied as a remote address to the response.
+ EXPECT_TRUE(q->getLocalHWAddr() == a->getLocalHWAddr());
+ EXPECT_TRUE(q->getRemoteHWAddr() == a->getRemoteHWAddr());
+
+ // Check that the server identifier is present in the response.
+ // Presence (or absence) of other options is checked elsewhere.
+ EXPECT_TRUE(a->getOption(DHO_DHCP_SERVER_IDENTIFIER));
+
+ // Check that something is offered
+ EXPECT_NE("0.0.0.0", a->getYiaddr().toText());
+}
+
+::testing::AssertionResult
+Dhcpv4SrvTest::basicOptionsPresent(const Pkt4Ptr& pkt) {
+ std::ostringstream errmsg;
+ errmsg << "option missing in the response";
+ if (!pkt->getOption(DHO_DOMAIN_NAME)) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "domain-name " << errmsg.str()));
+
+ } else if (!pkt->getOption(DHO_DOMAIN_NAME_SERVERS)) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "dns-servers " << errmsg.str()));
+
+ } else if (!pkt->getOption(DHO_SUBNET_MASK)) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "subnet-mask " << errmsg.str()));
+
+ } else if (!pkt->getOption(DHO_ROUTERS)) {
+ return (::testing::AssertionFailure(::testing::Message() << "routers "
+ << errmsg.str()));
+
+ } else if (!pkt->getOption(DHO_DHCP_LEASE_TIME)) {
+ return (::testing::AssertionFailure(::testing::Message() <<
+ "dhcp-lease-time " << errmsg.str()));
+
+ }
+
+ return (::testing::AssertionSuccess());
+}
+
+::testing::AssertionResult
+Dhcpv4SrvTest::noBasicOptions(const Pkt4Ptr& pkt) {
+ std::ostringstream errmsg;
+ errmsg << "option present in the response";
+ if (pkt->getOption(DHO_DOMAIN_NAME)) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "domain-name " << errmsg.str()));
+
+ } else if (pkt->getOption(DHO_DOMAIN_NAME_SERVERS)) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "dns-servers " << errmsg.str()));
+
+ } else if (pkt->getOption(DHO_SUBNET_MASK)) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "subnet-mask " << errmsg.str()));
+
+ } else if (pkt->getOption(DHO_ROUTERS)) {
+ return (::testing::AssertionFailure(::testing::Message() << "routers "
+ << errmsg.str()));
+
+ } else if (pkt->getOption(DHO_DHCP_LEASE_TIME)) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "dhcp-lease-time " << errmsg.str()));
+
+ }
+ return (::testing::AssertionSuccess());
+}
+
+::testing::AssertionResult
+Dhcpv4SrvTest::requestedOptionsPresent(const Pkt4Ptr& pkt) {
+ std::ostringstream errmsg;
+ errmsg << "option missing in the response";
+ if (!pkt->getOption(DHO_LOG_SERVERS)) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "log-servers " << errmsg.str()));
+
+ } else if (!pkt->getOption(DHO_COOKIE_SERVERS)) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "cookie-servers " << errmsg.str()));
+
+ }
+ return (::testing::AssertionSuccess());
+}
+
+::testing::AssertionResult
+Dhcpv4SrvTest::noRequestedOptions(const Pkt4Ptr& pkt) {
+ std::ostringstream errmsg;
+ errmsg << "option present in the response";
+ if (pkt->getOption(DHO_LOG_SERVERS)) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "log-servers " << errmsg.str()));
+
+ } else if (pkt->getOption(DHO_COOKIE_SERVERS)) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "cookie-servers " << errmsg.str()));
+
+ }
+ return (::testing::AssertionSuccess());
+}
+
+OptionPtr
+Dhcpv4SrvTest::generateClientId(size_t size /*= 4*/) {
+
+ OptionBuffer clnt_id(size);
+ for (size_t i = 0; i < size; i++) {
+ clnt_id[i] = 100 + i;
+ }
+
+ client_id_ = ClientIdPtr(new ClientId(clnt_id));
+
+ return (OptionPtr(new Option(Option::V4, DHO_DHCP_CLIENT_IDENTIFIER,
+ clnt_id.begin(),
+ clnt_id.begin() + size)));
+}
+
+HWAddrPtr
+Dhcpv4SrvTest::generateHWAddr(size_t size /*= 6*/) {
+ const uint8_t hw_type = 123; // Just a fake number (typically 6=HTYPE_ETHER, see dhcp4.h)
+ OptionBuffer mac(size);
+ for (size_t i = 0; i < size; ++i) {
+ mac[i] = 50 + i;
+ }
+ return (HWAddrPtr(new HWAddr(mac, hw_type)));
+}
+
+OptionCustomPtr
+Dhcpv4SrvTest::makeServerIdOption(const IOAddress& address) {
+ OptionDefinitionPtr option_def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE,
+ DHO_DHCP_SERVER_IDENTIFIER);
+ OptionCustomPtr server_id(new OptionCustom(*option_def, Option::V4));
+ server_id->writeAddress(address);
+ return (server_id);
+}
+
+OptionPtr
+Dhcpv4SrvTest::makeFqdnListOption() {
+ OptionDefinitionPtr def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE,
+ DHO_DOMAIN_SEARCH);
+
+ // Prepare buffer holding an array of FQDNs.
+ const uint8_t fqdn[] = {
+ 8, 109, 121, 100, 111, 109, 97, 105, 110, // "mydomain"
+ 7, 101, 120, 97, 109, 112, 108, 101, // "example"
+ 3, 99, 111, 109, // "com"
+ 0
+ };
+
+ // Initialize a vector with the FQDN data.
+ std::vector<uint8_t> fqdn_buf(fqdn, fqdn + sizeof(fqdn));
+
+ OptionPtr option = def->optionFactory(Option::V4, DHO_DOMAIN_SEARCH,
+ fqdn_buf.begin(), fqdn_buf.end());
+
+ return (option);
+}
+
+void
+Dhcpv4SrvTest::checkAddressParams(const Pkt4Ptr& rsp,
+ const Subnet4Ptr subnet,
+ bool t1_present,
+ bool t2_present,
+ uint32_t expected_valid) {
+
+ // Technically inPool implies inRange, but let's be on the safe
+ // side and check both.
+ EXPECT_TRUE(subnet->inRange(rsp->getYiaddr()));
+ EXPECT_TRUE(subnet->inPool(Lease::TYPE_V4, rsp->getYiaddr()));
+
+ // Check lease time
+ OptionUint32Ptr opt = boost::dynamic_pointer_cast<
+ OptionUint32>(rsp->getOption(DHO_DHCP_LEASE_TIME));
+ if (!opt) {
+ ADD_FAILURE() << "Lease time option missing in response or the"
+ " option has unexpected type";
+ } else if (subnet->getValid().getMin() != subnet->getValid().getMax()) {
+ EXPECT_GE(opt->getValue(), subnet->getValid().getMin());
+ EXPECT_LE(opt->getValue(), subnet->getValid().getMax());
+ } else {
+ EXPECT_EQ(opt->getValue(), subnet->getValid());
+ }
+
+ // Check expected value when wanted.
+ if (opt && expected_valid) {
+ EXPECT_EQ(opt->getValue(), expected_valid);
+ }
+
+ // Check T1 timer
+ opt = boost::dynamic_pointer_cast<
+ OptionUint32>(rsp->getOption(DHO_DHCP_RENEWAL_TIME));
+ if (t1_present) {
+ ASSERT_TRUE(opt) << "Required T1 option missing or it has"
+ " an unexpected type";
+ EXPECT_EQ(opt->getValue(), subnet->getT1());
+ } else {
+ EXPECT_FALSE(opt);
+ }
+
+ // Check T2 timer
+ opt = boost::dynamic_pointer_cast<
+ OptionUint32>(rsp->getOption(DHO_DHCP_REBINDING_TIME));
+ if (t2_present) {
+ ASSERT_TRUE(opt) << "Required T2 option missing or it has"
+ " an unexpected type";
+ EXPECT_EQ(opt->getValue(), subnet->getT2());
+ } else {
+ EXPECT_FALSE(opt);
+ }
+}
+
+void
+Dhcpv4SrvTest::checkResponse(const Pkt4Ptr& rsp, int expected_message_type,
+ uint32_t expected_transid) {
+ ASSERT_TRUE(rsp);
+ EXPECT_EQ(expected_message_type,
+ static_cast<int>(rsp->getType()));
+ EXPECT_EQ(expected_transid, rsp->getTransid());
+}
+
+Lease4Ptr
+Dhcpv4SrvTest::checkLease(const Pkt4Ptr& rsp, const OptionPtr& client_id,
+ const HWAddrPtr&, const IOAddress& expected_addr) {
+
+ ClientIdPtr id;
+ if (client_id) {
+ OptionBuffer data = client_id->getData();
+ id.reset(new ClientId(data));
+ }
+
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(expected_addr);
+ if (!lease) {
+ cout << "Lease for " << expected_addr
+ << " not found in the database backend.";
+ return (Lease4Ptr());
+ }
+
+ EXPECT_EQ(rsp->getYiaddr(), expected_addr);
+
+ EXPECT_EQ(expected_addr, lease->addr_);
+ if (client_id) {
+ EXPECT_TRUE(*lease->client_id_ == *id);
+ }
+ EXPECT_EQ(subnet_->getID(), lease->subnet_id_);
+
+ return (lease);
+}
+
+void
+Dhcpv4SrvTest::checkServerId(const Pkt4Ptr& rsp, const OptionPtr& expected_srvid) {
+ // Check that server included its server-id
+ OptionPtr opt = rsp->getOption(DHO_DHCP_SERVER_IDENTIFIER);
+ ASSERT_TRUE(opt);
+ EXPECT_EQ(opt->getType(), expected_srvid->getType() );
+ EXPECT_EQ(opt->len(), expected_srvid->len() );
+ EXPECT_TRUE(opt->getData() == expected_srvid->getData());
+}
+
+void
+Dhcpv4SrvTest::checkClientId(const Pkt4Ptr& rsp, const OptionPtr& expected_clientid) {
+
+ bool include_clientid =
+ CfgMgr::instance().getCurrentCfg()->getEchoClientId();
+
+ // check that server included our own client-id
+ OptionPtr opt = rsp->getOption(DHO_DHCP_CLIENT_IDENTIFIER);
+ if (include_clientid) {
+ // Normal mode: echo back (see RFC6842)
+ ASSERT_TRUE(opt);
+ EXPECT_EQ(expected_clientid->getType(), opt->getType());
+ EXPECT_EQ(expected_clientid->len(), opt->len());
+ EXPECT_TRUE(expected_clientid->getData() == opt->getData());
+ } else {
+ // Backward compatibility mode for pre-RFC6842 devices
+ ASSERT_FALSE(opt);
+ }
+}
+
+void
+Dhcpv4SrvTest::checkServerIdOption(const Pkt4Ptr& packet, const IOAddress& expected_address) {
+ OptionPtr opt = packet->getOption(DHO_DHCP_SERVER_IDENTIFIER);
+ ASSERT_TRUE(opt) << "no server-id option";
+
+ OptionCustomPtr server_id_opt = boost::dynamic_pointer_cast<OptionCustom>(opt);
+ ASSERT_TRUE(server_id_opt) << "server-id option is not an instance of OptionCustom";
+
+ EXPECT_EQ(expected_address, server_id_opt->readAddress());
+}
+
+::testing::AssertionResult
+Dhcpv4SrvTest::createPacketFromBuffer(const Pkt4Ptr& src_pkt,
+ Pkt4Ptr& dst_pkt) {
+ // Create on-wire format of the packet. If pack() has been called
+ // on this instance of the packet already, the next call to pack()
+ // should remove all contents of the output buffer.
+ try {
+ src_pkt->pack();
+ } catch (const Exception& ex) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "Failed to parse source packet: "
+ << ex.what()));
+ }
+ // Get the output buffer from the source packet.
+ const util::OutputBuffer& buf = src_pkt->getBuffer();
+ // Create a copy of the packet using the output buffer from the source
+ // packet.
+ try {
+ dst_pkt.reset(new Pkt4(static_cast<const uint8_t*>(buf.getData()),
+ buf.getLength()));
+ } catch (const Exception& ex) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "Failed to create a"
+ " destination packet from"
+ " the buffer: "
+ << ex.what()));
+ }
+
+ // The dst_pkt unpack is performed on processPacket by the server.
+
+ return (::testing::AssertionSuccess());
+}
+
+void
+// cppcheck-suppress unusedFunction
+Dhcpv4SrvTest::TearDown() {
+
+ CfgMgr::instance().clear();
+
+ // Close all open sockets.
+ IfaceMgr::instance().closeSockets();
+
+ // Some unit tests override the default packet filtering class, used
+ // by the IfaceMgr. The dummy class, called PktFilterTest, reports the
+ // capability to directly respond to the clients without IP address
+ // assigned. This capability is not supported by the default packet
+ // filtering class: PktFilterInet. Therefore setting the dummy class
+ // allows to test scenarios, when server responds to the broadcast address
+ // on client's request, despite having support for direct response.
+ // The following call restores the use of original packet filtering class
+ // after the test.
+ try {
+ IfaceMgr::instance().setPacketFilter(PktFilterPtr(new PktFilterInet()));
+
+ } catch (const Exception& ex) {
+ FAIL() << "Failed to restore the default (PktFilterInet) packet filtering"
+ << " class after the test. Exception has been caught: "
+ << ex.what();
+ }
+}
+
+void
+Dhcpv4SrvTest::testDiscoverRequest(const uint8_t msg_type) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Create an instance of the tested class.
+ boost::scoped_ptr<NakedDhcpv4Srv> srv(new NakedDhcpv4Srv(0));
+
+ // Initialize the source HW address.
+ vector<uint8_t> mac(6);
+ for (uint8_t i = 0; i < 6; ++i) {
+ mac[i] = i * 10;
+ }
+ // Initialized the destination HW address.
+ vector<uint8_t> dst_mac(6);
+ for (uint8_t i = 0; i < 6; ++i) {
+ dst_mac[i] = i * 20;
+ }
+ // Create a DHCP message. It will be used to simulate the
+ // incoming message.
+ boost::shared_ptr<Pkt4> req(new Pkt4(msg_type, 1234));
+ // Create a response message. It will hold a response packet.
+ // Initially, set it to NULL.
+ boost::shared_ptr<Pkt4> rsp;
+ // Set the name of the interface on which packet is received.
+ req->setIface("eth0");
+ // Set the interface index.
+ req->setIndex(ETH0_INDEX);
+ // Set the target HW address. This value is normally used to
+ // construct the data link layer header.
+ req->setRemoteHWAddr(1, 6, dst_mac);
+ // Set the HW address. This value is set on DHCP level (in chaddr).
+ req->setHWAddr(1, 6, mac);
+ // Set local HW address. It is used to construct the data link layer
+ // header.
+ req->setLocalHWAddr(1, 6, mac);
+ // Set target IP address.
+ req->setRemoteAddr(IOAddress("192.0.2.55"));
+ // Set relay address and hops.
+ req->setGiaddr(IOAddress("192.0.2.10"));
+ req->setHops(1);
+
+ // We are going to test that certain options are returned
+ // in the response message when requested using 'Parameter
+ // Request List' option. Let's configure those options that
+ // are returned when requested.
+ configureRequestedOptions();
+
+ // Create a copy of the original packet by parsing its wire format.
+ // This simulates the real life scenario when we process the packet
+ // which was parsed from its wire format.
+ Pkt4Ptr received;
+ ASSERT_TRUE(createPacketFromBuffer(req, received));
+ received->unpack();
+ // Set interface. It is required for the server to generate server id.
+ received->setIface("eth0");
+ received->setIndex(ETH0_INDEX);
+ if (msg_type == DHCPDISCOVER) {
+ ASSERT_NO_THROW(rsp = srv->processDiscover(received));
+
+ // Should return OFFER
+ ASSERT_TRUE(rsp);
+ EXPECT_EQ(DHCPOFFER, rsp->getType());
+
+ } else {
+ ASSERT_NO_THROW(rsp = srv->processRequest(received));
+
+ // Should return ACK
+ ASSERT_TRUE(rsp);
+ EXPECT_EQ(DHCPACK, rsp->getType());
+
+ }
+
+ messageCheck(received, rsp);
+
+ // Basic options should be present when we got the lease.
+ EXPECT_TRUE(basicOptionsPresent(rsp));
+ // We did not request any options so these should not be present
+ // in the RSP.
+ EXPECT_TRUE(noRequestedOptions(rsp));
+
+ // Repeat the test but request some options.
+ // Add 'Parameter Request List' option.
+ addPrlOption(req);
+
+ ASSERT_TRUE(createPacketFromBuffer(req, received));
+ received->unpack();
+ ASSERT_TRUE(received->getOption(DHO_DHCP_PARAMETER_REQUEST_LIST));
+
+ // Set interface. It is required for the server to generate server id.
+ received->setIface("eth0");
+ received->setIndex(ETH0_INDEX);
+
+ if (msg_type == DHCPDISCOVER) {
+ ASSERT_NO_THROW(rsp = srv->processDiscover(received));
+
+ // Should return non-NULL packet.
+ ASSERT_TRUE(rsp);
+ EXPECT_EQ(DHCPOFFER, rsp->getType());
+
+ } else {
+ ASSERT_NO_THROW(rsp = srv->processRequest(received));
+
+ // Should return non-NULL packet.
+ ASSERT_TRUE(rsp);
+ EXPECT_EQ(DHCPACK, rsp->getType());
+ }
+
+ // Check that the requested options are returned.
+ EXPECT_TRUE(basicOptionsPresent(rsp));
+ EXPECT_TRUE(requestedOptionsPresent(rsp));
+
+ // The following part of the test will test that the NAK is sent when
+ // there is no address pool configured. In the same time, we expect
+ // that the requested options are not included in NAK message, but that
+ // they are only included when yiaddr is set to non-zero value.
+ ASSERT_NO_THROW(subnet_->delPools(Lease::TYPE_V4));
+
+ // There has been a lease allocated for the particular client. So,
+ // even though we deleted the subnet, the client would get the
+ // existing lease (not a NAK). Therefore, we have to change the chaddr
+ // in the packet so as the existing lease is not returned.
+ req->setHWAddr(1, 6, std::vector<uint8_t>(2, 6));
+ ASSERT_TRUE(createPacketFromBuffer(req, received));
+ received->unpack();
+ ASSERT_TRUE(received->getOption(DHO_DHCP_PARAMETER_REQUEST_LIST));
+
+ // Set interface. It is required for the server to generate server id.
+ received->setIface("eth0");
+ received->setIndex(ETH0_INDEX);
+
+ if (msg_type == DHCPDISCOVER) {
+ ASSERT_NO_THROW(rsp = srv->processDiscover(received));
+ // Should return NULL packet.
+ ASSERT_FALSE(rsp);
+
+ } else {
+ ASSERT_NO_THROW(rsp = srv->processRequest(received));
+ // Should return non-NULL packet.
+ ASSERT_TRUE(rsp);
+ // We should get the NAK packet with yiaddr set to 0.
+ EXPECT_EQ(DHCPNAK, rsp->getType());
+ ASSERT_EQ("0.0.0.0", rsp->getYiaddr().toText());
+
+ // Make sure that none of the requested options is returned in NAK.
+ // Also options such as Routers or Subnet Mask should not be there,
+ // because lease hasn't been acquired.
+ EXPECT_TRUE(noRequestedOptions(rsp));
+ EXPECT_TRUE(noBasicOptions(rsp));
+ }
+}
+
+void
+Dhcpv4SrvTest::buildCfgOptionTest(IOAddress expected_server_id,
+ Pkt4Ptr& query,
+ IOAddress requested,
+ IOAddress server_id) {
+ OptionDefinitionPtr req_addr_def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE,
+ DHO_DHCP_REQUESTED_ADDRESS);
+ ASSERT_TRUE(req_addr_def);
+
+ OptionDefinitionPtr sbnsel_def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE,
+ DHO_SUBNET_SELECTION);
+ ASSERT_TRUE(sbnsel_def);
+
+ OptionCustomPtr req_addr(new OptionCustom(*req_addr_def, Option::V4));
+ req_addr->writeAddress(requested);
+
+ OptionCustomPtr sbnsel(new OptionCustom(*sbnsel_def, Option::V4));
+ sbnsel->writeAddress(requested);
+
+ query->addOption(req_addr);
+ query->addOption(sbnsel);
+ query->addOption(makeServerIdOption(server_id));
+
+ Pkt4Ptr response;
+ ASSERT_NO_THROW(response = srv_.processRequest(query));
+
+ checkServerIdOption(response, expected_server_id);
+
+ ASSERT_NO_THROW(query->delOption(DHO_DHCP_REQUESTED_ADDRESS));
+ ASSERT_NO_THROW(query->delOption(DHO_SUBNET_SELECTION));
+ ASSERT_NO_THROW(query->delOption(DHO_DHCP_SERVER_IDENTIFIER));
+}
+
+void
+Dhcpv4SrvTest::configure(const std::string& config,
+ const bool commit,
+ const bool open_sockets,
+ const bool create_managers,
+ const bool test,
+ const LeaseAffinity lease_affinity) {
+ configure(config, srv_, commit, open_sockets, create_managers, test,
+ lease_affinity);
+}
+
+void
+Dhcpv4SrvTest::configure(const std::string& config,
+ NakedDhcpv4Srv& srv,
+ const bool commit,
+ const bool open_sockets,
+ const bool create_managers,
+ const bool test,
+ const LeaseAffinity lease_affinity) {
+ setenv("KEA_LFC_EXECUTABLE", KEA_LFC_EXECUTABLE, 1);
+ MultiThreadingCriticalSection cs;
+ ConstElementPtr json;
+ try {
+ json = parseJSON(config);
+ } catch (const std::exception& ex){
+ // Fatal failure on parsing error
+ FAIL() << "parsing failure:"
+ << "config:" << config << std::endl
+ << "error: " << ex.what();
+ }
+
+ ConstElementPtr status;
+
+ // Disable the re-detect flag
+ disableIfacesReDetect(json);
+
+ // Set up multi-threading
+ configureMultiThreading(multi_threading_, json);
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = configureDhcp4Server(srv, json, test));
+ ASSERT_TRUE(status);
+ int rcode;
+ ConstElementPtr comment = config::parseAnswerText(rcode, status);
+ ASSERT_EQ(0, rcode) << "configuration failed, test is broken: "
+ << comment->str();
+
+ // Use specified lease database backend.
+ if (create_managers) {
+ ASSERT_NO_THROW( {
+ CfgDbAccessPtr cfg_db = CfgMgr::instance().getStagingCfg()->getCfgDbAccess();
+ cfg_db->setAppendedParameters("universe=4");
+ cfg_db->createManagers();
+ } );
+ }
+
+ if (lease_affinity == LEASE_AFFINITY_DISABLED) {
+ auto expiration_cfg = CfgMgr::instance().getStagingCfg()->getCfgExpiration();
+ expiration_cfg->setFlushReclaimedTimerWaitTime(0);
+ expiration_cfg->setHoldReclaimedTime(0);
+ }
+
+ try {
+ CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->initAllocatorsAfterConfigure();
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "Error initializing the allocators after configure: "
+ << ex.what();
+ }
+
+ try {
+ CfgMultiThreading::apply(CfgMgr::instance().getStagingCfg()->getDHCPMultiThreading());
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "Error applying multi threading settings: "
+ << ex.what();
+ }
+
+ if (commit) {
+ CfgMgr::instance().commit();
+ }
+
+ // Opening sockets.
+ if (open_sockets) {
+ IfaceMgr::instance().openSockets4();
+ }
+}
+
+std::pair<int, std::string>
+Dhcpv4SrvTest::configureWithStatus(const std::string& config, NakedDhcpv4Srv& srv,
+ const bool commit, const int exp_rcode) {
+ ConstElementPtr json;
+ try {
+ json = parseJSON(config);
+ } catch (const std::exception& ex){
+ // Fatal failure on parsing error
+
+ std::stringstream tmp;
+ tmp << "parsing failure:"
+ << "config:" << config << std::endl
+ << "error: " << ex.what();
+ ADD_FAILURE() << tmp.str();
+ return (std::make_pair(-1, tmp.str()));
+ }
+
+ ConstElementPtr status;
+
+ // Disable the re-detect flag
+ disableIfacesReDetect(json);
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = configureDhcp4Server(srv, json));
+ EXPECT_TRUE(status);
+ if (!status) {
+ return (make_pair(-1, "configureDhcp4Server didn't return anything"));
+ }
+
+ int rcode;
+ ConstElementPtr comment = config::parseAnswerText(rcode, status);
+ EXPECT_EQ(exp_rcode, rcode) << comment->stringValue();
+
+ // Use specified lease database backend.
+ if (rcode == 0) {
+ EXPECT_NO_THROW( {
+ CfgDbAccessPtr cfg_db = CfgMgr::instance().getStagingCfg()->getCfgDbAccess();
+ cfg_db->setAppendedParameters("universe=4");
+ cfg_db->createManagers();
+ } );
+ if (commit) {
+ CfgMgr::instance().commit();
+ }
+ }
+
+ return (std::make_pair(rcode, comment->stringValue()));
+}
+
+Dhcpv4Exchange
+Dhcpv4SrvTest::createExchange(const Pkt4Ptr& query) {
+ bool drop = false;
+ Subnet4Ptr subnet = srv_.selectSubnet(query, drop);
+ EXPECT_FALSE(drop);
+ AllocEngine::ClientContext4Ptr context(new AllocEngine::ClientContext4());
+ EXPECT_TRUE(srv_.earlyGHRLookup(query, context));
+ Dhcpv4Exchange ex(srv_.alloc_engine_, query, context, subnet, drop);
+ EXPECT_FALSE(context);
+ EXPECT_FALSE(drop);
+ return (ex);
+}
+
+void
+Dhcpv4SrvTest::pretendReceivingPkt(NakedDhcpv4Srv& srv, const std::string& config,
+ uint8_t pkt_type, const std::string& stat_name) {
+
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Apply the configuration we just received.
+ configure(config);
+
+ // Let's just use one of the actual captured packets that we have.
+ Pkt4Ptr pkt = PktCaptures::captureRelayedDiscover();
+
+ // We just need to tweak it a it, to pretend that its type is as desired.
+ // Note that when receiving a packet, its on-wire form is stored in data_
+ // field. Most methods (including setType()) operates on option objects
+ // (objects stored in options_ after unpack() is called). Finally, outgoing
+ // packets are stored in out_buffer_. So we need to go through the full
+ // unpack/tweak/pack cycle and do repack, i.e. move the output buffer back
+ // to incoming buffer.
+ pkt->unpack();
+ pkt->setType(pkt_type); // Set message type.
+ pkt->pack();
+ pkt->data_.resize(pkt->getBuffer().getLength());
+ // Copy out_buffer_ to data_ to pretend that it's what was just received.
+ memcpy(&pkt->data_[0], pkt->getBuffer().getData(), pkt->getBuffer().getLength());
+ // Clear options so that they can be recreated on unpack.
+ pkt->options_.clear();
+
+ // Simulate that we have received that traffic
+ srv.fakeReceive(pkt);
+ srv.run();
+
+ using namespace isc::stats;
+ StatsMgr& mgr = StatsMgr::instance();
+ ObservationPtr pkt4_rcvd = mgr.getObservation("pkt4-received");
+ ObservationPtr tested_stat = mgr.getObservation(stat_name);
+
+ // All expected statistics must be present.
+ ASSERT_TRUE(pkt4_rcvd);
+ ASSERT_TRUE(tested_stat);
+
+ // They also must have expected values.
+ EXPECT_EQ(1, pkt4_rcvd->getInteger().first);
+ EXPECT_EQ(1, tested_stat->getInteger().first);
+}
+
+} // end of isc::dhcp::test namespace
+} // end of isc::dhcp namespace
+} // end of isc namespace
diff --git a/src/bin/dhcp4/tests/dhcp4_test_utils.h b/src/bin/dhcp4/tests/dhcp4_test_utils.h
new file mode 100644
index 0000000..fa4cf7e
--- /dev/null
+++ b/src/bin/dhcp4/tests/dhcp4_test_utils.h
@@ -0,0 +1,799 @@
+// Copyright (C) 2013-2023 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+/// @file dhcp4_test_utils.h
+///
+/// @brief This file contains utility classes used for DHCPv4 server testing
+
+#ifndef DHCP4_TEST_UTILS_H
+#define DHCP4_TEST_UTILS_H
+
+#include <gtest/gtest.h>
+#include <dhcp/iface_mgr.h>
+#include <dhcp/option4_addrlst.h>
+#include <dhcp/pkt4.h>
+#include <dhcp/pkt_filter.h>
+#include <dhcp/pkt_filter_inet.h>
+#include <dhcpsrv/alloc_engine.h>
+#include <dhcpsrv/subnet.h>
+#include <dhcpsrv/lease.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <dhcp4/dhcp4_srv.h>
+#include <dhcp4/parser_context.h>
+#include <asiolink/io_address.h>
+#include <cc/command_interpreter.h>
+#include <util/multi_threading_mgr.h>
+#include <list>
+
+#include <boost/shared_ptr.hpp>
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @brief Dummy Packet Filtering class.
+///
+/// This class reports capability to respond directly to the client which
+/// doesn't have address configured yet.
+///
+/// All packet and socket handling functions do nothing because they are not
+/// used in unit tests.
+class PktFilterTest : public PktFilter {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Sets the 'direct response' capability to true.
+ PktFilterTest()
+ : direct_resp_supported_(true) {
+ }
+
+ /// @brief Reports 'direct response' capability.
+ ///
+ /// @return always true.
+ virtual bool isDirectResponseSupported() const {
+ return (direct_resp_supported_);
+ }
+
+ /// Does nothing.
+ virtual SocketInfo openSocket(Iface&,
+ const isc::asiolink::IOAddress& addr,
+ const uint16_t port, const bool, const bool) {
+ return (SocketInfo(addr, port, 0));
+ }
+
+ /// Does nothing.
+ virtual Pkt4Ptr receive(Iface&, const SocketInfo&) {
+ return Pkt4Ptr();
+ }
+
+ /// Does nothing.
+ virtual int send(const Iface&, uint16_t, const Pkt4Ptr&) {
+ return (0);
+ }
+
+ /// @brief Holds a boolean value which indicates whether direct response
+ /// capability is supported (true) or not (false).
+ bool direct_resp_supported_;
+
+};
+
+typedef boost::shared_ptr<PktFilterTest> PktFilterTestPtr;
+
+/// Forward definition for Dhcp4Client defined in dhcp4_client.h
+/// dhcp4_client.h includes dhcp_test_utils.h (this file), so to avoid
+/// circular dependencies, we need a forward class declaration.
+class Dhcp4Client;
+
+/// @brief "Naked" DHCPv4 server, exposes internal fields
+class NakedDhcpv4Srv: public Dhcpv4Srv {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// This constructor disables default modes of operation used by the
+ /// Dhcpv4Srv class:
+ /// - Send/receive broadcast messages through sockets on interfaces
+ /// which support broadcast traffic.
+ /// - Direct DHCPv4 traffic - communication with clients which do not
+ /// have IP address assigned yet.
+ ///
+ /// Enabling these modes requires root privileges so they must be
+ /// disabled for unit testing.
+ ///
+ /// Note, that disabling broadcast options on sockets does not impact
+ /// the operation of these tests because they use local loopback
+ /// interface which doesn't have broadcast capability anyway. It rather
+ /// prevents setting broadcast options on other (broadcast capable)
+ /// sockets which are opened on other interfaces in Dhcpv4Srv constructor.
+ ///
+ /// The Direct DHCPv4 Traffic capability can be disabled here because
+ /// it is tested with PktFilterLPFTest unittest. The tests which belong
+ /// to PktFilterLPFTest can be enabled on demand when root privileges can
+ /// be guaranteed.
+ ///
+ /// @param port port number to listen on; the default value 0 indicates
+ /// that sockets should not be opened.
+ NakedDhcpv4Srv(uint16_t port = 0)
+ : Dhcpv4Srv(port, false, false) {
+ // Create a default lease database backend.
+ std::string dbconfig = "type=memfile universe=4 persist=false";
+ isc::dhcp::LeaseMgrFactory::create(dbconfig);
+ // Create fixed server id.
+ server_id_.reset(new Option4AddrLst(DHO_DHCP_SERVER_IDENTIFIER,
+ asiolink::IOAddress("192.0.3.1")));
+ LeaseMgr::setIOService(getIOService());
+ }
+
+ /// @brief Returns fixed server identifier assigned to the naked server
+ /// instance.
+ OptionPtr getServerID() const {
+ return (server_id_);
+ }
+
+ /// @brief fakes packet reception
+ /// @param timeout ignored
+ ///
+ /// The method receives all packets queued in receive queue, one after
+ /// another. Once the queue is empty, it initiates the shutdown procedure.
+ ///
+ /// See fake_received_ field for description
+ virtual Pkt4Ptr receivePacket(int /*timeout*/) {
+ // If there is anything prepared as fake incoming traffic, use it
+ if (!fake_received_.empty()) {
+ Pkt4Ptr pkt = fake_received_.front();
+ fake_received_.pop_front();
+ return (pkt);
+ }
+
+ // Make sure the server processed all packets in MT.
+ isc::util::MultiThreadingMgr::instance().getThreadPool().wait(3);
+
+ // If not, just trigger shutdown and return immediately
+ shutdown();
+ return (Pkt4Ptr());
+ }
+
+ /// @brief fake packet sending
+ ///
+ /// Pretend to send a packet, but instead just store it in fake_send_ list
+ /// where test can later inspect server's response.
+ virtual void sendPacket(const Pkt4Ptr& pkt) {
+ isc::util::MultiThreadingLock lock(mutex_);
+ fake_sent_.push_back(pkt);
+ }
+
+ /// @brief fake receive packet from server
+ ///
+ /// The client uses this packet as a reply from the server.
+ ///
+ /// @return The received packet.
+ Pkt4Ptr receiveOneMsg() {
+ // Make sure the server processed all packets.
+ isc::util::MultiThreadingMgr::instance().getThreadPool().wait(2);
+
+ // Lock the mutex for the rest of the function.
+ isc::util::MultiThreadingLock lock(mutex_);
+
+ // Return empty pointer if server hasn't responded.
+ if (fake_sent_.empty()) {
+ return (Pkt4Ptr());
+ }
+
+ Pkt4Ptr msg = fake_sent_.front();
+ fake_sent_.pop_front();
+ return (msg);
+ }
+
+ /// @brief adds a packet to fake receive queue
+ ///
+ /// See fake_received_ field for description
+ void fakeReceive(const Pkt4Ptr& pkt) {
+ fake_received_.push_back(pkt);
+ }
+
+ virtual ~NakedDhcpv4Srv() {
+ }
+
+ /// @brief Runs processing DHCPDISCOVER.
+ ///
+ /// @param discover a message received from client
+ /// @return DHCPOFFER message or null
+ Pkt4Ptr processDiscover(Pkt4Ptr& discover) {
+ AllocEngine::ClientContext4Ptr context(new AllocEngine::ClientContext4());
+ earlyGHRLookup(discover, context);
+ return (processDiscover(discover, context));
+ }
+
+ /// @brief Runs processing DHCPREQUEST.
+ ///
+ /// @param request a message received from client
+ /// @return DHCPACK or DHCPNAK message
+ Pkt4Ptr processRequest(Pkt4Ptr& request) {
+ AllocEngine::ClientContext4Ptr context(new AllocEngine::ClientContext4());
+ earlyGHRLookup(request, context);
+ return (processRequest(request, context));
+ }
+
+ /// @brief Runs processing DHCPRELEASE.
+ ///
+ /// @param release message received from client
+ void processRelease(Pkt4Ptr& release) {
+ AllocEngine::ClientContext4Ptr context(new AllocEngine::ClientContext4());
+ earlyGHRLookup(release, context);
+ processRelease(release, context);
+ }
+
+ /// @brief Runs processing DHCPDECLINE.
+ ///
+ /// @param decline message received from client
+ void processDecline(Pkt4Ptr& decline) {
+ AllocEngine::ClientContext4Ptr context(new AllocEngine::ClientContext4());
+ earlyGHRLookup(decline, context);
+ processDecline(decline, context);
+ }
+
+ /// @brief Runs processing DHCPINFORM.
+ ///
+ /// @param inform a message received from client
+ /// @return DHCPACK message
+ Pkt4Ptr processInform(Pkt4Ptr& inform) {
+ AllocEngine::ClientContext4Ptr context(new AllocEngine::ClientContext4());
+ earlyGHRLookup(inform, context);
+ return (processInform(inform, context));
+ }
+
+ /// @brief Dummy server identifier option used by various tests.
+ OptionPtr server_id_;
+
+ /// @brief packets we pretend to receive.
+ ///
+ /// Instead of setting up sockets on interfaces that change between OSes, it
+ /// is much easier to fake packet reception. This is a list of packets that
+ /// we pretend to have received. You can schedule new packets to be received
+ /// using fakeReceive() and NakedDhcpv4Srv::receivePacket() methods.
+ std::list<Pkt4Ptr> fake_received_;
+
+ /// @brief packets we pretend to send.
+ std::list<Pkt4Ptr> fake_sent_;
+
+ using Dhcpv4Srv::adjustIfaceData;
+ using Dhcpv4Srv::appendServerID;
+ using Dhcpv4Srv::processDiscover;
+ using Dhcpv4Srv::processRequest;
+ using Dhcpv4Srv::processRelease;
+ using Dhcpv4Srv::processDecline;
+ using Dhcpv4Srv::processInform;
+ using Dhcpv4Srv::processClientName;
+ using Dhcpv4Srv::createNameChangeRequests;
+ using Dhcpv4Srv::acceptServerId;
+ using Dhcpv4Srv::sanityCheck;
+ using Dhcpv4Srv::srvidToString;
+ using Dhcpv4Srv::classifyPacket;
+ using Dhcpv4Srv::deferredUnpack;
+ using Dhcpv4Srv::accept;
+ using Dhcpv4Srv::acceptMessageType;
+ using Dhcpv4Srv::selectSubnet;
+ using Dhcpv4Srv::setSendResponsesToSource;
+ using Dhcpv4Srv::VENDOR_CLASS_PREFIX;
+ using Dhcpv4Srv::shutdown_;
+ using Dhcpv4Srv::alloc_engine_;
+ using Dhcpv4Srv::server_port_;
+ using Dhcpv4Srv::client_port_;
+
+ /// @brief Mutex to protect the packet buffers.
+ std::mutex mutex_;
+};
+
+// We need to pass one reference to the Dhcp4Client, which is defined in
+// dhcp4_client.h. That header includes this file. To avoid circular
+// dependencies, we use forward declaration here.
+class Dhcp4Client;
+
+/// @brief Base class for DHCPv4 server testing.
+///
+/// Currently it configures the test data path directory in
+/// the @c CfgMgr. When the object is destroyed, the original
+/// path is reverted.
+class BaseServerTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor.
+ BaseServerTest();
+
+ /// @brief Destructor.
+ virtual ~BaseServerTest();
+
+private:
+
+ /// @brief Holds the original data directory.
+ std::string original_datadir_;
+
+};
+
+class Dhcpv4SrvTest : public BaseServerTest {
+public:
+ /// @brief Specifies expected outcome
+ enum ExpectedResult {
+ SHOULD_PASS, // pass = accept decline, move lease to declined state.
+ SHOULD_FAIL // fail = reject the decline
+ };
+
+ /// @brief Specifies if lease affinity is enabled or disabled
+ enum LeaseAffinity {
+ LEASE_AFFINITY_ENABLED,
+ LEASE_AFFINITY_DISABLED
+ };
+
+ class Dhcpv4SrvMTTestGuard {
+ public:
+ Dhcpv4SrvMTTestGuard(Dhcpv4SrvTest& test, bool mt_enabled) : test_(test) {
+ test_.setMultiThreading(mt_enabled);
+ }
+ ~Dhcpv4SrvMTTestGuard() {
+ test_.setMultiThreading(false);
+ }
+ Dhcpv4SrvTest& test_;
+ };
+
+ /// @brief Constructor
+ ///
+ /// Initializes common objects used in many tests.
+ /// Also sets up initial configuration in CfgMgr.
+ Dhcpv4SrvTest();
+
+ /// @brief Destructor
+ ///
+ /// Removes existing configuration.
+ virtual ~Dhcpv4SrvTest();
+
+ /// @brief Add 'Parameter Request List' option to the packet.
+ ///
+ /// This function adds PRL option comprising the following option codes:
+ /// - 5 - Name Server
+ /// - 15 - Domain Name
+ /// - 7 - Log Server
+ /// - 8 - Quotes Server
+ /// - 9 - LPR Server
+ ///
+ /// @param pkt packet to add PRL option to.
+ void addPrlOption(Pkt4Ptr& pkt);
+
+ /// @brief Used to configure a server for tests.
+ ///
+ /// A wrapper over configureDhcp4Server() to which any other
+ /// simulations of production code are added.
+ ///
+ /// @brief server the server being tested
+ /// @brief config the configuration the server is configured with
+ ///
+ /// @return a JSON-formatted status of the reconfiguration
+ static ConstElementPtr configure(Dhcpv4Srv& server, isc::data::ConstElementPtr config);
+
+ /// @brief Configures options being requested in the PRL option.
+ ///
+ /// The lpr-servers option is NOT configured here although it is
+ /// added to the 'Parameter Request List' option in the
+ /// \ref addPrlOption. When requested option is not configured
+ /// the server should not return it in its response. The goal
+ /// of not configuring the requested option is to verify that
+ /// the server will not return it.
+ void configureRequestedOptions();
+
+ /// @brief Configures server identifier at different levels.
+ void configureServerIdentifier();
+
+ /// @brief checks that the response matches request
+ ///
+ /// @param q query (client's message)
+ /// @param a answer (server's message)
+ void messageCheck(const Pkt4Ptr& q, const Pkt4Ptr& a);
+
+ /// @brief Check that certain basic (always added when lease is acquired)
+ /// are present in a message.
+ ///
+ /// @param pkt A message to be checked.
+ /// @return Assertion result which indicates whether test passed or failed.
+ ::testing::AssertionResult basicOptionsPresent(const Pkt4Ptr& pkt);
+
+ /// @brief Check that certain basic (always added when lease is acquired)
+ /// are not present.
+ ///
+ /// @param pkt A packet to be checked.
+ /// @return Assertion result which indicates whether test passed or failed.
+ ::testing::AssertionResult noBasicOptions(const Pkt4Ptr& pkt);
+
+ /// @brief Check that certain requested options are present in the message.
+ ///
+ /// @param pkt A message to be checked.
+ /// @return Assertion result which indicates whether test passed or failed.
+ ::testing::AssertionResult requestedOptionsPresent(const Pkt4Ptr& pkt);
+
+ /// @brief Check that certain options (requested with PRL option)
+ /// are not present.
+ ///
+ /// @param pkt A packet to be checked.
+ /// @return Assertion result which indicates whether test passed or failed.
+ ::testing::AssertionResult noRequestedOptions(const Pkt4Ptr& pkt);
+
+ /// @brief generates client-id option
+ ///
+ /// Generate client-id option of specified length
+ /// Ids with different lengths are sufficient to generate
+ /// unique ids. If more fine grained control is required,
+ /// tests generate client-ids on their own.
+ /// Sets client_id_ field.
+ /// @param size size of the client-id to be generated
+ OptionPtr generateClientId(size_t size = 4);
+
+ /// @brief generate hardware address
+ ///
+ /// @param size size of the generated MAC address
+ /// @param pointer to Hardware Address object
+ HWAddrPtr generateHWAddr(size_t size = 6);
+
+ /// @brief Convenience method for making a server identifier option instance.
+ ///
+ /// @param address IP address to add to the option
+ ///
+ /// @return Pointer to the newly constructed option.
+ OptionCustomPtr makeServerIdOption(const isc::asiolink::IOAddress& address);
+
+ /// @brief Convenience method for making a fqdn list option instance.
+ ///
+ /// @return Pointer to the newly constructed option.
+ OptionPtr makeFqdnListOption();
+
+ /// Check that address was returned from proper range, that its lease
+ /// lifetime is correct, that T1 and T2 are returned properly
+ /// @param rsp response to be checked
+ /// @param subnet subnet that should be used to verify assigned address
+ /// and options
+ /// @param t1_present check that t1 must be present (true) or must not be
+ /// present (false)
+ /// @param t2_present check that t2 must be present (true) or must not be
+ /// present (false)
+ /// @param expected_valid check that lease lifetime has the not-zero
+ /// expected value (zero value means that do not check).
+ void checkAddressParams(const Pkt4Ptr& rsp, const Subnet4Ptr subnet,
+ bool t1_present = false,
+ bool t2_present = false,
+ uint32_t expected_valid = 0);
+
+ /// @brief Basic checks for generated response (message type and trans-id).
+ ///
+ /// @param rsp response packet to be validated
+ /// @param expected_message_type expected message type
+ /// @param expected_transid expected transaction-id
+ void checkResponse(const Pkt4Ptr& rsp, int expected_message_type,
+ uint32_t expected_transid);
+
+ /// @brief Checks if the lease sent to client is present in the database
+ ///
+ /// @param rsp response packet to be validated
+ /// @param client_id expected client-identifier (or NULL)
+ /// @param HWAddr expected hardware address (not used now)
+ /// @param expected_addr expected address
+ Lease4Ptr checkLease(const Pkt4Ptr& rsp, const OptionPtr& client_id,
+ const HWAddrPtr&,
+ const isc::asiolink::IOAddress& expected_addr);
+
+ /// @brief Checks if server response (OFFER, ACK, NAK) includes proper server-id
+ ///
+ /// @param rsp response packet to be validated
+ /// @param expected_srvid expected value of server-id
+ void checkServerId(const Pkt4Ptr& rsp, const OptionPtr& expected_srvid);
+
+ /// @brief Checks if server response (OFFER, ACK, NAK) includes proper client-id
+ ///
+ /// This method follows values reported by CfgMgr in echoClientId() method.
+ /// Depending on its configuration, the client-id is either mandatory or
+ /// forbidden to appear in the response.
+ ///
+ /// @param rsp response packet to be validated
+ /// @param expected_clientid expected value of client-id
+ void checkClientId(const Pkt4Ptr& rsp, const OptionPtr& expected_clientid);
+
+ /// @brief Checks the value of the dhcp-server-identifier option in a packet
+ ///
+ /// @param packet packet to test
+ /// @param expected_address IP address the packet's option should contain
+ void checkServerIdOption(const Pkt4Ptr& packet, const isc::asiolink::IOAddress& expected_address);
+
+ /// @brief Create packet from output buffer of another packet.
+ ///
+ /// This function creates a packet using an output buffer from another
+ /// packet. This imitates reception of a packet from the wire. The
+ /// unpack function is then called to parse packet contents and to
+ /// create a collection of the options carried by this packet.
+ ///
+ /// This function is useful for unit tests which verify that the received
+ /// packet is parsed correctly. In those cases it is usually inappropriate
+ /// to create an instance of the packet, add options, set packet
+ /// fields and use such packet as an input to processDiscover or
+ /// processRequest function. This is because, such a packet has certain
+ /// options already initialized and there is no way to verify that they
+ /// have been initialized when packet instance was created or wire buffer
+ /// processing. By creating a packet from the buffer we guarantee that the
+ /// new packet is entirely initialized during wire data parsing.
+ ///
+ /// @param src_pkt A source packet, to be copied.
+ /// @param [out] dst_pkt A destination packet.
+ ///
+ /// @return assertion result indicating if a function completed with
+ /// success or failure.
+ static ::testing::AssertionResult
+ createPacketFromBuffer(const isc::dhcp::Pkt4Ptr& src_pkt,
+ isc::dhcp::Pkt4Ptr& dst_pkt);
+
+ /// @brief Tests if Discover or Request message is processed correctly
+ ///
+ /// This test verifies that the Parameter Request List option is handled
+ /// correctly, i.e. it checks that certain options are present in the
+ /// server's response when they are requested and that they are not present
+ /// when they are not requested or NAK occurs.
+ ///
+ /// @todo We need an additional test for PRL option using real traffic
+ /// capture.
+ ///
+ /// @param msg_type DHCPDISCOVER or DHCPREQUEST
+ void testDiscoverRequest(const uint8_t msg_type);
+
+ /// @brief Create test which verifies server identifier.
+ ///
+ /// @param expected_server_id expected server identifier
+ /// @param query the query used to get associated client classes
+ /// @param requested the requested address
+ /// @param server_id server identifier
+ void buildCfgOptionTest(isc::asiolink::IOAddress expected_server_id,
+ Pkt4Ptr& query,
+ isc::asiolink::IOAddress requested,
+ isc::asiolink::IOAddress server_id);
+
+ /// @brief Runs DHCPv4 configuration from the JSON string.
+ ///
+ /// @param config String holding server configuration in JSON format.
+ /// @param commit A boolean flag indicating if the new configuration
+ /// should be committed (if true), or not (if false).
+ /// @param open_sockets A boolean flag indicating if sockets should
+ /// be opened (if true), or not (if false).
+ /// @param create_managers A boolean flag indicating if managers should be
+ /// recreated.
+ /// @param test A boolean flag which indicates if only testing config.
+ /// @param lease_affinity A flag which indicates if lease affinity should
+ /// be enabled or disabled.
+ void configure(const std::string& config,
+ const bool commit = true,
+ const bool open_sockets = true,
+ const bool create_managers = true,
+ const bool test = false,
+ const LeaseAffinity lease_affinity = LEASE_AFFINITY_DISABLED);
+
+ /// @brief Configure the DHCPv4 server using the JSON string.
+ ///
+ /// @param config String holding server configuration in JSON format.
+ /// @param srv Instance of the server to be configured.
+ /// @param commit A boolean flag indicating if the new configuration
+ /// should be committed (if true), or not (if false).
+ /// @param open_sockets A boolean flag indicating if sockets should
+ /// be opened (if true), or not (if false).
+ /// @param create_managers A boolean flag indicating if managers should be
+ /// recreated.
+ /// @param test A boolean flag which indicates if only testing config.
+ /// @param lease_affinity A flag which indicates if lease affinity should
+ /// be enabled or disabled.
+ void configure(const std::string& config,
+ NakedDhcpv4Srv& srv,
+ const bool commit = true,
+ const bool open_sockets = true,
+ const bool create_managers = true,
+ const bool test = false,
+ const LeaseAffinity lease_affinity = LEASE_AFFINITY_DISABLED);
+
+ /// @brief Configure specified DHCP server using JSON string.
+ ///
+ /// @param config String holding server configuration in JSON format.
+ /// @param srv Instance of the server to be configured.
+ /// @param commit A boolean flag indicating if the new configuration
+ /// should be committed (if true), or not (if false).
+ /// @param exp_rcode expected status code (default = 0 (success))
+ /// @return (a pair of status code and a string with result)
+ std::pair<int, std::string>
+ configureWithStatus(const std::string& config, NakedDhcpv4Srv& srv,
+ const bool commit = true, const int exp_rcode = 0);
+
+ /// @brief Pretends a packet of specified type was received.
+ ///
+ /// Instantiates fake network interfaces, configures passed Dhcpv4Srv,
+ /// then creates a message of specified type and sends it to the
+ /// server and then checks whether expected statistics were set
+ /// appropriately.
+ ///
+ /// @param srv the DHCPv4 server to be used
+ /// @param config JSON configuration to be used
+ /// @param pkt_type type of the packet to be faked
+ /// @param stat_name name of the expected statistic
+ void pretendReceivingPkt(NakedDhcpv4Srv& srv, const std::string& config,
+ uint8_t pkt_type, const std::string& stat_name);
+
+ /// @brief Create @c Dhcpv4Exchange from client's query.
+ Dhcpv4Exchange createExchange(const Pkt4Ptr& query);
+
+ /// @brief Performs 4-way exchange to obtain new lease.
+ ///
+ /// This is used as a preparatory step for Decline operation.
+ ///
+ /// @param client Client to be used to obtain a lease.
+ void acquireLease(Dhcp4Client& client);
+
+ /// @brief Tests if the acquired lease is or is not declined.
+ ///
+ /// @param client Dhcp4Client instance
+ /// @param hw_address_1 HW Address to be used to acquire the lease.
+ /// @param client_id_1 Client id to be used to acquire the lease.
+ /// @param hw_address_2 HW Address to be used to decline the lease.
+ /// @param client_id_2 Client id to be used to decline the lease.
+ /// @param expected_result SHOULD_PASS if the lease is expected to
+ /// be successfully declined, or SHOULD_FAIL if the lease is expected
+ /// to not be declined.
+ /// @param config_index specifies which config index should be used:
+ /// 0 - memfile, 1 - mysql, 2 - pgsql
+ void acquireAndDecline(Dhcp4Client& client,
+ const std::string& hw_address_1,
+ const std::string& client_id_1,
+ const std::string& hw_address_2,
+ const std::string& client_id_2,
+ ExpectedResult expected_result,
+ uint8_t config_index = 0);
+
+ /// @brief Checks if received relay agent info option is echoed back to the
+ /// client.
+ void relayAgentInfoEcho();
+
+ /// @brief Checks if received bad relay agent info option is not echoed back
+ /// to the client.
+ void badRelayAgentInfoEcho();
+
+ /// @brief Checks if client port can be overridden in packets being sent.
+ void portsClientPort();
+
+ /// @brief Checks if server port can be overridden in packets being sent.
+ void portsServerPort();
+
+ /// @brief Check if example files contain valid configuration.
+ void checkConfigFiles();
+
+ /// @brief Check if the server configuration stored in file is valid.
+ ///
+ /// @param path The path to the configuration file.
+ void loadConfigFile(const std::string& path);
+
+ /// @brief This function cleans up after the test.
+ virtual void TearDown();
+
+ /// @brief Set multi-threading mode.
+ void setMultiThreading(bool enabled) {
+ multi_threading_ = enabled;
+ }
+
+ /// @brief A subnet used in most tests.
+ Subnet4Ptr subnet_;
+
+ /// @brief A pool used in most tests.
+ Pool4Ptr pool_;
+
+ /// @brief A client-id used in most tests.
+ ClientIdPtr client_id_;
+
+ /// @brief Return code
+ int rcode_;
+
+ /// @brief Comment received from configuration.
+ isc::data::ConstElementPtr comment_;
+
+ /// @brief Server object under test.
+ NakedDhcpv4Srv srv_;
+
+ /// @brief The multi-threading flag.
+ bool multi_threading_;
+};
+
+/// @brief Patch the server config to add interface-config/re-detect=false
+/// @param json the server config
+inline void
+disableIfacesReDetect(isc::data::ConstElementPtr json) {
+ isc::data::ConstElementPtr ifaces_cfg = json->get("interfaces-config");
+ if (ifaces_cfg) {
+ isc::data::ElementPtr mutable_cfg =
+ boost::const_pointer_cast<isc::data::Element>(ifaces_cfg);
+ mutable_cfg->set("re-detect", isc::data::Element::create(false));
+ }
+}
+
+/// @brief Patch the server config to add multi-threading/enable-multi-threading
+/// @param json the server config
+inline void
+configureMultiThreading(bool enabled, isc::data::ConstElementPtr json) {
+ isc::data::ConstElementPtr multi_threading = json->get("multi-threading");
+ if (!multi_threading) {
+ isc::data::ElementPtr mutable_cfg =
+ boost::const_pointer_cast<isc::data::Element>(json);
+ multi_threading = isc::data::Element::createMap();
+ mutable_cfg->set("multi-threading", multi_threading);
+ }
+
+ isc::data::ElementPtr mutable_cfg =
+ boost::const_pointer_cast<isc::data::Element>(multi_threading);
+ if (enabled) {
+ mutable_cfg->set("enable-multi-threading", isc::data::Element::create(true));
+ mutable_cfg->set("thread-pool-size", isc::data::Element::create(4));
+ mutable_cfg->set("packet-queue-size", isc::data::Element::create(4));
+ } else {
+ mutable_cfg->set("enable-multi-threading", isc::data::Element::create(false));
+ }
+}
+
+/// @brief Runs parser in JSON mode, useful for parser testing
+///
+/// @param in string to be parsed
+/// @return ElementPtr structure representing parsed JSON
+inline isc::data::ElementPtr
+parseJSON(const std::string& in) {
+ isc::dhcp::Parser4Context ctx;
+ return (ctx.parseString(in, isc::dhcp::Parser4Context::PARSER_JSON));
+}
+
+/// @brief Runs parser in Dhcp4 mode
+///
+/// This is a simplified Dhcp4 mode, so no outer { } and "Dhcp4" is
+/// needed. This format is used by most of the tests.
+///
+/// @param in string to be parsed
+/// @param verbose display the exception message when it fails
+/// @return ElementPtr structure representing parsed JSON
+inline isc::data::ElementPtr
+parseDHCP4(const std::string& in, bool verbose = false) {
+ try {
+ isc::dhcp::Parser4Context ctx;
+ isc::data::ElementPtr json;
+ json = ctx.parseString(in, isc::dhcp::Parser4Context::SUBPARSER_DHCP4);
+ disableIfacesReDetect(json);
+ return (json);
+ }
+ catch (const std::exception& ex) {
+ if (verbose) {
+ std::cout << "EXCEPTION: " << ex.what() << std::endl;
+ }
+ throw;
+ }
+}
+
+/// @brief Runs parser in option definition mode
+///
+/// This function parses specified text as JSON that defines option definitions.
+///
+/// @param in string to be parsed
+/// @param verbose display the exception message when it fails
+/// @return ElementPtr structure representing parsed JSON
+inline isc::data::ElementPtr
+parseOPTION_DEFS(const std::string& in, bool verbose = false) {
+ try {
+ isc::dhcp::Parser4Context ctx;
+ return (ctx.parseString(in, isc::dhcp::Parser4Context::PARSER_OPTION_DEFS));
+ }
+ catch (const std::exception& ex) {
+ if (verbose) {
+ std::cout << "EXCEPTION: " << ex.what() << std::endl;
+ }
+ throw;
+ }
+}
+
+} // end of isc::dhcp::test namespace
+} // end of isc::dhcp namespace
+} // end of isc namespace
+
+#endif // DHCP4_TEST_UTILS_H
diff --git a/src/bin/dhcp4/tests/dhcp4_unittests.cc b/src/bin/dhcp4/tests/dhcp4_unittests.cc
new file mode 100644
index 0000000..3a7d4ec
--- /dev/null
+++ b/src/bin/dhcp4/tests/dhcp4_unittests.cc
@@ -0,0 +1,26 @@
+// Copyright (C) 2011-2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <log/logger_support.h>
+#include <dhcp4/dhcp4_log.h>
+#include <gtest/gtest.h>
+
+int
+main(int argc, char* argv[]) {
+
+ ::testing::InitGoogleTest(&argc, argv);
+
+ // See the documentation of the KEA_* environment variables in
+ // src/lib/log/README for info on how to tweak logging
+ isc::log::initLogger();
+
+ setenv("KEA_PIDFILE_DIR", TEST_DATA_BUILDDIR, 1);
+ int result = RUN_ALL_TESTS();
+
+ return (result);
+}
diff --git a/src/bin/dhcp4/tests/dhcp4to6_ipc_unittest.cc b/src/bin/dhcp4/tests/dhcp4to6_ipc_unittest.cc
new file mode 100644
index 0000000..2a47d71
--- /dev/null
+++ b/src/bin/dhcp4/tests/dhcp4to6_ipc_unittest.cc
@@ -0,0 +1,411 @@
+// Copyright (C) 2015-2020 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <asiolink/io_address.h>
+#include <dhcp/pkt4o6.h>
+#include <dhcp/pkt6.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcp4/ctrl_dhcp4_srv.h>
+#include <dhcp4/dhcp4to6_ipc.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/testutils/dhcp4o6_test_ipc.h>
+#include <stats/stats_mgr.h>
+#include <hooks/callout_handle.h>
+#include <hooks/hooks_manager.h>
+
+#include <gtest/gtest.h>
+#include <stdint.h>
+#include <utility>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::stats;
+using namespace isc::hooks;
+using namespace isc::util;
+
+namespace {
+
+/// @brief Port number used in tests.
+const uint16_t TEST_PORT = 32000;
+
+/// @brief Define short name for the test IPC.
+typedef Dhcp4o6TestIpc TestIpc;
+
+/// @brief Test fixture class for DHCPv4 endpoint of DHCPv4o6 IPC.
+class Dhcp4to6IpcTest : public Dhcpv4SrvTest {
+public:
+
+ /// @brief Constructor
+ ///
+ /// Configures IPC to use a test port. It also provides a fake
+ /// configuration of interfaces.
+ Dhcp4to6IpcTest()
+ : Dhcpv4SrvTest(),
+ iface_mgr_test_config_(true) {
+ IfaceMgr::instance().openSockets4();
+ configurePort(TEST_PORT);
+ // Install buffer4_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().
+ registerCallout("buffer4_receive",
+ buffer4_receive_callout));
+ // Install buffer4_send_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().
+ registerCallout("buffer4_send", buffer4_send_callout));
+ // Verify we have a controlled server
+ ControlledDhcpv4Srv* srv = NULL;
+ EXPECT_NO_THROW(srv = ControlledDhcpv4Srv::getInstance());
+ EXPECT_TRUE(srv);
+ // Let's wipe all existing statistics.
+ StatsMgr::instance().removeAll();
+
+ // Set the flags to false as we expect them to be set in callouts.
+ callback_recv_pkt_options_copy_ = std::make_pair(false, false);
+ callback_sent_pkt_options_copy_ = std::make_pair(false, false);
+ }
+
+ /// @brief Destructor
+ ///
+ /// Various cleanups.
+ virtual ~Dhcp4to6IpcTest() {
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts("buffer4_send");
+ callback_recv_pkt_.reset();
+ callback_sent_pkt_.reset();
+ bool status = HooksManager::unloadLibraries();
+ if (!status) {
+ cerr << "(fixture dtor) unloadLibraries failed" << endl;
+ }
+ }
+
+ /// @brief Configure DHCP4o6 port.
+ ///
+ /// @param port New port.
+ void configurePort(uint16_t port);
+
+ /// @brief Creates an instance of the DHCPv4o6 Message option.
+ ///
+ /// The option will contain an empty DHCPREQUEST message, with
+ /// just the Message Type option inside and nothing else.
+ ///
+ /// @return Pointer to the instance of the DHCPv4-query Message option.
+ OptionPtr createDHCPv4MsgOption() const;
+
+ /// @brief Handler for the buffer4_receive hook
+ ///
+ /// This hook is at the beginning of processPacket
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int buffer4_receive_callout(CalloutHandle& callout_handle) {
+ callout_handle.getArgument("query4", callback_recv_pkt_);
+ Pkt4o6Ptr pkt4 = boost::dynamic_pointer_cast<Pkt4o6>(callback_recv_pkt_);
+ if (pkt4) {
+ callback_recv_pkt_options_copy_.first = pkt4->isCopyRetrievedOptions();
+ Pkt6Ptr pkt6 = pkt4->getPkt6();
+ if (pkt6) {
+ callback_recv_pkt_options_copy_.second =
+ pkt6->isCopyRetrievedOptions();
+ }
+ }
+ return (0);
+ }
+
+ /// @brief Handler for the buffer4_send hook
+ ///
+ /// This hook is at the end of the DHCPv4o6 packet handler
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int buffer4_send_callout(CalloutHandle& callout_handle) {
+ callout_handle.getArgument("response4", callback_sent_pkt_);
+ Pkt4o6Ptr pkt4 = boost::dynamic_pointer_cast<Pkt4o6>(callback_sent_pkt_);
+ if (pkt4) {
+ callback_sent_pkt_options_copy_.first = pkt4->isCopyRetrievedOptions();
+ Pkt6Ptr pkt6 = pkt4->getPkt6();
+ if (pkt6) {
+ callback_sent_pkt_options_copy_.second =
+ pkt6->isCopyRetrievedOptions();
+ }
+ }
+ return (0);
+ }
+
+ /// @brief Response Pkt4 shared pointer returned in the receive callout
+ static Pkt4Ptr callback_recv_pkt_;
+
+ /// @brief Response Pkt4 shared pointer returned in the send callout
+ static Pkt4Ptr callback_sent_pkt_;
+
+ /// Flags indicating if copying retrieved options was enabled for
+ /// a received packet during callout execution.
+ static std::pair<bool, bool> callback_recv_pkt_options_copy_;
+
+ /// Flags indicating if copying retrieved options was enabled for
+ /// a sent packet during callout execution.
+ static std::pair<bool, bool> callback_sent_pkt_options_copy_;
+
+ /// @brief reference to a controlled server
+ ///
+ /// Dhcp4to6Ipc::handler() uses the instance of the controlled server
+ /// so it has to be build. This reference does this.
+ ControlledDhcpv4Srv srv_;
+
+private:
+
+ /// @brief Provides fake configuration of interfaces.
+ IfaceMgrTestConfig iface_mgr_test_config_;
+
+};
+
+Pkt4Ptr Dhcp4to6IpcTest::callback_recv_pkt_;
+Pkt4Ptr Dhcp4to6IpcTest::callback_sent_pkt_;
+std::pair<bool, bool> Dhcp4to6IpcTest::callback_recv_pkt_options_copy_;
+std::pair<bool, bool> Dhcp4to6IpcTest::callback_sent_pkt_options_copy_;
+
+void
+Dhcp4to6IpcTest::configurePort(uint16_t port) {
+ CfgMgr::instance().getStagingCfg()->setDhcp4o6Port(port);
+}
+
+OptionPtr
+Dhcp4to6IpcTest::createDHCPv4MsgOption() const {
+ // Create the DHCPv4 message.
+ Pkt4Ptr pkt(new Pkt4(DHCPREQUEST, 1234));
+ // Make a wire representation of the DHCPv4 message.
+ pkt->pack();
+ OutputBuffer& output_buffer = pkt->getBuffer();
+ const uint8_t* data = static_cast<const uint8_t*>(output_buffer.getData());
+ OptionBuffer option_buffer(data, data + output_buffer.getLength());
+
+ // Create the DHCPv4 Message option holding the created message.
+ OptionPtr opt_msg(new Option(Option::V6, D6O_DHCPV4_MSG, option_buffer));
+ return (opt_msg);
+}
+
+// This test verifies that the IPC returns an error when trying to bind
+// to the out of range port.
+TEST_F(Dhcp4to6IpcTest, invalidPortError) {
+ // Create instance of the IPC endpoint under test with out-of-range port.
+ configurePort(65535);
+ Dhcp4to6Ipc& ipc = Dhcp4to6Ipc::instance();
+ EXPECT_THROW(ipc.open(), isc::OutOfRange);
+}
+
+// This test verifies that the DHCPv4 endpoint of the DHCPv4o6 IPC can
+// receive messages.
+TEST_F(Dhcp4to6IpcTest, receive) {
+ // Create instance of the IPC endpoint under test.
+ Dhcp4to6Ipc& ipc = Dhcp4to6Ipc::instance();
+ // Create instance of the IPC endpoint being used as a source of messages.
+ TestIpc src_ipc(TEST_PORT, TestIpc::ENDPOINT_TYPE_V6);
+
+ // Open both endpoints.
+ ASSERT_NO_THROW(ipc.open());
+ ASSERT_NO_THROW(src_ipc.open());
+
+ // Create message to be sent over IPC.
+ Pkt6Ptr pkt(new Pkt6(DHCPV6_DHCPV4_QUERY, 1234));
+ pkt->addOption(createDHCPv4MsgOption());
+ pkt->setIface("eth0");
+ pkt->setIndex(ETH0_INDEX);
+ pkt->setRemoteAddr(IOAddress("2001:db8:1::123"));
+ ASSERT_NO_THROW(pkt->pack());
+
+ // Reset the received packet
+ Dhcp4to6IpcTest::callback_recv_pkt_.reset();
+
+ // Send and wait up to 1 second to receive it.
+ ASSERT_NO_THROW(src_ipc.send(pkt));
+ ASSERT_NO_THROW(IfaceMgr::instance().receive6(1, 0));
+
+ // Make sure that the message has been received.
+ // The buffer4_receive hook is at the beginning of processPacket
+ // so this proves it was passed to it.
+ Pkt4Ptr pkt4_received = Dhcp4to6IpcTest::callback_recv_pkt_;
+ ASSERT_TRUE(pkt4_received);
+ Pkt4o6Ptr pkt_received =
+ boost::dynamic_pointer_cast<Pkt4o6>(pkt4_received);
+ ASSERT_TRUE(pkt_received);
+ Pkt6Ptr pkt6_received = pkt_received->getPkt6();
+ ASSERT_TRUE(pkt6_received);
+ EXPECT_EQ("eth0", pkt6_received->getIface());
+ EXPECT_EQ(ETH0_INDEX, pkt6_received->getIndex());
+ EXPECT_EQ("2001:db8:1::123", pkt6_received->getRemoteAddr().toText());
+
+ // Both DHCP4o6 and encapsulated DHCPv6 packet should have the
+ // flag enabled.
+ EXPECT_TRUE(callback_recv_pkt_options_copy_.first);
+ EXPECT_TRUE(callback_recv_pkt_options_copy_.second);
+}
+
+// This test verifies that message with multiple DHCPv4 query options
+// is rejected.
+TEST_F(Dhcp4to6IpcTest, receiveMultipleQueries) {
+ // Create instance of the IPC endpoint under test.
+ Dhcp4to6Ipc& ipc = Dhcp4to6Ipc::instance();
+ // Create instance of the IPC endpoint being used as a source of messages.
+ TestIpc src_ipc(TEST_PORT, TestIpc::ENDPOINT_TYPE_V6);
+
+ // Open both endpoints.
+ ASSERT_NO_THROW(ipc.open());
+ ASSERT_NO_THROW(src_ipc.open());
+
+ // Create message to be sent over IPC.
+ Pkt6Ptr pkt(new Pkt6(DHCPV6_DHCPV4_QUERY, 1234));
+ // Add two DHCPv4 query options.
+ pkt->addOption(createDHCPv4MsgOption());
+ pkt->addOption(createDHCPv4MsgOption());
+ pkt->setIface("eth0");
+ pkt->setIndex(ETH0_INDEX);
+ pkt->setRemoteAddr(IOAddress("2001:db8:1::123"));
+ ASSERT_NO_THROW(pkt->pack());
+
+ // Reset the received packet
+ Dhcp4to6IpcTest::callback_recv_pkt_.reset();
+
+ // Send and wait up to 1 second to receive it.
+ ASSERT_NO_THROW(src_ipc.send(pkt));
+ EXPECT_NO_THROW(IfaceMgr::instance().receive6(1, 0));
+
+ // No message should has been sent.
+ Pkt4Ptr pkt4_received = Dhcp4to6IpcTest::callback_recv_pkt_;
+ EXPECT_FALSE(pkt4_received);
+}
+
+// This test verifies that message with no DHCPv4 query options is rejected.
+TEST_F(Dhcp4to6IpcTest, receiveNoQueries) {
+ // Create instance of the IPC endpoint under test.
+ Dhcp4to6Ipc& ipc = Dhcp4to6Ipc::instance();
+ // Create instance of the IPC endpoint being used as a source of messages.
+ TestIpc src_ipc(TEST_PORT, TestIpc::ENDPOINT_TYPE_V6);
+
+ // Open both endpoints.
+ ASSERT_NO_THROW(ipc.open());
+ ASSERT_NO_THROW(src_ipc.open());
+
+ // Create message to be sent over IPC without DHCPv4 query option.
+ Pkt6Ptr pkt(new Pkt6(DHCPV6_DHCPV4_QUERY, 1234));
+ pkt->setIface("eth0");
+ pkt->setIndex(ETH0_INDEX);
+ pkt->setRemoteAddr(IOAddress("2001:db8:1::123"));
+ ASSERT_NO_THROW(pkt->pack());
+
+ // Reset the received packet
+ Dhcp4to6IpcTest::callback_recv_pkt_.reset();
+
+ // Send and wait up to 1 second to receive it.
+ ASSERT_NO_THROW(src_ipc.send(pkt));
+ EXPECT_NO_THROW(IfaceMgr::instance().receive6(1, 0));
+
+ // No message should has been sent.
+ Pkt4Ptr pkt4_received = Dhcp4to6IpcTest::callback_recv_pkt_;
+ EXPECT_FALSE(pkt4_received);
+}
+
+// This test verifies that the DHCPv4 endpoint of the DHCPv4o6 IPC can
+// process messages.
+TEST_F(Dhcp4to6IpcTest, process) {
+ // Create instance of the IPC endpoint under test.
+ Dhcp4to6Ipc& ipc = Dhcp4to6Ipc::instance();
+ // Create instance of the IPC endpoint being used as a source of messages.
+ TestIpc src_ipc(TEST_PORT, TestIpc::ENDPOINT_TYPE_V6);
+
+ // Open both endpoints.
+ ASSERT_NO_THROW(ipc.open());
+ ASSERT_NO_THROW(src_ipc.open());
+
+ // Get statistics
+ StatsMgr& mgr = StatsMgr::instance();
+ ObservationPtr pkt4_snd = mgr.getObservation("pkt4-sent");
+ ObservationPtr pkt4_ack = mgr.getObservation("pkt4-ack-sent");
+ EXPECT_FALSE(pkt4_snd);
+ EXPECT_FALSE(pkt4_ack);
+
+ // Create an information request message
+ Pkt4Ptr infreq(new Pkt4(DHCPINFORM, 1234));
+ infreq->setHWAddr(generateHWAddr(6));
+ infreq->setCiaddr(IOAddress("192.0.1.2"));
+ // Make a wire representation of the DHCPv4 message.
+ infreq->pack();
+ OutputBuffer& output_buffer = infreq->getBuffer();
+ const uint8_t* data = static_cast<const uint8_t*>(output_buffer.getData());
+ OptionBuffer option_buffer(data, data + output_buffer.getLength());
+
+ // Create the DHCPv4 Message option holding the created message.
+ OptionPtr opt_msg(new Option(Option::V6, D6O_DHCPV4_MSG, option_buffer));
+
+ // Create message to be sent over IPC.
+ Pkt6Ptr pkt(new Pkt6(DHCPV6_DHCPV4_QUERY, 1234));
+ pkt->addOption(opt_msg);
+ pkt->setIface("eth0");
+ pkt->setIndex(ETH0_INDEX);
+ pkt->setRemoteAddr(IOAddress("2001:db8:1::123"));
+ ASSERT_NO_THROW(pkt->pack());
+
+ // Reset the received packet
+ Dhcp4to6IpcTest::callback_recv_pkt_.reset();
+
+ // Send and wait up to 1 second to receive it.
+ ASSERT_NO_THROW(src_ipc.send(pkt));
+ ASSERT_NO_THROW(IfaceMgr::instance().receive6(1, 0));
+
+ // Make sure that the message has been received.
+ Pkt4Ptr pkt4_received = Dhcp4to6IpcTest::callback_recv_pkt_;
+ ASSERT_TRUE(pkt4_received);
+ Pkt4o6Ptr pkt_received =
+ boost::dynamic_pointer_cast<Pkt4o6>(pkt4_received);
+ ASSERT_TRUE(pkt_received);
+ Pkt6Ptr pkt6_received = pkt_received->getPkt6();
+ ASSERT_TRUE(pkt6_received);
+ EXPECT_EQ("eth0", pkt6_received->getIface());
+ EXPECT_EQ(ETH0_INDEX, pkt6_received->getIndex());
+ EXPECT_EQ("2001:db8:1::123", pkt6_received->getRemoteAddr().toText());
+
+ // Make sure that the message has been processed.
+ // Using the buffer4_send hook
+ Pkt4Ptr pkt4_sent = Dhcp4to6IpcTest::callback_sent_pkt_;
+ ASSERT_TRUE(pkt4_sent);
+ EXPECT_EQ(DHCPACK, pkt4_sent->getType());
+ Pkt4o6Ptr pkt_sent = boost::dynamic_pointer_cast<Pkt4o6>(pkt4_sent);
+ ASSERT_TRUE(pkt_sent);
+ Pkt6Ptr pkt6_sent = pkt_sent->getPkt6();
+ ASSERT_TRUE(pkt6_sent);
+ EXPECT_EQ(DHCPV6_DHCPV4_RESPONSE, pkt6_sent->getType());
+ EXPECT_EQ("eth0", pkt6_sent->getIface());
+ EXPECT_EQ(ETH0_INDEX, pkt6_sent->getIndex());
+ EXPECT_EQ("2001:db8:1::123", pkt6_sent->getRemoteAddr().toText());
+
+ // Both DHCP4o6 and encapsulated DHCPv6 packet should have the
+ // flag enabled.
+ EXPECT_TRUE(callback_sent_pkt_options_copy_.first);
+ EXPECT_TRUE(callback_sent_pkt_options_copy_.second);
+
+ // Verify the 4o6 part
+ OptionCollection sent_msgs = pkt6_sent->getOptions(D6O_DHCPV4_MSG);
+ ASSERT_EQ(1, sent_msgs.size());
+ OptionPtr sent_msg = sent_msgs.begin()->second;
+ ASSERT_TRUE(sent_msg);
+ const OptionBuffer sent_buf = sent_msg->getData();
+ Pkt4Ptr pkt4_opt;
+ ASSERT_NO_THROW(pkt4_opt.reset(new Pkt4(&sent_buf[0], sent_buf.size())));
+ ASSERT_NO_THROW(pkt4_opt->unpack());
+ EXPECT_EQ(DHCPACK, pkt4_sent->getType());
+ EXPECT_EQ(pkt4_sent->len(), pkt4_opt->len());
+
+ // Verify statistics
+ pkt4_snd = mgr.getObservation("pkt4-sent");
+ pkt4_ack = mgr.getObservation("pkt4-ack-sent");
+ ASSERT_TRUE(pkt4_snd);
+ ASSERT_TRUE(pkt4_ack);
+ EXPECT_EQ(1, pkt4_snd->getInteger().first);
+ EXPECT_EQ(1, pkt4_ack->getInteger().first);
+}
+
+} // end of anonymous namespace
diff --git a/src/bin/dhcp4/tests/direct_client_unittest.cc b/src/bin/dhcp4/tests/direct_client_unittest.cc
new file mode 100644
index 0000000..27439e2
--- /dev/null
+++ b/src/bin/dhcp4/tests/direct_client_unittest.cc
@@ -0,0 +1,438 @@
+// Copyright (C) 2014-2023 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <dhcp/iface_mgr.h>
+#include <dhcp/pkt4.h>
+#include <dhcp/classify.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/cfg_subnets4.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <dhcpsrv/subnet.h>
+#include <dhcp4/json_config_parser.h>
+#include <dhcp4/tests/dhcp4_client.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <gtest/gtest.h>
+#include <string>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+
+namespace {
+
+/// @brief Test fixture class for testing message processing from directly
+/// connected clients.
+///
+/// This class provides mechanisms for testing processing of DHCPv4 messages
+/// from directly connected clients.
+class DirectClientTest : public Dhcpv4SrvTest {
+public:
+ /// @brief Constructor.
+ ///
+ /// Initializes DHCPv4 server object used by various tests.
+ DirectClientTest();
+
+ /// @brief Configures the server with one subnet.
+ ///
+ /// This creates new configuration for the DHCPv4 with one subnet having
+ /// a specified prefix.
+ ///
+ /// The subnet parameters (such as options, timers etc.) are arbitrarily
+ /// selected. The subnet and pool mask is always /24. The real configuration
+ /// would exclude .0 (network address) and .255 (broadcast address), but we
+ /// ignore that fact for the sake of test simplicity.
+ ///
+ /// @param prefix Prefix for a subnet.
+ void configureSubnet(const std::string& prefix);
+
+ /// @brief Configures the server with two subnets.
+ ///
+ /// This function configures DHCPv4 server with two different subnets.
+ /// The subnet parameters (such as options, timers etc.) are arbitrarily
+ /// selected. The subnet and pool mask is /24. The real configuration
+ /// would exclude .0 (network address) and .255 (broadcast address), but we
+ /// ignore that fact for the sake of test simplicity.
+ ///
+ /// @param prefix1 Prefix of the first subnet to be configured.
+ /// @param prefix2 Prefix of the second subnet to be configured.
+ void configureTwoSubnets(const std::string& prefix1,
+ const std::string& prefix2);
+
+ /// @brief Creates simple message from a client.
+ ///
+ /// This function creates a DHCPv4 message having a specified type
+ /// (e.g. Discover, Request) and sets some properties of this
+ /// message: client identifier, address and interface. The copy of
+ /// this message is then created by parsing wire data of the original
+ /// message. This simulates the case when the message is received and
+ /// parsed by the server.
+ ///
+ /// @param msg_type Type of the message to be created.
+ /// @param iface Name of the interface on which the message has been
+ /// "received" by the server.
+ /// @param ifindex Index of the interface on which the message has been
+ /// "received" by the server.
+ ///
+ /// @return Generated message.
+ Pkt4Ptr createClientMessage(const uint16_t msg_type,
+ const std::string& iface,
+ const unsigned int ifindex);
+
+ /// @brief Creates simple message from a client.
+ ///
+ /// This function configures a client's message by adding client identifier,
+ /// setting interface and addresses. The copy of this message is then
+ /// created by parsing wire data of the original message. This simulates the
+ /// case when the message is received and parsed by the server.
+ ///
+ /// @param msg Caller supplied message to be configured. This object must
+ /// not be NULL.
+ /// @param iface Name of the interface on which the message has been
+ /// "received" by the server.
+ /// @param ifindex Index of the interface on which the message has been
+ /// "received" by the server.
+ ///
+ /// @return Configured and parsed message.
+ Pkt4Ptr createClientMessage(const Pkt4Ptr &msg,
+ const std::string& iface,
+ const unsigned int ifindex);
+
+ /// @brief This test checks that the message from directly connected client
+ /// is processed and that client is offered IPv4 address from the subnet
+ /// which is suitable for the local interface on which the client's message
+ /// is received. This test uses two subnets, with two active interfaces
+ /// which IP addresses belong to these subnets. The address offered to the
+ /// client which message has been sent over eth0 should belong to a
+ /// different subnet than the address offered for the client sending its
+ /// message via eth1.
+ void twoSubnets();
+
+ /// @brief This test checks that server selects a subnet when receives a
+ /// message through an interface for which the subnet has been configured.
+ /// This interface has IPv4 address assigned which belongs to this subnet.
+ /// This test also verifies that when the message is received through the
+ /// interface for which there is no suitable subnet, the message is
+ /// discarded.
+ void oneSubnet();
+
+ /// @brief This test verifies that the server uses ciaddr to select a subnet
+ /// for a client which renews its lease.
+ void renew();
+
+ /// This test verifies that when a client in the Rebinding state broadcasts
+ /// a Request message through an interface for which a subnet is configured,
+ /// the server responds to this Request. It also verifies that when such a
+ /// Request is sent through the interface for which there is no subnet
+ /// configured the client's message is discarded.
+ void rebind();
+
+ /// @brief classes the client belongs to
+ ///
+ /// This is empty in most cases, but it is needed as a parameter for all
+ /// getSubnet4() calls.
+ ClientClasses classify_;
+};
+
+DirectClientTest::DirectClientTest() : Dhcpv4SrvTest() {
+}
+
+void
+DirectClientTest::configureSubnet(const std::string& prefix) {
+ std::ostringstream config;
+ config << "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"option-data\": [ ],"
+ "\"subnet4\": [ { "
+ " \"id\": 1, "
+ " \"pools\": [ { \"pool\": \"" << prefix << "/24\" } ],"
+ " \"subnet\": \"" << prefix << "/24\", "
+ " \"rebind-timer\": 2000, "
+ " \"renew-timer\": 1000, "
+ " \"valid-lifetime\": 4000"
+ "} ],"
+ "\"valid-lifetime\": 4000 }";
+
+ configure(config.str());
+}
+
+void
+DirectClientTest::configureTwoSubnets(const std::string& prefix1,
+ const std::string& prefix2) {
+ std::ostringstream config;
+ config << "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"option-data\": [ ],"
+ "\"subnet4\": [ { "
+ " \"id\": 1, "
+ " \"pools\": [ { \"pool\": \"" << prefix1 << "/24\" } ],"
+ " \"subnet\": \"" << prefix1 << "/24\", "
+ " \"rebind-timer\": 2000, "
+ " \"renew-timer\": 1000, "
+ " \"valid-lifetime\": 4000"
+ " },"
+ "{ "
+ " \"id\": 2, "
+ " \"pools\": [ { \"pool\": \"" << prefix2 << "/24\" } ],"
+ " \"subnet\": \"" << prefix2 << "/24\", "
+ " \"rebind-timer\": 2000, "
+ " \"renew-timer\": 1000, "
+ " \"valid-lifetime\": 4000"
+ "} ],"
+ "\"valid-lifetime\": 4000 }";
+
+ configure(config.str());
+}
+
+Pkt4Ptr
+DirectClientTest::createClientMessage(const uint16_t msg_type,
+ const std::string& iface,
+ const unsigned int ifindex) {
+ // Create a source packet.
+ Pkt4Ptr msg = Pkt4Ptr(new Pkt4(msg_type, 1234));
+ return (createClientMessage(msg, iface, ifindex));
+
+}
+
+Pkt4Ptr
+DirectClientTest::createClientMessage(const Pkt4Ptr& msg,
+ const std::string& iface,
+ const unsigned int ifindex) {
+ msg->setRemoteAddr(IOAddress("255.255.255.255"));
+ msg->addOption(generateClientId());
+ msg->setIface(iface);
+ msg->setIndex(ifindex);
+
+ // Create copy of this packet by parsing its wire data. Make sure that the
+ // local and remote address are set like it was a message sent from the
+ // directly connected client.
+ Pkt4Ptr received;
+ createPacketFromBuffer(msg, received);
+ received->setIface(iface);
+ received->setIndex(ifindex);
+ received->setLocalAddr(IOAddress("255.255.255.255"));
+ received->setRemoteAddr(IOAddress("0.0.0.0"));
+
+ return (received);
+}
+
+void
+DirectClientTest::twoSubnets() {
+ // Configure IfaceMgr with fake interfaces lo, eth0 and eth1.
+ IfaceMgrTestConfig iface_config(true);
+ // After creating interfaces we have to open sockets as it is required
+ // by the message processing code.
+ ASSERT_NO_THROW(IfaceMgr::instance().openSockets4());
+ // Add two subnets: address on eth0 belongs to the second subnet,
+ // address on eth1 belongs to the first subnet.
+ ASSERT_NO_FATAL_FAILURE(configureTwoSubnets("192.0.2.0", "10.0.0.0"));
+ // Create Discover and simulate reception of this message through eth0.
+ Pkt4Ptr dis = createClientMessage(DHCPDISCOVER, "eth0", ETH0_INDEX);
+ srv_.fakeReceive(dis);
+ // Create Request and simulate reception of this message through eth1.
+ Pkt4Ptr req = createClientMessage(DHCPREQUEST, "eth1", ETH1_INDEX);
+ srv_.fakeReceive(req);
+
+ // Process clients' messages.
+ srv_.run();
+
+ // Check that the server did send responses.
+ ASSERT_EQ(2, srv_.fake_sent_.size());
+
+ // In multi-threading responses can be received out of order.
+ Pkt4Ptr offer;
+ Pkt4Ptr ack;
+
+ while (srv_.fake_sent_.size()) {
+ // Make sure that we received a response.
+ Pkt4Ptr response = srv_.fake_sent_.front();
+ ASSERT_TRUE(response);
+ srv_.fake_sent_.pop_front();
+
+ if (response->getType() == DHCPOFFER) {
+ offer = response;
+ } else if (response->getType() == DHCPACK) {
+ ack = response;
+ }
+ }
+
+ // Client should get an Offer (not a NAK).
+ ASSERT_TRUE(offer);
+
+ // Client should get an Ack (not a NAK).
+ ASSERT_TRUE(ack);
+
+ // Check that the offered address belongs to the suitable subnet.
+ Subnet4Ptr subnet = CfgMgr::instance().getCurrentCfg()->
+ getCfgSubnets4()->selectSubnet(offer->getYiaddr());
+ ASSERT_TRUE(subnet);
+ EXPECT_EQ("10.0.0.0", subnet->get().first.toText());
+
+
+ // Check that the offered address belongs to the suitable subnet.
+ subnet = CfgMgr::instance().getCurrentCfg()->
+ getCfgSubnets4()->selectSubnet(ack->getYiaddr());
+ ASSERT_TRUE(subnet);
+ EXPECT_EQ("192.0.2.0", subnet->get().first.toText());
+}
+
+TEST_F(DirectClientTest, twoSubnets) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ twoSubnets();
+}
+
+TEST_F(DirectClientTest, twoSubnetsMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ twoSubnets();
+}
+
+void
+DirectClientTest::oneSubnet() {
+ // Configure IfaceMgr with fake interfaces lo, eth0 and eth1.
+ IfaceMgrTestConfig iface_config(true);
+ // After creating interfaces we have to open sockets as it is required
+ // by the message processing code.
+ ASSERT_NO_THROW(IfaceMgr::instance().openSockets4());
+ // Add a subnet which will be selected when a message from directly
+ // connected client is received through interface eth0.
+ ASSERT_NO_FATAL_FAILURE(configureSubnet("10.0.0.0"));
+ // Create Discover and simulate reception of this message through eth0.
+ Pkt4Ptr dis = createClientMessage(DHCPDISCOVER, "eth0", ETH0_INDEX);
+ srv_.fakeReceive(dis);
+ // Create Request and simulate reception of this message through eth1.
+ Pkt4Ptr req = createClientMessage(DHCPDISCOVER, "eth1", ETH1_INDEX);
+ srv_.fakeReceive(req);
+
+ // Process clients' messages.
+ srv_.run();
+
+ // Check that the server sent one response for the message received
+ // through eth0. The other client's message should be discarded.
+ ASSERT_EQ(1, srv_.fake_sent_.size());
+
+ // Check the response. The first Discover was sent via eth0 for which
+ // the subnet has been configured.
+ Pkt4Ptr response = srv_.fake_sent_.front();
+ ASSERT_TRUE(response);
+ srv_.fake_sent_.pop_front();
+
+ // Since Discover has been received through the interface for which
+ // the subnet has been configured, the server should respond with
+ // an Offer message.
+ ASSERT_EQ(DHCPOFFER, response->getType());
+ // Check that the offered address belongs to the suitable subnet.
+ Subnet4Ptr subnet = CfgMgr::instance().getCurrentCfg()->
+ getCfgSubnets4()->selectSubnet(response->getYiaddr());
+ ASSERT_TRUE(subnet);
+ EXPECT_EQ("10.0.0.0", subnet->get().first.toText());
+}
+
+TEST_F(DirectClientTest, oneSubnet) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ oneSubnet();
+}
+
+TEST_F(DirectClientTest, oneSubnetMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ oneSubnet();
+}
+
+void
+DirectClientTest::renew() {
+ // Configure IfaceMgr with fake interfaces lo, eth0 and eth1.
+ IfaceMgrTestConfig iface_config(true);
+ // After creating interfaces we have to open sockets as it is required
+ // by the message processing code.
+ ASSERT_NO_THROW(IfaceMgr::instance().openSockets4());
+ // Add a subnet.
+ ASSERT_NO_FATAL_FAILURE(configureSubnet("10.0.0.0"));
+
+ // Create the DHCPv4 client.
+ Dhcp4Client client;
+ client.useRelay(false);
+
+ // Obtain the lease using the 4-way exchange.
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<IOAddress>(new IOAddress("10.0.0.10"))));
+ ASSERT_EQ("10.0.0.10", client.config_.lease_.addr_.toText());
+
+ // Put the client into the renewing state.
+ client.setState(Dhcp4Client::RENEWING);
+
+ // Renew, and make sure we have obtained the same address.
+ ASSERT_NO_THROW(client.doRequest());
+ ASSERT_TRUE(client.getContext().response_);
+ EXPECT_EQ(DHCPACK, static_cast<int>(client.getContext().response_->getType()));
+ EXPECT_EQ("10.0.0.10", client.config_.lease_.addr_.toText());
+}
+
+TEST_F(DirectClientTest, renew) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ renew();
+}
+
+TEST_F(DirectClientTest, renewMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ renew();
+}
+
+void
+DirectClientTest::rebind() {
+ // Configure IfaceMgr with fake interfaces lo, eth0 and eth1.
+ IfaceMgrTestConfig iface_config(true);
+ // After creating interfaces we have to open sockets as it is required
+ // by the message processing code.
+ ASSERT_NO_THROW(IfaceMgr::instance().openSockets4());
+ // Add a subnet.
+ ASSERT_NO_FATAL_FAILURE(configureSubnet("10.0.0.0"));
+
+ // Create the DHCPv4 client.
+ Dhcp4Client client;
+ client.useRelay(false);
+
+ // Obtain the lease using the 4-way exchange.
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<IOAddress>(new IOAddress("10.0.0.10"))));
+ ASSERT_EQ("10.0.0.10", client.config_.lease_.addr_.toText());
+
+ // Put the client into the rebinding state.
+ client.setState(Dhcp4Client::REBINDING);
+
+ // Broadcast Request through an interface for which there is no subnet
+ // configured. This message should be discarded by the server.
+ client.setIfaceName("eth1");
+ client.setIfaceIndex(ETH1_INDEX);
+ ASSERT_NO_THROW(client.doRequest());
+ EXPECT_FALSE(client.getContext().response_);
+
+ // Send Rebind over the correct interface, and make sure we have obtained
+ // the same address.
+ client.setIfaceName("eth0");
+ client.setIfaceIndex(ETH0_INDEX);
+ ASSERT_NO_THROW(client.doRequest());
+ ASSERT_TRUE(client.getContext().response_);
+ EXPECT_EQ(DHCPACK, static_cast<int>(client.getContext().response_->getType()));
+ EXPECT_EQ("10.0.0.10", client.config_.lease_.addr_.toText());
+}
+
+TEST_F(DirectClientTest, rebind) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ rebind();
+}
+
+TEST_F(DirectClientTest, rebindMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ rebind();
+}
+
+}
diff --git a/src/bin/dhcp4/tests/dora_unittest.cc b/src/bin/dhcp4/tests/dora_unittest.cc
new file mode 100644
index 0000000..be2c7cf
--- /dev/null
+++ b/src/bin/dhcp4/tests/dora_unittest.cc
@@ -0,0 +1,3192 @@
+// Copyright (C) 2014-2023 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <asiolink/io_address.h>
+#include <cc/data.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/host.h>
+#include <dhcpsrv/host_mgr.h>
+#include <dhcpsrv/subnet_id.h>
+#include <testutils/gtest_utils.h>
+
+#ifdef HAVE_MYSQL
+#include <mysql/testutils/mysql_schema.h>
+#endif
+
+#ifdef HAVE_PGSQL
+#include <pgsql/testutils/pgsql_schema.h>
+#endif
+
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <dhcp4/tests/dhcp4_client.h>
+#include <boost/shared_ptr.hpp>
+#include <stats/stats_mgr.h>
+
+#include <set>
+#include <vector>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::stats;
+
+namespace {
+
+/// @brief Set of JSON configurations used throughout the DORA tests.
+///
+/// - Configuration 0:
+/// - Used for testing direct traffic
+/// - 1 subnet: 10.0.0.0/24
+/// - 1 pool: 10.0.0.10-10.0.0.100
+/// - Router option present: 10.0.0.200 and 10.0.0.201
+/// - Domain Name Server option present: 10.0.0.202, 10.0.0.203.
+/// - Log Servers option present: 192.0.2.200 and 192.0.2.201
+/// - Quotes Servers option present: 192.0.2.202, 192.0.2.203.
+///
+/// - Configuration 1:
+/// - Use for testing relayed messages
+/// - 1 subnet: 192.0.2.0/24
+/// - Router option present: 192.0.2.200 and 192.0.2.201
+/// - Domain Name Server option present: 192.0.2.202, 192.0.2.203.
+/// - Log Servers option present: 192.0.2.200 and 192.0.2.201
+/// - Quotes Servers option present: 192.0.2.202, 192.0.2.203.
+///
+/// - Configuration 2:
+/// - Use for testing simple scenarios with host reservations
+/// - 1 subnet: 10.0.0.0/24
+/// - One reservation for the client using MAC address:
+/// aa:bb:cc:dd:ee:ff, reserved address 10.0.0.7
+///
+/// - Configuration 3:
+/// - Use for testing match-client-id flag
+/// - 1 subnet: 10.0.0.0/24
+/// - 1 pool: 10.0.0.10-10.0.0.100
+/// - match-client-id flag is set to false, thus the server
+/// uses HW address for lease lookup, rather than client id
+///
+/// - Configuration 4:
+/// - Used for testing host reservations where circuit-id takes precedence
+/// over hw-address, and the hw-address takes precedence over duid.
+/// - 1 subnet: 10.0.0.0/24
+/// - 3 reservations for this subnet:
+/// - IP address 10.0.0.7 for HW address aa:bb:cc:dd:ee:ff
+/// - IP address 10.0.0.8 for DUID 01:02:03:04:05
+/// - IP address 10.0.0.9 for circuit-id 'charter950'
+/// - IP address 10.0.0.1 for client-id
+///
+/// - Configuration 5:
+/// - The same as configuration 4, but using the following order of
+/// host-reservation-identifiers: duid, circuit-id, hw-address,
+/// client-id.
+///
+/// - Configuration 6:
+/// - This configuration provides reservations for next-server,
+/// server-hostname and boot-file-name value.
+/// - 1 subnet: 10.0.0.0/24
+/// - 1 reservation for this subnet:
+/// - Client's HW address: aa:bb:cc:dd:ee:ff
+/// - next-server = 10.0.0.7
+/// - server name = "some-name.example.org"
+/// - boot-file-name = "bootfile.efi"
+///
+/// - Configuration 7:
+/// - Used for testing custom value of dhcp-server-identifier option.
+/// - 3 subnets: 10.0.0.0/24, 192.0.2.0/26 and 192.0.2.64/26
+/// - Custom server identifier specified for 2 subnets subnet.
+/// - Custom server identifier specified at global level.
+///
+/// - Configuration 8:
+/// - Simple configuration with a single subnet and single pool
+/// - Using MySQL lease database backend to store leases
+///
+/// - Configuration 9:
+/// - Simple configuration with a single subnet and single pool
+/// - Using PostgreSQL lease database backend to store leases
+///
+/// - Configuration 10:
+/// - Simple configuration with a single subnet
+/// - One in-pool reservation for a circuit-id of 'charter950'
+///
+/// - Configuration 11:
+/// - Simple configuration with a single subnet
+/// - One in-pool reservation for MAC address aa:bb:cc:dd:ee:ff
+/// - The reservations-in-subnet flag is set to true
+///
+/// - Configuration 12:
+/// - Simple configuration with a single subnet as in #12
+/// - The reservations-in-subnet flag is set to false for testing that the
+/// reservations are ignored
+///
+/// - Configuration 13:
+/// - Simple configuration with a single subnet
+/// - Two host reservations, one out of the pool, another one in pool
+/// - The reservations-in-subnet and reservations-out-of-pool flags are set to
+/// true to test that only out of pool reservations are honored.
+///
+/// - Configuration 14:
+/// - Use for testing authoritative flag
+/// - 1 subnet: 10.0.0.0/24
+/// - 1 pool: 10.0.0.10-10.0.0.100
+/// - authoritative flag is set to true, thus the server responds
+/// with DHCPNAK to requests from unknown clients.
+///
+/// - Configuration 15:
+/// - Use for testing authoritative flag
+/// - 1 subnet: 10.0.0.0/24
+/// - 1 pool: 10.0.0.10-10.0.0.100
+/// - authoritative flag is set to false, thus the server does not
+/// respond to requests from unknown clients.
+///
+/// - Configuration 16:
+/// - Use for testing storing extended info
+/// - Two subnets with one with storing extended info enabled and
+/// one with disabled.
+///
+/// - Configuration 17:
+/// - Selects random allocator.
+/// - One subnet with three distinct pools.
+/// - Random allocator enabled globally.
+///
+/// - Configuration 18:
+/// - Two subnets, one with an address pool, one without.
+/// - cache-max-age set to 600
+/// - cache-threshold set to .50.
+///
+/// - Configuration 19:
+/// - No subnets.
+/// - Global authoritative flag is false.
+///
+/// - Configuration 20:
+/// - No subnets.
+/// - Global authoritative flag is true.
+const char* DORA_CONFIGS[] = {
+ // Configuration 0
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"option-data\": [ {"
+ " \"name\": \"routers\","
+ " \"data\": \"10.0.0.200,10.0.0.201\""
+ " },"
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"10.0.0.202,10.0.0.203\""
+ " },"
+ " {"
+ " \"name\": \"log-servers\","
+ " \"data\": \"10.0.0.200,10.0.0.201\""
+ " },"
+ " {"
+ " \"name\": \"cookie-servers\","
+ " \"data\": \"10.0.0.202,10.0.0.203\""
+ " } ]"
+ " } ]"
+ "}",
+
+ // Configuration 1
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"id\": 1,"
+ " \"option-data\": [ {"
+ " \"name\": \"routers\","
+ " \"data\": \"192.0.2.200,192.0.2.201\""
+ " },"
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.202,192.0.2.203\""
+ " },"
+ " {"
+ " \"name\": \"log-servers\","
+ " \"data\": \"10.0.0.200,10.0.0.201\""
+ " },"
+ " {"
+ " \"name\": \"cookie-servers\","
+ " \"data\": \"10.0.0.202,10.0.0.203\""
+ " } ]"
+ " } ]"
+ "}",
+
+ // Configuration 2
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"reservations\": [ "
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"ip-address\": \"10.0.0.7\""
+ " },"
+ " {"
+ " \"duid\": \"01:02:03:04:05\","
+ " \"ip-address\": \"10.0.0.8\""
+ " },"
+ " {"
+ " \"circuit-id\": \"'charter950'\","
+ " \"ip-address\": \"10.0.0.9\""
+ " },"
+ " {"
+ " \"client-id\": \"01:11:22:33:44:55:66\","
+ " \"ip-address\": \"10.0.0.1\""
+ " }"
+ " ]"
+ "} ]"
+ "}",
+
+ // Configuration 3
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"match-client-id\": false,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"option-data\": [ {"
+ " \"name\": \"routers\","
+ " \"data\": \"10.0.0.200,10.0.0.201\""
+ " } ]"
+ " } ]"
+ "}",
+
+ // Configuration 4
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"host-reservation-identifiers\": [ \"circuit-id\", \"hw-address\","
+ " \"duid\", \"client-id\" ],"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"reservations\": [ "
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"ip-address\": \"10.0.0.7\""
+ " },"
+ " {"
+ " \"duid\": \"01:02:03:04:05\","
+ " \"ip-address\": \"10.0.0.8\""
+ " },"
+ " {"
+ " \"circuit-id\": \"'charter950'\","
+ " \"ip-address\": \"10.0.0.9\""
+ " },"
+ " {"
+ " \"client-id\": \"01:11:22:33:44:55:66\","
+ " \"ip-address\": \"10.0.0.1\""
+ " }"
+ " ]"
+ "} ]"
+ "}",
+
+ // Configuration 5
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"host-reservation-identifiers\": [ \"duid\", \"client-id\","
+ " \"circuit-id\", \"hw-address\" ],"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"reservations\": [ "
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"ip-address\": \"10.0.0.7\""
+ " },"
+ " {"
+ " \"duid\": \"01:02:03:04:05\","
+ " \"ip-address\": \"10.0.0.8\""
+ " },"
+ " {"
+ " \"circuit-id\": \"'charter950'\","
+ " \"ip-address\": \"10.0.0.9\""
+ " },"
+ " {"
+ " \"client-id\": \"01:11:22:33:44:55:66\","
+ " \"ip-address\": \"10.0.0.1\""
+ " }"
+ " ]"
+ "} ]"
+ "}",
+
+ // Configuration 6
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"next-server\": \"10.0.0.1\","
+ "\"server-hostname\": \"nohost\","
+ "\"boot-file-name\": \"nofile\","
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"reservations\": [ "
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"next-server\": \"10.0.0.7\","
+ " \"server-hostname\": \"some-name.example.org\","
+ " \"boot-file-name\": \"bootfile.efi\""
+ " }"
+ " ]"
+ "} ]"
+ "}",
+
+ // Configuration 7
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"option-data\": ["
+ " {"
+ " \"name\": \"dhcp-server-identifier\","
+ " \"data\": \"3.4.5.6\""
+ " }"
+ "],"
+ "\"subnet4\": ["
+ " {"
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1, "
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"interface\": \"eth0\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"dhcp-server-identifier\","
+ " \"data\": \"1.2.3.4\""
+ " }"
+ " ]"
+ " },"
+ " {"
+ " \"subnet\": \"192.0.2.0/26\", "
+ " \"id\": 2, "
+ " \"pools\": [ { \"pool\": \"192.0.2.10-192.0.2.63\" } ],"
+ " \"interface\": \"eth1\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"dhcp-server-identifier\","
+ " \"data\": \"2.3.4.5\""
+ " }"
+ " ]"
+ " },"
+ " {"
+ " \"subnet\": \"192.0.2.64/26\", "
+ " \"id\": 3, "
+ " \"pools\": [ { \"pool\": \"192.0.2.65-192.0.2.100\" } ],"
+ " \"relay\": {"
+ " \"ip-address\": \"10.2.3.4\""
+ " }"
+ " }"
+ "]"
+ "}",
+
+ // Configuration 8
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"lease-database\": {"
+ " \"type\": \"mysql\","
+ " \"name\": \"keatest\","
+ " \"user\": \"keatest\","
+ " \"password\": \"keatest\""
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ]"
+ " } ]"
+ "}",
+
+ // Configuration 9
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"lease-database\": {"
+ " \"type\": \"postgresql\","
+ " \"name\": \"keatest\","
+ " \"user\": \"keatest\","
+ " \"password\": \"keatest\""
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ]"
+ " } ]"
+ "}",
+
+ // Configuration 10
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"host-reservation-identifiers\": [ \"circuit-id\" ],"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1, "
+ " \"pools\": [ { \"pool\": \"10.0.0.5-10.0.0.100\" } ],"
+ " \"reservations\": [ "
+ " {"
+ " \"circuit-id\": \"'charter950'\","
+ " \"ip-address\": \"10.0.0.9\""
+ " }"
+ " ]"
+ "} ]"
+ "}",
+
+ // Configuration 11
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\","
+ " \"id\": 1, "
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"reservations-global\": false,"
+ " \"reservations-in-subnet\": true,"
+ " \"reservations-out-of-pool\": false,"
+ " \"reservations\": [ "
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"ip-address\": \"10.0.0.65\""
+ " }"
+ " ]"
+ "} ]"
+ "}",
+
+ // Configuration 12
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\","
+ " \"id\": 1, "
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"reservations-global\": false,"
+ " \"reservations-in-subnet\": false,"
+ " \"reservations\": [ "
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"ip-address\": \"10.0.0.65\""
+ " }"
+ " ]"
+ "} ]"
+ "}",
+
+ // Configuration 13
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\","
+ " \"id\": 1, "
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"reservations-global\": false,"
+ " \"reservations-in-subnet\": true,"
+ " \"reservations-out-of-pool\": true,"
+ " \"reservations\": [ "
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"ip-address\": \"10.0.0.200\""
+ " },"
+ " {"
+ " \"hw-address\": \"11:22:33:44:55:66\","
+ " \"ip-address\": \"10.0.0.65\""
+ " }"
+ " ]"
+ "} ]"
+ "}",
+
+ // Configuration 14
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"authoritative\": true,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"option-data\": [ {"
+ " \"name\": \"routers\","
+ " \"data\": \"10.0.0.200,10.0.0.201\""
+ " } ]"
+ " } ]"
+ "}",
+
+ // Configuration 15
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"authoritative\": false,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"option-data\": [ {"
+ " \"name\": \"routers\","
+ " \"data\": \"10.0.0.200,10.0.0.201\""
+ " } ]"
+ " } ]"
+ "}",
+
+ // Configuration 16
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": ["
+ " {"
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1, "
+ " \"store-extended-info\": true,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"interface\": \"eth0\""
+ " },"
+ " {"
+ " \"subnet\": \"192.0.2.0/26\", "
+ " \"id\": 2, "
+ " \"store-extended-info\": false,"
+ " \"pools\": [ { \"pool\": \"192.0.2.10-192.0.2.63\" } ],"
+ " \"interface\": \"eth1\""
+ " }"
+ "]"
+ "}",
+
+ // Configuration 17
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"allocator\": \"random\","
+ "\"subnet4\": ["
+ " {"
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1, "
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"10.0.0.10-10.0.0.20\""
+ " },"
+ " {"
+ " \"pool\": \"10.0.0.30-10.0.0.40\""
+ " },"
+ " {"
+ " \"pool\": \"10.0.0.50-10.0.0.60\""
+ " }"
+ " ],"
+ " \"interface\": \"eth0\""
+ " }"
+ "]"
+ "}",
+
+ // Configuration 18
+ R"({
+ "cache-max-age": 600,
+ "cache-threshold": .50,
+ "interfaces-config": {
+ "interfaces": [ "*" ]
+ },
+ "subnet4": [
+ {
+ "id": 1,
+ "pools": [
+ {
+ "pool": "10.0.0.10 - 10.0.0.20"
+ },
+ ],
+ "subnet": "10.0.0.0/24"
+ },
+ {
+ "id": 2,
+ "subnet": "192.168.0.0/24"
+ }
+ ],
+ "valid-lifetime": 600
+ })",
+
+ // Configuration 19
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"authoritative\": false,"
+ "\"subnet4\": []"
+ "}",
+
+ // Configuration 20
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"authoritative\": true,"
+ "\"subnet4\": []"
+ "}",
+};
+
+/// @brief Test fixture class for testing 4-way (DORA) exchanges.
+///
+/// @todo Currently there is a limit number of test cases covered here.
+/// In the future it is planned that the tests from the
+/// dhcp4_srv_unittest.cc will be migrated here and will use the
+/// @c Dhcp4Client class.
+class DORATest : public Dhcpv4SrvTest {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Sets up fake interfaces.
+ DORATest()
+ : Dhcpv4SrvTest(),
+ iface_mgr_test_config_(true) {
+ // Let's wipe all existing statistics.
+ isc::stats::StatsMgr::instance().removeAll();
+ }
+
+ /// @brief Destructor.
+ ///
+ /// Cleans up statistics after the test.
+ ~DORATest() {
+ // Let's wipe all existing statistics.
+ isc::stats::StatsMgr::instance().removeAll();
+ }
+
+ /// @brief Test that server returns the same lease for the client which is
+ /// sometimes using client identifier, sometimes not.
+ ///
+ /// This test checks the server's behavior in the following scenario:
+ /// - Client identifies itself to the server using HW address, and may use
+ /// client identifier.
+ /// - Client performs the 4-way exchange and obtains a lease from the server.
+ /// - If the client identifier was in use when the client has acquired the lease,
+ /// the client uses null client identifier in the next exchange with the server.
+ /// - If the client identifier was not in use when the client has acquired the
+ /// lease, the client uses client identifier in the next exchange with the
+ /// server.
+ /// - When the client contacts the server for the second time using the
+ /// DHCPDISCOVER the server determines (using HW address) that the client
+ /// already has a lease and returns this lease to the client.
+ /// - The client renews the existing lease.
+ ///
+ /// @param clientid_a Client identifier when the client initially allocates
+ /// the lease. An empty value means "no client identifier".
+ /// @param clientid_b Client identifier when the client sends the DHCPDISCOVER
+ /// and then DHCPREQUEST to renew lease.
+ void oneAllocationOverlapTest(const std::string& clientid_a,
+ const std::string& clientid_b);
+
+ /// @brief Test that the client using the same hardware address but
+ /// multiple client identifiers will obtain multiple leases.
+ ///
+ /// This reflects the scenario of the OS installation over the network
+ /// when BIOS, installer and the host request DHCPv4 lease assignment
+ /// using the same MAC address/interface but generating different
+ /// client identifiers.
+ ///
+ /// @param config_index Index of the configuration within the
+ /// @c DORA_CONFIGS array.
+ void testMultiStageBoot(const unsigned int config_index);
+
+ /// @brief This test verifies that the client in the SELECTING state can get
+ /// an address when it doesn't request any specific address in the
+ /// DHCPDISCOVER message.
+ void selectingDoNotRequestAddress();
+
+ /// @brief This test verifies that multiple clients may use the DHCPv4
+ /// server and obtain unique leases.
+ void selectingMultipleClients();
+
+ /// @brief This test verifies that the client in a SELECTING state can
+ /// request a specific address and that this address will be assigned when
+ /// available. It also tests that if the client requests an address which is
+ /// in use the client will get a different address.
+ void selectingRequestAddress();
+
+ /// @brief This test verifies that the client will get the address that it
+ /// has been allocated when the client requests a different address.
+ void selectingRequestNonMatchingAddress();
+
+ /// @brief Test that the client in the INIT-REBOOT state can request the IP
+ /// address it has and the address is returned. Also, check that if the
+ /// client requests invalid address the server sends a DHCPNAK.
+ void initRebootRequest();
+
+ /// @brief Test that the client in the INIT-REBOOT state can request the IP
+ /// address it has and the address is returned. Also, check that if the
+ /// client is unknown the server sends a DHCPNAK.
+ void authoritative();
+
+ /// @brief Test that the client in the INIT-REBOOT state can request the IP
+ /// address it has and the address is returned. Also, check that if the
+ /// client is unknown the request is dropped.
+ void notAuthoritative();
+
+ /// @brief Test that the authoritative server sends a DHCPNAK to the client
+ /// in the INIT-REBOOT state when subnet selection fails.
+ void authoritativeSubnetSelectionFail();
+
+ /// @brief Test that the server does not respond to the client in the
+ /// INIT-REBOOT state when subnet selection fails.
+ void notAuthoritativeSubnetSelectionFail();
+
+ /// @brief Check that the ciaddr returned by the server is correct for
+ /// DHCPOFFER and DHCPNAK according to RFC2131, section 4.3.1.
+ void ciaddr();
+
+ /// @brief This test checks the server behavior in the following situation:
+ /// - Client A identifies itself to the server using client identifier
+ /// and the hardware address and requests allocation of the new lease.
+ /// - Server allocates the lease to the client.
+ /// - Client B has the same hardware address but is using a different
+ /// client identifier then Client A.
+ /// - Client B sends DHCPDISCOVER.
+ /// - Server should determine that the client B is not client A, because
+ /// it is using a different client identifier, even though they use the
+ /// same HW address. As a consequence, the server should offer a
+ /// different address to the client B.
+ /// - The client B performs the 4-way exchange again and the server
+ /// allocates a new address to the client, which should be different
+ /// than the address used by the client A.
+ /// - Client B is in the renewing state and it successfully renews its
+ /// address.
+ /// - Client A also renews its address successfully.
+ void twoAllocationsOverlap();
+
+ /// @brief This is a simple test for the host reservation. It creates a
+ /// reservation for an address for a single client, identified by the HW
+ /// address. The test verifies that the client using this HW address will
+ /// obtain a lease for the reserved address. It also checks that the client
+ /// using a different HW address will obtain an address from the dynamic
+ /// pool.
+ void reservation();
+
+ /// @brief This test checks that it is possible to make a reservation by
+ /// DUID carried in the Client Identifier option.
+ void reservationByDUID();
+
+ /// @brief This test checks that it is possible to make a reservation by
+ /// circuit-id inserted by the relay agent.
+ void reservationByCircuitId();
+
+ /// @brief This test checks that it is possible to make a reservation by
+ /// client-id.
+ void reservationByClientId();
+
+ /// @brief This test verifies that order in which host identifiers are used
+ /// to retrieve host reservations can be controlled.
+ void hostIdentifiersOrder();
+
+ /// @brief This test checks that setting the match-client-id value to false
+ /// causes the server to ignore changing client identifier when the client
+ /// is using consistent HW address.
+ void ignoreChangingClientId();
+
+ /// @brief This test checks that the match-client-id parameter doesn't have
+ /// effect on the lease lookup using the HW address.
+ void changingHWAddress();
+
+ /// @brief This test verifies that the server assigns reserved values for
+ /// the siaddr, sname and file fields carried within DHCPv4 message.
+ void messageFieldsReservations();
+
+ /// @brief This test checks the following scenario:
+ /// 1. Client A performs 4-way exchange and obtains a lease from the dynamic
+ /// pool.
+ /// 2. Reservation is created for the client A using an address out of the
+ /// dynamic pool.
+ /// 3. Client A renews the lease.
+ /// 4. Server responds with DHCPNAK to indicate that the client should stop
+ /// using an address for which it has a lease. Server doesn't want to
+ /// renew an address for which the client doesn't have a reservation,
+ /// while it has a reservation for a different address.
+ /// 5. Client A receives a DHCPNAK and returns to the DHCP server discovery.
+ /// 6. Client A performs a 4-way exchange with a server and the server
+ /// allocates a reserved address to the Client A.
+ /// 7. Client A renews the allocated address and the server returns a
+ /// DHCPACK.
+ /// 8. Reservation for the Client A is removed.
+ /// 9. Client A renews the (previously reserved) lease and the server
+ /// returns DHCPNAK because the address in use is neither reserved nor
+ /// belongs to the dynamic pool.
+ /// 10. Client A returns to the DHCP server discovery.
+ /// 11. Client A uses 4-way exchange to obtain a lease from the dynamic
+ /// pool.
+ /// 12. The new address that the Client A is using is reserved for Client B.
+ /// Client A still holds this address.
+ /// 13. Client B uses 4-way exchange to obtain a new lease.
+ /// 14. The server determines that the Client B has a reservation for the
+ /// address which is in use by Client A and offers an address different
+ /// than reserved.
+ /// 15. Client B requests the allocation of the offered address and the
+ /// server allocates this address.
+ /// 16. Client A renews the lease.
+ /// 17. The server determines that the address that Client A is using is
+ /// reserved for Client B. The server returns DHCPNAK to the Client A.
+ /// 18. Client B uses 4-way exchange to obtain the reserved lease but the
+ /// lease for the Client A hasn't been removed yet. Client B is assigned
+ /// the same address it has been using.
+ /// 19. Client A uses 4-way exchange to allocate a new lease.
+ /// 20. The server allocates a new lease from the dynamic pool but it avoids
+ /// allocating the address reserved for the Client B.
+ /// 21. Client B uses 4-way exchange to obtain a new lease.
+ /// 22. The server finally allocates a reserved address to the Client B.
+ void reservationsWithConflicts();
+
+ /// @brief This test verifies that the allocation engine ignores
+ /// reservations when reservations flags are set to "disabled".
+ void reservationModeDisabled();
+
+ /// @brief This test verifies that allocation engine assigns a reserved
+ /// address to the client which doesn't own this reservation. We want to
+ /// avoid such cases in the real deployments, but this is just a test that
+ /// the allocation engine skips checking if the reservation exists when it
+ /// allocates an address. In the real deployment the reservation simply
+ /// wouldn't exist.
+ void reservationIgnoredInDisabledMode();
+
+ /// @brief This test verifies that in pool reservations are ignored when the
+ /// reservations-out-of-pool flag is set to true.
+ void reservationModeOutOfPool();
+
+ /// @brief This test verifies that the in-pool reservation can be assigned
+ /// to a client not owning this reservation when the
+ /// reservations-out-of-pool flag is set to true.
+ void reservationIgnoredInOutOfPoolMode();
+
+ /// @brief This test verifies that after a client completes its DORA
+ /// exchange, appropriate statistics are updated.
+ void statisticsDORA();
+
+ /// @brief This test verifies that after a client completes an exchange that
+ /// result in NAK, appropriate statistics are updated.
+ void statisticsNAK();
+
+ /// @brief This test verifies that custom server identifier can be specified
+ /// for a subnet.
+ void customServerIdentifier();
+
+ /// @brief This test verifies that reserved lease is not assigned to a
+ /// client which identifier doesn't match the identifier in the reservation.
+ void changingCircuitId();
+
+ /// @brief Verifies that extended info is stored on the lease when
+ /// store-extended-info is enabled.
+ void storeExtendedInfoEnabled();
+
+ /// @brief Verifies that extended info is not stored on the lease when
+ /// store-extended-info is disabled.
+ void storeExtendedInfoDisabled();
+
+ /// @brief This test verifies that random allocator is used according
+ /// to the configuration and it allocates random addresses.
+ void randomAllocation();
+
+ /// @brief Checks that features related to lease caching (such as lease reuse statistics) work.
+ void leaseCaching();
+
+ /// @brief Checks the value of a statistic.
+ ///
+ /// @param name name of statistic to check
+ /// @param expected_size expected number of statistic samples
+ /// @param expected_value expected value of the latest statistic sample
+ void checkStat(string const& name,
+ size_t const expected_size,
+ int64_t const expected_value);
+
+ /// @brief Interface Manager's fake configuration control.
+ IfaceMgrTestConfig iface_mgr_test_config_;
+};
+
+void
+DORATest::selectingDoNotRequestAddress() {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Configure DHCP server.
+ configure(DORA_CONFIGS[0], *client.getServer());
+
+ // Perform 4-way exchange with the server but to not request any
+ // specific address in the DHCPDISCOVER message.
+ ASSERT_NO_THROW(client.doDORA());
+
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Response must not be relayed.
+ EXPECT_FALSE(resp->isRelayed());
+ // Make sure that the server id is present.
+ EXPECT_EQ("10.0.0.1", client.config_.serverid_.toText());
+ // Make sure that the client has got the lease with the requested address.
+ ASSERT_NE(client.config_.lease_.addr_.toText(), "0.0.0.0");
+}
+
+TEST_F(DORATest, selectingDoNotRequestAddress) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ selectingDoNotRequestAddress();
+}
+
+TEST_F(DORATest, selectingDoNotRequestAddressMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ selectingDoNotRequestAddress();
+}
+
+void
+DORATest::selectingMultipleClients() {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Configure DHCP server.
+ configure(DORA_CONFIGS[0], *client.getServer());
+
+ // Get the first lease.
+ ASSERT_NO_THROW(client.doDORA());
+
+ // Make sure that the server responded.
+ Pkt4Ptr resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Store the lease.
+ Lease4 lease1 = client.config_.lease_;
+
+ // Get the lease for a different client.
+ client.modifyHWAddr();
+ ASSERT_NO_THROW(client.doDORA());
+ // Make sure that the server responded.
+ resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Store the lease.
+ Lease4 lease2 = client.config_.lease_;
+
+ // Get the lease for a different client.
+ client.modifyHWAddr();
+ ASSERT_NO_THROW(client.doDORA());
+ // Make sure that the server responded.
+ resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Store the lease.
+ Lease4 lease3 = client.config_.lease_;
+
+ // Make sure that unique addresses have been assigned.
+ EXPECT_NE(lease1.addr_, lease2.addr_);
+ EXPECT_NE(lease2.addr_, lease3.addr_);
+ EXPECT_NE(lease1.addr_, lease3.addr_);
+}
+
+TEST_F(DORATest, selectingMultipleClients) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ selectingMultipleClients();
+}
+
+TEST_F(DORATest, selectingMultipleClientsMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ selectingMultipleClients();
+}
+
+void
+DORATest::selectingRequestAddress() {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Configure DHCP server.
+ configure(DORA_CONFIGS[0], *client.getServer());
+
+ // Perform 4-way exchange with the server.
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("10.0.0.50"))));
+
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Response must not be relayed.
+ EXPECT_FALSE(resp->isRelayed());
+ // Make sure that the server id is present.
+ EXPECT_EQ("10.0.0.1", client.config_.serverid_.toText());
+ // Make sure that the client has got the lease with the requested address.
+ ASSERT_EQ("10.0.0.50", client.config_.lease_.addr_.toText());
+
+ // Simulate different client requesting the same address.
+ client.modifyHWAddr();
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("10.0.0.50"))));
+ resp = client.getContext().response_;
+ // Make sure that the server responded.
+ ASSERT_TRUE(resp);
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Response must not be relayed.
+ EXPECT_FALSE(resp->isRelayed());
+ // Make sure that the server id is present.
+ EXPECT_EQ("10.0.0.1", client.config_.serverid_.toText());
+ // Make sure that the client has got some address.
+ EXPECT_NE(client.config_.lease_.addr_.toText(), "0.0.0.0");
+ // Make sure that the client has got a different address than requested
+ // as the requested one is already in use.
+ EXPECT_NE(client.config_.lease_.addr_.toText(), "10.0.0.50");
+}
+
+TEST_F(DORATest, selectingRequestAddress) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ selectingRequestAddress();
+}
+
+TEST_F(DORATest, selectingRequestAddressMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ selectingRequestAddress();
+}
+
+void
+DORATest::selectingRequestNonMatchingAddress() {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Configure DHCP server.
+ configure(DORA_CONFIGS[0], *client.getServer());
+
+ // Perform 4-way exchange with the server.
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("10.0.0.50"))));
+
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Response must not be relayed.
+ EXPECT_FALSE(resp->isRelayed());
+ // Make sure that the server id is present.
+ EXPECT_EQ("10.0.0.1", client.config_.serverid_.toText());
+ // Make sure that the client has got the lease with the requested address.
+ ASSERT_EQ("10.0.0.50", client.config_.lease_.addr_.toText());
+
+ // Let's request a different address. The server should respond with
+ // the one that the client already has allocated.
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("10.0.0.80"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Response must not be relayed.
+ EXPECT_FALSE(resp->isRelayed());
+ // Make sure that the server id is present.
+ EXPECT_EQ("10.0.0.1", client.config_.serverid_.toText());
+ // Make sure that the client has got the lease with the address that
+ // the client has recorded in the lease database.
+ EXPECT_EQ("10.0.0.50", client.config_.lease_.addr_.toText());
+}
+
+TEST_F(DORATest, selectingRequestNonMatchingAddress) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ selectingRequestNonMatchingAddress();
+}
+
+TEST_F(DORATest, selectingRequestNonMatchingAddressMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ selectingRequestNonMatchingAddress();
+}
+
+void
+DORATest::initRebootRequest() {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Configure DHCP server.
+ configure(DORA_CONFIGS[0], *client.getServer());
+ client.includeClientId("11:22");
+ // Obtain a lease from the server using the 4-way exchange.
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("10.0.0.50"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Response must not be relayed.
+ EXPECT_FALSE(resp->isRelayed());
+ // Make sure that the server id is present.
+ EXPECT_EQ("10.0.0.1", client.config_.serverid_.toText());
+ // Make sure that the client has got the lease with the requested address.
+ ASSERT_EQ("10.0.0.50", client.config_.lease_.addr_.toText());
+
+ // Client has a lease in the database. Let's transition the client
+ // to the INIT_REBOOT state so as the client can request the cached
+ // lease using the DHCPREQUEST message.
+ client.setState(Dhcp4Client::INIT_REBOOT);
+ ASSERT_NO_THROW(client.doRequest());
+
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Response must not be relayed.
+ EXPECT_FALSE(resp->isRelayed());
+ // Make sure that the server id is present.
+ EXPECT_EQ("10.0.0.1", client.config_.serverid_.toText());
+ // Make sure that the client has got the lease with the requested address.
+ ASSERT_EQ("10.0.0.50", client.config_.lease_.addr_.toText());
+
+ // Try to request a different address than the client has. The server
+ // should respond with DHCPNAK.
+ client.config_.lease_.addr_ = IOAddress("10.0.0.30");
+ ASSERT_NO_THROW(client.doRequest());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ EXPECT_EQ(DHCPNAK, static_cast<int>(resp->getType()));
+
+ // Change client identifier. The server should treat the request
+ // as a request from unknown client and ignore it.
+ client.includeClientId("12:34");
+ ASSERT_NO_THROW(client.doRequest());
+ ASSERT_FALSE(client.getContext().response_);
+
+ // Now let's fix the IP address. The client identifier is still
+ // invalid so the message should be dropped.
+ client.config_.lease_.addr_ = IOAddress("10.0.0.50");
+ ASSERT_NO_THROW(client.doRequest());
+ ASSERT_FALSE(client.getContext().response_);
+
+ // Restore original client identifier.
+ client.includeClientId("11:22");
+
+ // Try to request from a different HW address. This should be successful
+ // because the client identifier matches.
+ client.modifyHWAddr();
+ ASSERT_NO_THROW(client.doRequest());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Make sure that the client has got the lease with the requested address.
+ ASSERT_EQ("10.0.0.50", client.config_.lease_.addr_.toText());
+}
+
+TEST_F(DORATest, initRebootRequest) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ initRebootRequest();
+}
+
+TEST_F(DORATest, initRebootRequestMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ initRebootRequest();
+}
+
+void
+DORATest::authoritative() {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Configure DHCP server.
+ configure(DORA_CONFIGS[14], *client.getServer());
+ client.includeClientId("11:22");
+
+ // Try to renew an address that is outside the pool.
+ client.setState(Dhcp4Client::RENEWING);
+ client.ciaddr_ = IOAddress("10.0.0.9");
+ ASSERT_NO_THROW_LOG(client.doRequest());
+ // Even though we're authoritative server should not respond
+ // since it does not know this address.
+ ASSERT_FALSE(client.getContext().response_);
+
+ // Obtain a lease from the server using the 4-way exchange.
+ client.ciaddr_ = IOAddress("0.0.0.0");
+ client.setState(Dhcp4Client::SELECTING);
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("10.0.0.50"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Response must not be relayed.
+ EXPECT_FALSE(resp->isRelayed());
+ // Make sure that the server id is present.
+ EXPECT_EQ("10.0.0.1", client.config_.serverid_.toText());
+ // Make sure that the client has got the lease with the requested address.
+ ASSERT_EQ("10.0.0.50", client.config_.lease_.addr_.toText());
+
+ // Client has a lease in the database. Let's transition the client
+ // to the INIT_REBOOT state so as the client can request the cached
+ // lease using the DHCPREQUEST message.
+ client.setState(Dhcp4Client::INIT_REBOOT);
+ ASSERT_NO_THROW(client.doRequest());
+
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Response must not be relayed.
+ EXPECT_FALSE(resp->isRelayed());
+ // Make sure that the server id is present.
+ EXPECT_EQ("10.0.0.1", client.config_.serverid_.toText());
+ // Make sure that the client has got the lease with the requested address.
+ ASSERT_EQ("10.0.0.50", client.config_.lease_.addr_.toText());
+
+ // Try to request a different address than the client has. The server
+ // should respond with DHCPNAK.
+ client.config_.lease_.addr_ = IOAddress("10.0.0.30");
+ ASSERT_NO_THROW(client.doRequest());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ EXPECT_EQ(DHCPNAK, static_cast<int>(resp->getType()));
+
+ // Try to request another different address from an unknown subnet.
+ // The server should respond with DHCPNAK.
+ client.config_.lease_.addr_ = IOAddress("10.1.0.30");
+ ASSERT_NO_THROW(client.doRequest());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ EXPECT_EQ(DHCPNAK, static_cast<int>(resp->getType()));
+
+ // Change client identifier. The server should treat the request
+ // as a request from unknown client and respond with DHCPNAK.
+ client.includeClientId("12:34");
+ client.config_.lease_.addr_ = IOAddress("10.1.0.30");
+ ASSERT_NO_THROW(client.doRequest());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ EXPECT_EQ(DHCPNAK, static_cast<int>(resp->getType()));
+
+ // Now let's fix the IP address. The client identifier is still
+ // invalid so the server still responds with DHCPNAK.
+
+ ASSERT_NO_THROW(client.doRequest());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ EXPECT_EQ(DHCPNAK, static_cast<int>(resp->getType()));
+
+ // Restore original client identifier.
+ client.includeClientId("11:22");
+ client.config_.lease_.addr_ = IOAddress("10.0.0.50");
+
+ // Try to request from a different HW address. This should be successful
+ // because the client identifier matches.
+ client.modifyHWAddr();
+ ASSERT_NO_THROW(client.doRequest());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Make sure that the client has got the lease with the requested address.
+ ASSERT_EQ("10.0.0.50", client.config_.lease_.addr_.toText());
+}
+
+TEST_F(DORATest, authoritative) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ authoritative();
+}
+
+TEST_F(DORATest, authoritativeMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ authoritative();
+}
+
+void
+DORATest::notAuthoritative() {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Configure DHCP server.
+ configure(DORA_CONFIGS[15], *client.getServer());
+ client.includeClientId("11:22");
+
+ // Try to renew an address that is outside the pool.
+ client.setState(Dhcp4Client::RENEWING);
+ client.ciaddr_ = IOAddress("10.0.0.9");
+ ASSERT_NO_THROW_LOG(client.doRequest());
+ // We are not authoritative sure that the server does
+ // not respond at all.
+ ASSERT_FALSE(client.getContext().response_);
+
+ // Obtain a lease from the server using the 4-way exchange.
+ client.ciaddr_ = IOAddress("0.0.0.0");
+ client.setState(Dhcp4Client::SELECTING);
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("10.0.0.50"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Response must not be relayed.
+ EXPECT_FALSE(resp->isRelayed());
+ // Make sure that the server id is present.
+ EXPECT_EQ("10.0.0.1", client.config_.serverid_.toText());
+ // Make sure that the client has got the lease with the requested address.
+ ASSERT_EQ("10.0.0.50", client.config_.lease_.addr_.toText());
+
+ // Client has a lease in the database. Let's transition the client
+ // to the INIT_REBOOT state so as the client can request the cached
+ // lease using the DHCPREQUEST message.
+ client.setState(Dhcp4Client::INIT_REBOOT);
+ ASSERT_NO_THROW(client.doRequest());
+
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Response must not be relayed.
+ EXPECT_FALSE(resp->isRelayed());
+ // Make sure that the server id is present.
+ EXPECT_EQ("10.0.0.1", client.config_.serverid_.toText());
+ // Make sure that the client has got the lease with the requested address.
+ ASSERT_EQ("10.0.0.50", client.config_.lease_.addr_.toText());
+
+ // Try to request a different address than the client has. The server
+ // should respond with DHCPNAK.
+ client.config_.lease_.addr_ = IOAddress("10.0.0.30");
+ ASSERT_NO_THROW(client.doRequest());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ EXPECT_EQ(DHCPNAK, static_cast<int>(resp->getType()));
+
+ // Try to request another different address from an unknown subnet.
+ // The server should respond with DHCPNAK.
+ client.config_.lease_.addr_ = IOAddress("10.1.0.30");
+ ASSERT_NO_THROW(client.doRequest());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ ASSERT_EQ(DHCPNAK, static_cast<int>(resp->getType()));
+
+ // Change client identifier. The server should treat the request
+ // as a request from unknown client and not respond (no DHCPNAK).
+ // Changed behavior vs authoritative!
+ client.includeClientId("12:34");
+ client.config_.lease_.addr_ = IOAddress("10.1.0.30");
+ ASSERT_NO_THROW(client.doRequest());
+ // Make sure that the server did not respond.
+ EXPECT_FALSE(client.getContext().response_);
+
+ // Now let's fix the IP address. The client identifier is still
+ // invalid so the message should be dropped (no DHCPNAK).
+ // Changed behavior vs authoritative!
+ client.config_.lease_.addr_ = IOAddress("10.0.0.50");
+ ASSERT_NO_THROW(client.doRequest());
+ // Make sure that the server did not respond.
+ EXPECT_FALSE(client.getContext().response_);
+
+ // Restore original client identifier.
+ client.includeClientId("11:22");
+ client.config_.lease_.addr_ = IOAddress("10.0.0.50");
+
+ // Try to request from a different HW address. This should be successful
+ // because the client identifier matches.
+ client.modifyHWAddr();
+ ASSERT_NO_THROW(client.doRequest());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Make sure that the client has got the lease with the requested address.
+ ASSERT_EQ("10.0.0.50", client.config_.lease_.addr_.toText());
+}
+
+TEST_F(DORATest, notAuthoritative) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ notAuthoritative();
+}
+
+TEST_F(DORATest, notAuthoritativeMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ notAuthoritative();
+}
+
+void
+DORATest::authoritativeSubnetSelectionFail() {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Configure DHCP server.
+ configure(DORA_CONFIGS[15], *client.getServer());
+ client.includeClientId("11:22");
+ client.useRelay(true, IOAddress("10.0.0.1"), IOAddress("10.0.0.2"));
+
+ // Current configuration contains a matching subnet from which
+ // the client should get a lease.
+ client.doDORA();
+
+ // Let's now reconfigure the server to remove the subnet. The
+ // global authoritative flag is true.
+ configure(DORA_CONFIGS[20], *client.getServer());
+
+ // Simulate that the client is in the INIT-REBOOT state. The
+ // client will request the previously assigned address and
+ // remove the server-id.
+ client.setState(Dhcp4Client::INIT_REBOOT);
+ ASSERT_NO_THROW_LOG(client.doRequest());
+
+ // The server should respond because it is authoritative.
+ auto resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ EXPECT_EQ(DHCPNAK, static_cast<int>(resp->getType()));
+}
+
+TEST_F(DORATest, authoritativeSubnetSelectionFail) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ authoritativeSubnetSelectionFail();
+}
+
+TEST_F(DORATest, authoritativeSubnetSelectionFailMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ authoritativeSubnetSelectionFail();
+}
+
+void
+DORATest::notAuthoritativeSubnetSelectionFail() {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Configure DHCP server.
+ configure(DORA_CONFIGS[15], *client.getServer());
+ client.includeClientId("11:22");
+ client.useRelay(true, IOAddress("10.0.0.1"), IOAddress("10.0.0.2"));
+
+ // Current configuration contains a matching subnet from which
+ // the client should get a lease.
+ client.doDORA();
+
+ // Let's now reconfigure the server to remove the subnet. The
+ // global authoritative flag is true.
+ configure(DORA_CONFIGS[19], *client.getServer());
+
+ // Simulate that the client is in the INIT-REBOOT state. The
+ // client will request the previously assigned address and
+ // remove the server-id.
+ client.setState(Dhcp4Client::INIT_REBOOT);
+ ASSERT_NO_THROW_LOG(client.doRequest());
+
+ // We are not authoritative so the server does not respond
+ // at all.
+ EXPECT_FALSE(client.getContext().response_);
+}
+
+TEST_F(DORATest, notAuthoritativeSubnetSelectionFail) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ notAuthoritativeSubnetSelectionFail();
+}
+
+TEST_F(DORATest, notAuthoritativeSubnetSelectionFailMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ notAuthoritativeSubnetSelectionFail();
+}
+
+void
+DORATest::ciaddr() {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Configure DHCP server.
+ configure(DORA_CONFIGS[0], *client.getServer());
+ // Force ciaddr of Discover message to be non-zero.
+ client.ciaddr_ = IOAddress("10.0.0.50");
+ // Obtain a lease from the server using the 4-way exchange.
+ ASSERT_NO_THROW(client.doDiscover(boost::shared_ptr<
+ IOAddress>(new IOAddress("10.0.0.50"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPOFFER.
+ ASSERT_EQ(DHCPOFFER, static_cast<int>(resp->getType()));
+ // Make sure ciaddr is not set for DHCPOFFER.
+ EXPECT_EQ("0.0.0.0", resp->getCiaddr().toText());
+
+ // Obtain a lease from the server using the 4-way exchange.
+ ASSERT_NO_THROW(client.doRequest());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Let's transition the client to Renewing state.
+ client.setState(Dhcp4Client::RENEWING);
+
+ // Set the unicast destination address to indicate that it is a renewal.
+ client.setDestAddress(IOAddress("10.0.0.1"));
+ ASSERT_NO_THROW(client.doRequest());
+ // The client is sending invalid ciaddr so the server should send a NAK.
+ resp = client.getContext().response_;
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // For DHCPACK the ciaddr may be 0 or may be set to the ciaddr value
+ // from the client's message. Kea sets it to the latter.
+ EXPECT_EQ("10.0.0.50", resp->getCiaddr().toText());
+
+ // Replace the address held by the client. The client will request
+ // the assignment of this address but the server has a different
+ // address for this client.
+ client.ciaddr_ = IOAddress("192.168.0.30");
+ ASSERT_NO_THROW(client.doRequest());
+ // The client is sending invalid ciaddr so the server should send a NAK.
+ resp = client.getContext().response_;
+ ASSERT_EQ(DHCPNAK, static_cast<int>(resp->getType()));
+ // For DHCPNAK the ciaddr is always 0 (should not be copied) from the
+ // client's message.
+ EXPECT_EQ("0.0.0.0", resp->getCiaddr().toText());
+}
+
+TEST_F(DORATest, ciaddr) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ ciaddr();
+}
+
+TEST_F(DORATest, ciaddrMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ ciaddr();
+}
+
+void
+DORATest::oneAllocationOverlapTest(const std::string& clientid_a,
+ const std::string& clientid_b) {
+ // Allocate a lease by client using the 4-way exchange.
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ client.includeClientId(clientid_a);
+ client.setHWAddress("01:02:03:04:05:06");
+ configure(DORA_CONFIGS[0], *client.getServer());
+ ASSERT_NO_THROW(client.doDORA());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ Lease4Ptr lease_a = LeaseMgrFactory::instance().getLease4(client.config_.lease_.addr_);
+ ASSERT_TRUE(lease_a);
+ // Remember the allocated address.
+ IOAddress yiaddr = lease_a->addr_;
+
+ // Change client identifier. If parameters clientid_a and clientid_b
+ // are specified correctly, this removes the client identifier from
+ // client's requests if the lease has been acquired with the client
+ // identifier, or adds the client identifier otherwise.
+ client.includeClientId(clientid_b);
+
+ // Check if the server will offer the same address.
+ ASSERT_NO_THROW(client.doDiscover());
+ resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ EXPECT_EQ(yiaddr, resp->getYiaddr());
+
+ // Client should also be able to renew its address.
+ client.setState(Dhcp4Client::RENEWING);
+ ASSERT_NO_THROW(client.doRequest());
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ ASSERT_EQ(yiaddr, client.config_.lease_.addr_);
+}
+
+void
+DORATest::twoAllocationsOverlap() {
+ // Allocate a lease by client A using the 4-way exchange.
+ Dhcp4Client client_a(Dhcp4Client::SELECTING);
+ client_a.includeClientId("12:34");
+ client_a.setHWAddress("01:02:03:04:05:06");
+ configure(DORA_CONFIGS[0], *client_a.getServer());
+ ASSERT_NO_THROW(client_a.doDORA());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client_a.getContext().response_);
+ Pkt4Ptr resp_a = client_a.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp_a->getType()));
+
+ // Make sure that the lease has been recorded by the server.
+ Lease4Ptr lease_a = LeaseMgrFactory::instance().getLease4(client_a.config_.lease_.addr_);
+ ASSERT_TRUE(lease_a);
+
+ // Create client B.
+ Dhcp4Client client_b(client_a.getServer(), Dhcp4Client::SELECTING);
+ client_b.setHWAddress("01:02:03:04:05:06");
+ client_b.includeClientId("45:67");
+ // Send DHCPDISCOVER and expect the response.
+ ASSERT_NO_THROW(client_b.doDiscover());
+ Pkt4Ptr resp_b = client_b.getContext().response_;
+ // Make sure that the server has responded with DHCPOFFER.
+ ASSERT_EQ(DHCPOFFER, static_cast<int>(resp_b->getType()));
+ // The offered address should be different than the address which
+ // was obtained by the client A.
+ ASSERT_NE(resp_b->getYiaddr(), client_a.config_.lease_.addr_);
+
+ // Make sure that the client A lease hasn't been modified.
+ lease_a = LeaseMgrFactory::instance().getLease4(client_a.config_.lease_.addr_);
+ ASSERT_TRUE(lease_a);
+
+ // Now that we know that the server will avoid assigning the same
+ // address that the client A has, use the 4-way exchange to actually
+ // allocate some address.
+ ASSERT_NO_THROW(client_b.doDORA());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client_b.getContext().response_);
+ resp_b = client_b.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp_b->getType()));
+ // Again, make sure the assigned addresses are different.
+ ASSERT_NE(client_b.config_.lease_.addr_, client_a.config_.lease_.addr_);
+
+ // Make sure that the client A still has a lease.
+ lease_a = LeaseMgrFactory::instance().getLease4(client_a.config_.lease_.addr_);
+ ASSERT_TRUE(lease_a);
+
+ // Make sure that the client B has a lease.
+ Lease4Ptr lease_b = LeaseMgrFactory::instance().getLease4(client_b.config_.lease_.addr_);
+ ASSERT_TRUE(lease_b);
+
+ // Client B should be able to renew its address.
+ client_b.setState(Dhcp4Client::RENEWING);
+ ASSERT_NO_THROW(client_b.doRequest());
+ ASSERT_TRUE(client_b.getContext().response_);
+ resp_b = client_b.getContext().response_;
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp_b->getType()));
+ ASSERT_NE(client_b.config_.lease_.addr_, client_a.config_.lease_.addr_);
+
+ // Client A should also be able to renew its address.
+ client_a.setState(Dhcp4Client::RENEWING);
+ ASSERT_NO_THROW(client_a.doRequest());
+ ASSERT_TRUE(client_a.getContext().response_);
+ resp_b = client_a.getContext().response_;
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp_b->getType()));
+ ASSERT_NE(client_a.config_.lease_.addr_, client_b.config_.lease_.addr_);
+}
+
+TEST_F(DORATest, twoAllocationsOverlap) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ twoAllocationsOverlap();
+}
+
+TEST_F(DORATest, twoAllocationsOverlapMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ twoAllocationsOverlap();
+}
+
+// This test checks the server behavior in the following situation:
+// - Client A identifies itself to the server using the hardware address
+// and client identifier.
+// - Client A performs the 4-way exchange and obtains a lease from the server.
+// - Client B uses the same HW address as the client A, but it doesn't use
+// the client identifier.
+// - Client B sends the DHCPDISCOVER to the server.
+// The server determines that there is a lease for the client A using the
+// same HW address as the client B. Server discards the client's message and
+// doesn't offer the lease for the client B to prevent allocation of the
+// lease without a unique identifier.
+// - The client sends the DHCPREQUEST and the server sends the DHCPNAK for the
+// same reason.
+// - Client A renews its address successfully.
+TEST_F(DORATest, oneAllocationOverlap1) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ oneAllocationOverlapTest("12:34", "");
+}
+
+// This test checks the server behavior in the following situation:
+// - Client A identifies itself to the server using the hardware address
+// and client identifier.
+// - Client A performs the 4-way exchange and obtains a lease from the server.
+// - Client B uses the same HW address as the client A, but it doesn't use
+// the client identifier.
+// - Client B sends the DHCPDISCOVER to the server.
+// The server determines that there is a lease for the client A using the
+// same HW address as the client B. Server discards the client's message and
+// doesn't offer the lease for the client B to prevent allocation of the
+// lease without a unique identifier.
+// - The client sends the DHCPREQUEST and the server sends the DHCPNAK for the
+// same reason.
+// - Client A renews its address successfully.
+TEST_F(DORATest, oneAllocationOverlap1MultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ oneAllocationOverlapTest("12:34", "");
+}
+
+// This test is similar to oneAllocationOverlap2 but this time the client A
+// uses no client identifier, and the client B uses the HW address and the
+// client identifier. The server behaves as previously.
+TEST_F(DORATest, oneAllocationOverlap2) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ oneAllocationOverlapTest("", "12:34");
+}
+
+// This test is similar to oneAllocationOverlap2 but this time the client A
+// uses no client identifier, and the client B uses the HW address and the
+// client identifier. The server behaves as previously.
+TEST_F(DORATest, oneAllocationOverlap2MultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ oneAllocationOverlapTest("", "12:34");
+}
+
+void
+DORATest::reservation() {
+ // Client A is a one which will have a reservation.
+ Dhcp4Client clientA(Dhcp4Client::SELECTING);
+ // Set explicit HW address so as it matches the reservation in the
+ // configuration used below.
+ clientA.setHWAddress("aa:bb:cc:dd:ee:ff");
+ // Configure DHCP server.
+ configure(DORA_CONFIGS[2], *clientA.getServer());
+ // Client A performs 4-way exchange and should obtain a reserved
+ // address.
+ ASSERT_NO_THROW(clientA.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("0.0.0.0"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(clientA.getContext().response_);
+ Pkt4Ptr resp = clientA.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Make sure that the client has got the lease for the reserved address.
+ ASSERT_EQ("10.0.0.7", clientA.config_.lease_.addr_.toText());
+
+ // Client B uses the same server as Client A.
+ Dhcp4Client clientB(clientA.getServer(), Dhcp4Client::SELECTING);
+ // Client B has no reservation so it should get the lease from
+ // the dynamic pool.
+ ASSERT_NO_THROW(clientB.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("0.0.0.0"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(clientB.getContext().response_);
+ resp = clientB.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Obtain the subnet to which the returned address belongs.
+ Subnet4Ptr subnet = CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->
+ selectSubnet(clientB.config_.lease_.addr_);
+ ASSERT_TRUE(subnet);
+ // Make sure that the address has been allocated from the dynamic pool.
+ ASSERT_TRUE(subnet->inPool(Lease::TYPE_V4, clientB.config_.lease_.addr_));
+}
+
+TEST_F(DORATest, reservation) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ reservation();
+}
+
+TEST_F(DORATest, reservationMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ reservation();
+}
+
+void
+DORATest:: reservationByDUID() {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Use relay agent.
+ client.useRelay(true, IOAddress("10.0.0.1"), IOAddress("10.0.0.2"));
+ // Modify HW address so as the server doesn't assign reserved
+ // address by HW address.
+ client.modifyHWAddr();
+ // Specify DUID for which address 10.0.0.8 is reserved.
+ // The value specified as client id includes:
+ // - FF is a client identifier type for DUID,
+ // - 45454545 - represents 4 bytes for IAID
+ // - 01:02:03:04:05 - is an actual DUID for which there is a
+ // reservation.
+ client.includeClientId("FF:45:45:45:45:01:02:03:04:05");
+
+ // Configure DHCP server.
+ configure(DORA_CONFIGS[2], *client.getServer());
+ // Client A performs 4-way exchange and should obtain a reserved
+ // address.
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("0.0.0.0"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Make sure that the client has got the lease for the reserved address.
+ ASSERT_EQ("10.0.0.8", client.config_.lease_.addr_.toText());
+}
+
+TEST_F(DORATest, reservationByDUID) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ reservationByDUID();
+}
+
+TEST_F(DORATest, reservationByDUIDMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ reservationByDUID();
+}
+
+void
+DORATest::reservationByCircuitId() {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Use relay agent so as the circuit-id can be inserted.
+ client.useRelay(true, IOAddress("10.0.0.1"), IOAddress("10.0.0.2"));
+ // Specify circuit-id.
+ client.setCircuitId("charter950");
+
+ // Configure DHCP server.
+ configure(DORA_CONFIGS[2], *client.getServer());
+ // Client A performs 4-way exchange and should obtain a reserved
+ // address.
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("0.0.0.0"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Make sure that the client has got the lease for the reserved address.
+ ASSERT_EQ("10.0.0.9", client.config_.lease_.addr_.toText());
+}
+
+TEST_F(DORATest, reservationByCircuitId) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ reservationByCircuitId();
+}
+
+TEST_F(DORATest, reservationByCircuitIdMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ reservationByCircuitId();
+}
+
+void
+DORATest::reservationByClientId() {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Use relay agent to make sure that the desired subnet is
+ // selected for our client.
+ client.useRelay(true, IOAddress("10.0.0.20"), IOAddress("10.0.0.21"));
+ // Specify client identifier.
+ client.includeClientId("01:11:22:33:44:55:66");
+
+ // Configure DHCP server.
+ configure(DORA_CONFIGS[2], *client.getServer());
+ // Client A performs 4-way exchange and should obtain a reserved
+ // address.
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("0.0.0.0"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Make sure that the client has got the lease for the reserved address.
+ ASSERT_EQ("10.0.0.1", client.config_.lease_.addr_.toText());
+}
+
+TEST_F(DORATest, reservationByClientId) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ reservationByClientId();
+}
+
+TEST_F(DORATest, reservationByClientIdMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ reservationByClientId();
+}
+
+void
+DORATest::hostIdentifiersOrder() {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+ // Use relay agent so as the circuit-id can be inserted.
+ client.useRelay(true, IOAddress("10.0.0.1"), IOAddress("10.0.0.2"));
+ // Specify DUID for which address 10.0.0.8 is reserved.
+ // The value specified as client id includes:
+ // - FF is a client identifier type for DUID,
+ // - 45454545 - represents 4 bytes for IAID
+ // - 01:02:03:04:05 - is an actual DUID for which there is a
+ // reservation.
+ client.includeClientId("FF:45:45:45:45:01:02:03:04:05");
+ // Specify circuit-id.
+ client.setCircuitId("charter950");
+
+ // Configure DHCP server.
+ configure(DORA_CONFIGS[2], *client.getServer());
+ // Perform 4-way exchange to obtain reserved address.
+ // The client has in fact two reserved addresses, but the one assigned
+ // should be by hw-address.
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("0.0.0.0"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Make sure that the client has got the lease for the reserved address.
+ ASSERT_EQ("10.0.0.7", client.config_.lease_.addr_.toText());
+
+ // Reconfigure the server to change the preference order of the
+ // host identifiers. The 'circuit-id' should now take precedence over
+ // the hw-address, duid and client-id.
+ configure(DORA_CONFIGS[4], *client.getServer());
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("0.0.0.0"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Make sure that the client has got the lease for the reserved address.
+ ASSERT_EQ("10.0.0.9", client.config_.lease_.addr_.toText());
+
+ // Reconfigure the server to change the preference order of the
+ // host identifiers. The 'duid' should now take precedence over
+ // the client-id, hw-address and circuit-id
+ configure(DORA_CONFIGS[5], *client.getServer());
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("0.0.0.0"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Make sure that the client has got the lease for the reserved address.
+ ASSERT_EQ("10.0.0.8", client.config_.lease_.addr_.toText());
+
+ // Replace the client identifier with the one for which address
+ // 10.0.0.1 is reserved. Because the DUID is a special type of
+ // client identifier, this change effectively removes the association
+ // of the client with the DUID for which address 10.0.0.8 is reserved.
+ // The next identifier type to be used by the server (after DUID) is
+ // client-id and thus the server should assign address 10.0.0.1.
+ client.includeClientId("01:11:22:33:44:55:66");
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("0.0.0.0"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Make sure that the client has got the lease for the reserved address.
+ ASSERT_EQ("10.0.0.1", client.config_.lease_.addr_.toText());
+}
+
+TEST_F(DORATest, hostIdentifiersOrder) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ hostIdentifiersOrder();
+}
+
+TEST_F(DORATest, hostIdentifiersOrderMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ hostIdentifiersOrder();
+}
+
+void
+DORATest::ignoreChangingClientId() {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Configure DHCP server.
+ configure(DORA_CONFIGS[3], *client.getServer());
+ client.includeClientId("12:12");
+ // Obtain the lease using 4-way exchange.
+ ASSERT_NO_THROW(client.doDORA());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ EXPECT_FALSE(client.config_.lease_.client_id_);
+
+ // Remember address which the client has obtained.
+ IOAddress leased_address = client.config_.lease_.addr_;
+
+ // Modify client id. Because we have set the configuration flag which
+ // forces the server to lookup leases using the HW address, the
+ // client id modification should not matter and the client should
+ // obtain the same lease.
+ client.includeClientId("14:14");
+ // Obtain the lease using 4-way exchange.
+ ASSERT_NO_THROW(client.doDORA());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Make sure that the server assigned the same address, even though the
+ // client id has changed.
+ EXPECT_EQ(leased_address, client.config_.lease_.addr_);
+ // Check that the client id is not present in the lease.
+ EXPECT_FALSE(client.config_.lease_.client_id_);
+}
+
+TEST_F(DORATest, ignoreChangingClientId) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ ignoreChangingClientId();
+}
+
+TEST_F(DORATest, ignoreChangingClientIdMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ ignoreChangingClientId();
+}
+
+void
+DORATest::changingHWAddress() {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Configure DHCP server.
+ configure(DORA_CONFIGS[3], *client.getServer());
+ client.includeClientId("12:12");
+ client.setHWAddress("00:01:02:03:04:05");
+ // Obtain the lease using 4-way exchange.
+ ASSERT_NO_THROW(client.doDORA());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Check that the client id is not present in the lease.
+ EXPECT_FALSE(client.config_.lease_.client_id_);
+
+ // Remember address which the client has obtained.
+ IOAddress leased_address = client.config_.lease_.addr_;
+
+ // Modify HW address but leave client id in place. The value of the
+ // match-client-id set to false must not have any effect on the
+ // case when the HW address is changing. In such case the server will
+ // allocate the new address for the client.
+ client.setHWAddress("01:01:01:01:01:01");
+ // Obtain a lease.
+ ASSERT_NO_THROW(client.doDORA());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Client must assign different address because the client id is
+ // ignored and the HW address was changed.
+ EXPECT_NE(client.config_.lease_.addr_, leased_address);
+ // Check that the client id is not present in the lease.
+ EXPECT_FALSE(client.config_.lease_.client_id_);
+}
+
+TEST_F(DORATest, changingHWAddress) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ changingHWAddress();
+}
+
+TEST_F(DORATest, changingHWAddressMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ changingHWAddress();
+}
+
+void
+DORATest::messageFieldsReservations() {
+ // Client has a reservation.
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Set explicit HW address so as it matches the reservation in the
+ // configuration used below.
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+ // Configure DHCP server.
+ configure(DORA_CONFIGS[6], *client.getServer());
+ // Client performs 4-way exchange and should obtain a reserved
+ // address and fixed fields.
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("0.0.0.0"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Check that the reserved values have been assigned.
+ EXPECT_EQ("10.0.0.7", client.config_.siaddr_.toText());
+ EXPECT_EQ("some-name.example.org", client.config_.sname_);
+ EXPECT_EQ("bootfile.efi", client.config_.boot_file_name_);
+}
+
+TEST_F(DORATest, messageFieldsReservations) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ messageFieldsReservations();
+}
+
+TEST_F(DORATest, messageFieldsReservationsMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ messageFieldsReservations();
+}
+
+void
+DORATest::reservationsWithConflicts() {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Configure DHCP server.
+ configure(DORA_CONFIGS[0], *client.getServer());
+ // Client A performs 4-way exchange and obtains a lease from the
+ // dynamic pool.
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("10.0.0.50"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Make sure that the client has got the lease with the requested address.
+ ASSERT_EQ("10.0.0.50", client.config_.lease_.addr_.toText());
+
+ configure(DORA_CONFIGS[0], false);
+ // Reservation is created for the client A using an address out of the
+ // dynamic pool.
+ HostPtr host(new Host(&client.getHWAddress()->hwaddr_[0],
+ client.getHWAddress()->hwaddr_.size(),
+ Host::IDENT_HWADDR, SubnetID(1),
+ SUBNET_ID_UNUSED, IOAddress("10.0.0.9")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ // Let's transition the client to Renewing state.
+ client.setState(Dhcp4Client::RENEWING);
+
+ // Set the unicast destination address to indicate that it is a renewal.
+ client.setDestAddress(IOAddress("10.0.0.1"));
+ ASSERT_NO_THROW(client.doRequest());
+
+ // Client should get the DHCPNAK from the server because the client has
+ // a reservation for a different address that it is trying to renew.
+ resp = client.getContext().response_;
+ ASSERT_EQ(DHCPNAK, static_cast<int>(resp->getType()));
+
+ // A conforming client would go back to the server discovery.
+ client.setState(Dhcp4Client::SELECTING);
+ // Obtain a lease from the server using the 4-way exchange.
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("0.0.0.0"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK with a reserved
+ // address
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ ASSERT_EQ("10.0.0.9", client.config_.lease_.addr_.toText());
+
+ // Client A renews the allocated address.
+ client.setState(Dhcp4Client::RENEWING);
+ // Set the unicast destination address to indicate that it is a renewal.
+ client.setDestAddress(IOAddress("10.0.0.1"));
+ ASSERT_NO_THROW(client.doRequest());
+ // Make sure the server responded and renewed the client's address.
+ resp = client.getContext().response_;
+ ASSERT_EQ("10.0.0.9", client.config_.lease_.addr_.toText());
+
+ // By reconfiguring the server, we remove the existing reservations.
+ configure(DORA_CONFIGS[0]);
+
+ // Try to renew the existing lease again.
+ ASSERT_NO_THROW(client.doRequest());
+
+ // The reservation has been removed. Since address that the client is
+ // using doesn't belong to a dynamic pool and the server is not
+ // authoritative it should not send a DHCPNAK.
+ resp = client.getContext().response_;
+ ASSERT_FALSE(resp);
+
+ // A conforming client would go back to the server discovery.
+ client.setState(Dhcp4Client::SELECTING);
+ // Obtain a lease from the server using the 4-way exchange.
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("0.0.0.0"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Obtain the subnet to which the returned address belongs.
+ Subnet4Ptr subnet = CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->
+ selectSubnet(client.config_.lease_.addr_);
+ ASSERT_TRUE(subnet);
+ // Make sure that the address has been allocated from the dynamic pool.
+ ASSERT_TRUE(subnet->inPool(Lease::TYPE_V4, client.config_.lease_.addr_));
+
+ // Remember the address allocated in the dynamic pool.
+ IOAddress in_pool_addr = client.config_.lease_.addr_;
+
+ // Create Client B.
+ Dhcp4Client clientB(client.getServer());
+ clientB.modifyHWAddr();
+
+ // Create reservation for the Client B, for the address that the
+ // Client A is using.
+ configure(DORA_CONFIGS[0], false);
+ host.reset(new Host(&clientB.getHWAddress()->hwaddr_[0],
+ clientB.getHWAddress()->hwaddr_.size(),
+ Host::IDENT_HWADDR, SubnetID(1),
+ SUBNET_ID_UNUSED, in_pool_addr));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ // Client B performs a DHCPDISCOVER.
+ clientB.setState(Dhcp4Client::SELECTING);
+ // The server determines that the address reserved for Client B is
+ // in use by Client A so it offers a different address.
+ ASSERT_NO_THROW(clientB.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("0.0.0.0"))));
+ ASSERT_TRUE(clientB.getContext().response_);
+ ASSERT_EQ(DHCPACK, static_cast<int>(clientB.getContext().response_->getType()));
+ IOAddress client_b_addr = clientB.config_.lease_.addr_;
+ ASSERT_NE(client_b_addr, in_pool_addr);
+ // Ensure stats are being recorded for HR conflicts
+ ObservationPtr subnet_conflicts = StatsMgr::instance().getObservation(
+ "subnet[1].v4-reservation-conflicts");
+ ASSERT_TRUE(subnet_conflicts);
+ ASSERT_EQ(1, subnet_conflicts->getInteger().first);
+ subnet_conflicts = StatsMgr::instance().getObservation("v4-reservation-conflicts");
+ ASSERT_TRUE(subnet_conflicts);
+ ASSERT_EQ(1, subnet_conflicts->getInteger().first);
+
+ // Client A renews the lease.
+ client.setState(Dhcp4Client::RENEWING);
+ // Set the unicast destination address to indicate that it is a renewal.
+ client.setDestAddress(IOAddress(in_pool_addr));
+ ASSERT_NO_THROW(client.doRequest());
+ // Client A should get a DHCPNAK because it is using an address reserved
+ // for Client B.
+ resp = client.getContext().response_;
+ ASSERT_EQ(DHCPNAK, static_cast<int>(resp->getType()));
+
+ // Client B performs 4-way exchange but still gets an address from the
+ // dynamic pool, because Client A hasn't obtained a new lease, so it is
+ // still using an address reserved for Client B.
+ clientB.setState(Dhcp4Client::SELECTING);
+ // Obtain a lease from the server using the 4-way exchange.
+ ASSERT_NO_THROW(clientB.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("0.0.0.0"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(clientB.getContext().response_);
+ ASSERT_EQ(DHCPACK, static_cast<int>(clientB.getContext().response_->getType()));
+ ASSERT_NE(clientB.config_.lease_.addr_, in_pool_addr);
+ ASSERT_EQ(client_b_addr, clientB.config_.lease_.addr_);
+ // Ensure stats are being recorded for HR conflicts
+ subnet_conflicts = StatsMgr::instance().getObservation(
+ "subnet[1].v4-reservation-conflicts");
+ ASSERT_TRUE(subnet_conflicts);
+ ASSERT_EQ(2, subnet_conflicts->getInteger().first);
+ subnet_conflicts = StatsMgr::instance().getObservation("v4-reservation-conflicts");
+ ASSERT_TRUE(subnet_conflicts);
+ ASSERT_EQ(2, subnet_conflicts->getInteger().first);
+
+ // Client B renews its lease.
+ clientB.setState(Dhcp4Client::RENEWING);
+ clientB.setDestAddress(IOAddress("10.0.0.1"));
+ ASSERT_NO_THROW(clientB.doRequest());
+ // The server should renew the client's B lease because the address
+ // reserved for client B is still in use by the client A.
+ ASSERT_TRUE(clientB.getContext().response_);
+ EXPECT_EQ(DHCPACK, static_cast<int>(clientB.getContext().response_->getType()));
+ ASSERT_NE(clientB.config_.lease_.addr_, in_pool_addr);
+ ASSERT_EQ(client_b_addr, clientB.config_.lease_.addr_);
+
+ // Client A performs 4-way exchange.
+ client.setState(Dhcp4Client::SELECTING);
+ // Revert to the broadcast address for the selecting client.
+ client.setDestAddress(IOAddress::IPV4_BCAST_ADDRESS());
+ // Obtain a lease from the server using the 4-way exchange.
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("0.0.0.0"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // The server should have assigned a different address than the one
+ // reserved for the Client B.
+ ASSERT_NE(client.config_.lease_.addr_, in_pool_addr);
+ subnet = CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->
+ selectSubnet(client.config_.lease_.addr_);
+ ASSERT_TRUE(subnet);
+ ASSERT_TRUE(subnet->inPool(Lease::TYPE_V4, client.config_.lease_.addr_));
+
+ // Client B renews again.
+ ASSERT_NO_THROW(clientB.doRequest());
+ // The client B should now receive the DHCPNAK from the server because
+ // the reserved address is now available and the client should
+ // revert to the DHCPDISCOVER to obtain it.
+ ASSERT_TRUE(clientB.getContext().response_);
+ EXPECT_EQ(DHCPNAK, static_cast<int>(clientB.getContext().response_->getType()));
+
+ // Client B performs 4-way exchange and obtains a lease for the
+ // reserved address.
+ clientB.setState(Dhcp4Client::SELECTING);
+ ASSERT_NO_THROW(clientB.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("0.0.0.0"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(clientB.getContext().response_);
+ resp = clientB.getContext().response_;
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ ASSERT_EQ(in_pool_addr, clientB.config_.lease_.addr_);
+}
+
+TEST_F(DORATest, reservationsWithConflicts) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ reservationsWithConflicts();
+}
+
+TEST_F(DORATest, reservationsWithConflictsMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ reservationsWithConflicts();
+}
+
+void
+DORATest::reservationModeDisabled() {
+ // Client has a reservation.
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Set explicit HW address so as it matches the reservation in the
+ // configuration used below.
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+ // Configure DHCP server. In this configuration the reservations flags are
+ // set to false. Thus, the server should ignore the reservation for
+ // this client.
+ configure(DORA_CONFIGS[12], *client.getServer());
+ // Client requests the 10.0.0.50 address and the server should assign it
+ // as it ignores the reservation in the current mode.
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("10.0.0.50"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Check that the requested IP address was assigned.
+ ASSERT_EQ("10.0.0.50", client.config_.lease_.addr_.toText());
+
+ // Reconfigure the server to respect the host reservations.
+ configure(DORA_CONFIGS[11], *client.getServer());
+
+ // The client requests the previously allocated address again, but the
+ // server should allocate the reserved address this time.
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("10.0.0.50"))));
+ // Check that the reserved IP address has been assigned.
+ ASSERT_EQ("10.0.0.65", client.config_.lease_.addr_.toText());
+}
+
+TEST_F(DORATest, reservationModeDisabled) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ reservationModeDisabled();
+}
+
+TEST_F(DORATest, reservationModeDisabledMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ reservationModeDisabled();
+}
+
+void
+DORATest::reservationIgnoredInDisabledMode() {
+ // Client has a reservation.
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Set MAC address which doesn't match the reservation configured.
+ client.setHWAddress("11:22:33:44:55:66");
+ // Configure DHCP server. In this configuration the reservations flags are
+ // disabled. Any client should be able to hijack the reserved address.
+ configure(DORA_CONFIGS[13], *client.getServer());
+ // Client requests the 10.0.0.65 address reserved for another client.
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("10.0.0.65"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Check that the address was hijacked.
+ ASSERT_EQ("10.0.0.65", client.config_.lease_.addr_.toText());
+}
+
+TEST_F(DORATest, reservationIgnoredInDisabledMode) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ reservationIgnoredInDisabledMode();
+}
+
+TEST_F(DORATest, reservationIgnoredInDisabledModeMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ reservationIgnoredInDisabledMode();
+}
+
+void
+DORATest::reservationModeOutOfPool() {
+ // Create the first client for which we have a reservation out of the
+ // dynamic pool.
+ Dhcp4Client clientA(Dhcp4Client::SELECTING);
+ clientA.setHWAddress("aa:bb:cc:dd:ee:ff");
+ // Configure the server to respect out of the pool reservations.
+ configure(DORA_CONFIGS[13], *clientA.getServer());
+ // The client for which we have a reservation is doing 4-way exchange
+ // and requests a different address than reserved. The server should
+ // allocate the reserved address to this client.
+ ASSERT_NO_THROW(clientA.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("10.0.0.40"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(clientA.getContext().response_);
+ Pkt4Ptr resp = clientA.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Check that the server allocated the reserved address.
+ ASSERT_EQ("10.0.0.200", clientA.config_.lease_.addr_.toText());
+
+ // Create another client which has a reservation within the pool.
+ // The server should ignore this reservation in the current mode.
+ Dhcp4Client clientB(clientA.getServer(), Dhcp4Client::SELECTING);
+ clientB.setHWAddress("11:22:33:44:55:66");
+ // This client is requesting a different address than reserved. The
+ // server should allocate this address to the client.
+ ASSERT_NO_THROW(clientB.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("10.0.0.40"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(clientB.getContext().response_);
+ resp = clientB.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Check that the requested address was assigned.
+ ASSERT_EQ("10.0.0.40", clientB.config_.lease_.addr_.toText());
+}
+
+TEST_F(DORATest, reservationModeOutOfPool) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ reservationModeOutOfPool();
+}
+
+TEST_F(DORATest, reservationModeOutOfPoolMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ reservationModeOutOfPool();
+}
+
+void
+DORATest::reservationIgnoredInOutOfPoolMode() {
+ // Create the first client for which we have a reservation out of the
+ // dynamic pool.
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ client.setHWAddress("12:34:56:78:9A:BC");
+ // Configure the server to respect out of the pool reservations only.
+ configure(DORA_CONFIGS[14], *client.getServer());
+ // The client which doesn't have a reservation is trying to hijack
+ // the reserved address and it should succeed.
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("10.0.0.65"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Check that the server allocated the requested address.
+ ASSERT_EQ("10.0.0.65", client.config_.lease_.addr_.toText());
+}
+
+TEST_F(DORATest, reservationIgnoredInOutOfPoolMode) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ reservationIgnoredInOutOfPoolMode();
+}
+
+TEST_F(DORATest, reservationIgnoredInOutOfPoolModeMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ reservationIgnoredInOutOfPoolMode();
+}
+
+void
+DORATest::statisticsDORA() {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Configure DHCP server.
+ configure(DORA_CONFIGS[0], *client.getServer());
+
+ // Perform 4-way exchange with the server but to not request any
+ // specific address in the DHCPDISCOVER message.
+ ASSERT_NO_THROW(client.doDORA());
+
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Ok, let's check the statistics.
+ using namespace isc::stats;
+ StatsMgr& mgr = StatsMgr::instance();
+ ObservationPtr pkt4_received = mgr.getObservation("pkt4-received");
+ ObservationPtr pkt4_discover_received = mgr.getObservation("pkt4-discover-received");
+ ObservationPtr pkt4_offer_sent = mgr.getObservation("pkt4-offer-sent");
+ ObservationPtr pkt4_request_received = mgr.getObservation("pkt4-request-received");
+ ObservationPtr pkt4_ack_sent = mgr.getObservation("pkt4-ack-sent");
+ ObservationPtr pkt4_sent = mgr.getObservation("pkt4-sent");
+
+ // All expected statistics must be present.
+ ASSERT_TRUE(pkt4_received);
+ ASSERT_TRUE(pkt4_discover_received);
+ ASSERT_TRUE(pkt4_offer_sent);
+ ASSERT_TRUE(pkt4_request_received);
+ ASSERT_TRUE(pkt4_ack_sent);
+ ASSERT_TRUE(pkt4_sent);
+
+ // They also must have expected values.
+ EXPECT_EQ(2, pkt4_received->getInteger().first);
+ EXPECT_EQ(1, pkt4_discover_received->getInteger().first);
+ EXPECT_EQ(1, pkt4_offer_sent->getInteger().first);
+ EXPECT_EQ(1, pkt4_request_received->getInteger().first);
+ EXPECT_EQ(1, pkt4_ack_sent->getInteger().first);
+ EXPECT_EQ(2, pkt4_sent->getInteger().first);
+
+ // Let the client send request 3 times, which should make the server
+ // to send 3 acks.
+ client.setState(Dhcp4Client::RENEWING);
+ ASSERT_NO_THROW(client.doRequest());
+ ASSERT_NO_THROW(client.doRequest());
+ ASSERT_NO_THROW(client.doRequest());
+
+ // Let's see if the stats are properly updated.
+ EXPECT_EQ(5, pkt4_received->getInteger().first);
+ EXPECT_EQ(1, pkt4_discover_received->getInteger().first);
+ EXPECT_EQ(1, pkt4_offer_sent->getInteger().first);
+ EXPECT_EQ(4, pkt4_request_received->getInteger().first);
+ EXPECT_EQ(4, pkt4_ack_sent->getInteger().first);
+ EXPECT_EQ(5, pkt4_sent->getInteger().first);
+}
+
+TEST_F(DORATest, statisticsDORA) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ statisticsDORA();
+}
+
+TEST_F(DORATest, statisticsDORAMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ statisticsDORA();
+}
+
+void
+DORATest::statisticsNAK() {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Configure DHCP server.
+ configure(DORA_CONFIGS[0], *client.getServer());
+ // Obtain a lease from the server using the 4-way exchange.
+
+ // Get a lease.
+ client.doDORA();
+
+ // Wipe all stats.
+ isc::stats::StatsMgr::instance().removeAll();
+
+ client.setState(Dhcp4Client::INIT_REBOOT);
+ client.config_.lease_.addr_ = IOAddress("10.0.0.30");
+ ASSERT_NO_THROW(client.doRequest());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ EXPECT_EQ(DHCPNAK, static_cast<int>(resp->getType()));
+
+ using namespace isc::stats;
+ StatsMgr& mgr = StatsMgr::instance();
+ ObservationPtr pkt4_received = mgr.getObservation("pkt4-received");
+ ObservationPtr pkt4_request_received = mgr.getObservation("pkt4-request-received");
+ ObservationPtr pkt4_ack_sent = mgr.getObservation("pkt4-ack-sent");
+ ObservationPtr pkt4_nak_sent = mgr.getObservation("pkt4-nak-sent");
+ ObservationPtr pkt4_sent = mgr.getObservation("pkt4-sent");
+
+ // All expected statistics must be present.
+ ASSERT_TRUE(pkt4_received);
+ ASSERT_TRUE(pkt4_request_received);
+ ASSERT_FALSE(pkt4_ack_sent); // No acks were sent, no such statistic expected.
+ ASSERT_TRUE(pkt4_nak_sent);
+ ASSERT_TRUE(pkt4_sent);
+
+ // They also must have expected values.
+ EXPECT_EQ(1, pkt4_received->getInteger().first);
+ EXPECT_EQ(1, pkt4_request_received->getInteger().first);
+ EXPECT_EQ(1, pkt4_nak_sent->getInteger().first);
+ EXPECT_EQ(1, pkt4_sent->getInteger().first);
+}
+
+TEST_F(DORATest, statisticsNAK) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ statisticsNAK();
+}
+
+TEST_F(DORATest, statisticsNAKMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ statisticsNAK();
+}
+
+void
+DORATest::testMultiStageBoot(const unsigned int config_index) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Configure DHCP server.
+ ASSERT_NO_THROW(configure(DORA_CONFIGS[config_index],
+ *client.getServer()));
+
+ // Stage 1: get the first lease for our client. In PXE boot, it would be
+ // a stage when the BIOS requests a lease.
+
+ // Include client id apart from the MAC address.
+ client.includeClientId("10:21:32:AB:CD:EF");
+
+ ASSERT_NO_THROW(client.doDORA());
+
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Make sure that the client has got the lease which belongs
+ // to a pool.
+ IOAddress leased_address1 = client.config_.lease_.addr_;
+ Subnet4Ptr subnet = CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->
+ selectSubnet(leased_address1);
+ ASSERT_TRUE(subnet);
+ // Make sure that the address has been allocated from the dynamic pool.
+ ASSERT_TRUE(subnet->inPool(Lease::TYPE_V4, leased_address1));
+
+ // Stage 2: the client with a given MAC address has a lease in the
+ // lease database. The installer comes up and uses the same MAC address
+ // but generates a different client id. The server should treat the
+ // client with modified client identifier as a different client and
+ // create a new lease for it.
+
+ // Modify client identifier.
+ client.includeClientId("11:54:45:AB:AA:FE");
+
+ ASSERT_NO_THROW(client.doDORA());
+
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Make sure that the client has got the lease which belongs
+ // to a pool.
+ IOAddress leased_address2 = client.config_.lease_.addr_;
+ // Make sure that the address has been allocated from the dynamic pool.
+ ASSERT_TRUE(subnet->inPool(Lease::TYPE_V4, leased_address2));
+
+ // The client should have got a new lease.
+ ASSERT_NE(leased_address1, leased_address2);
+
+ // Modify client identifier again.
+ client.includeClientId("22:34:AC:BE:44:54");
+
+ ASSERT_NO_THROW(client.doDORA());
+
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Make sure that the client has got the lease which belongs
+ // to a pool.
+ IOAddress leased_address3 = client.config_.lease_.addr_;
+ // Make sure that the address has been allocated from the dynamic pool.
+ ASSERT_TRUE(subnet->inPool(Lease::TYPE_V4, leased_address3));
+
+ // The client should have got a new lease.
+ ASSERT_NE(leased_address1, leased_address3);
+ ASSERT_NE(leased_address2, leased_address3);
+}
+
+// Test that the client using the same hardware address but multiple
+// client identifiers will obtain multiple leases.
+TEST_F(DORATest, multiStageBoot) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ // DORA_CONFIGS[0] to be used for server configuration.
+ testMultiStageBoot(0);
+}
+
+// Test that the client using the same hardware address but multiple
+// client identifiers will obtain multiple leases.
+TEST_F(DORATest, multiStageBootMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ // DORA_CONFIGS[0] to be used for server configuration.
+ testMultiStageBoot(0);
+}
+
+void
+DORATest::customServerIdentifier() {
+ Dhcp4Client client1(Dhcp4Client::SELECTING);
+ // Configure DHCP server.
+ ASSERT_NO_THROW(configure(DORA_CONFIGS[7], *client1.getServer()));
+
+ ASSERT_NO_THROW(client1.doDORA());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client1.getContext().response_);
+ Pkt4Ptr resp = client1.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // The explicitly configured server identifier should take precedence
+ // over generated server identifier.
+ EXPECT_EQ("1.2.3.4", client1.config_.serverid_.toText());
+
+ // Repeat the test for different subnet.
+ Dhcp4Client client2(client1.getServer(), Dhcp4Client::SELECTING);
+ client2.setIfaceName("eth1");
+ client2.setIfaceIndex(ETH1_INDEX);
+
+ ASSERT_NO_THROW(client2.doDORA());
+ ASSERT_TRUE(client2.getContext().response_);
+ resp = client2.getContext().response_;
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ EXPECT_EQ("2.3.4.5", client2.config_.serverid_.toText());
+
+ // Create relayed client which will be assigned a lease from the third
+ // subnet. This subnet inherits server identifier value from the global
+ // scope.
+ Dhcp4Client client3(client1.getServer(), Dhcp4Client::SELECTING);
+ client3.useRelay(true, IOAddress("10.2.3.4"));
+
+ ASSERT_NO_THROW(client3.doDORA());
+ ASSERT_TRUE(client3.getContext().response_);
+ resp = client3.getContext().response_;
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ EXPECT_EQ("3.4.5.6", client3.config_.serverid_.toText());
+}
+
+TEST_F(DORATest, customServerIdentifier) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ customServerIdentifier();
+}
+
+TEST_F(DORATest, customServerIdentifierMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ customServerIdentifier();
+}
+
+void
+DORATest::changingCircuitId() {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+ // Use relay agent so as the circuit-id can be inserted.
+ client.useRelay(true, IOAddress("10.0.0.1"), IOAddress("10.0.0.2"));
+
+ // Configure DHCP server.
+ configure(DORA_CONFIGS[10], *client.getServer());
+
+ // Send DHCPDISCOVER.
+ boost::shared_ptr<IOAddress> requested_address(new IOAddress("10.0.0.9"));
+ ASSERT_NO_THROW(client.doDiscover(requested_address));
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPOFFER
+ ASSERT_EQ(DHCPOFFER, static_cast<int>(resp->getType()));
+ // Make sure that the client has been offered a different address
+ // given that circuit-id is not used.
+ EXPECT_NE("10.0.0.9", resp->getYiaddr().toText());
+
+ // Specify circuit-id matching the one in the configuration.
+ client.setCircuitId("charter950");
+
+ // Send DHCPDISCOVER.
+ ASSERT_NO_THROW(client.doDiscover());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPOFFER
+ ASSERT_EQ(DHCPOFFER, static_cast<int>(resp->getType()));
+ // Make sure that the client has been offered reserved address given that
+ // matching circuit-id has been specified.
+ EXPECT_EQ("10.0.0.9", resp->getYiaddr().toText());
+
+ // Let's now change the circuit-id.
+ client.setCircuitId("gdansk");
+
+ // The client requests offered address but should be refused this address
+ // given that the circuit-id is not matching.
+ ASSERT_NO_THROW(client.doRequest());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ // The client should be refused this address.
+ EXPECT_EQ(DHCPNAK, static_cast<int>(resp->getType()));
+
+ // In this case, the client falls back to the 4-way exchange and should be
+ // allocated an address from the dynamic pool.
+ ASSERT_NO_THROW(client.doDORA());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ resp = client.getContext().response_;
+ // The client should be allocated some address.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ EXPECT_NE("10.0.0.9", client.config_.lease_.addr_.toText());
+}
+
+TEST_F(DORATest, changingCircuitId) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ changingCircuitId();
+}
+
+TEST_F(DORATest, changingCircuitIdMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ changingCircuitId();
+}
+
+void
+DORATest::storeExtendedInfoEnabled() {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Use relay agent to make sure that the desired subnet is
+ // selected for our client.
+ client.useRelay(true, IOAddress("10.0.0.20"), IOAddress("10.0.0.21"));
+ // Specify client identifier.
+ client.includeClientId("01:11:22:33:44:55:66");
+ // Set the circuit id to make relay-agent-info more interesting.
+ client.setCircuitId("charter950");
+
+ // Configure DHCP server.
+ configure(DORA_CONFIGS[16], *client.getServer());
+ // Client A performs 4-way exchange and should obtain a reserved
+ // address.
+ ASSERT_NO_THROW_LOG(client.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("0.0.0.0"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Make sure that the client has got the lease for the reserved address.
+ ASSERT_EQ("10.0.0.10", client.config_.lease_.addr_.toText());
+
+ // The stored lease should have extended info.
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(client.config_.lease_.addr_);
+ ASSERT_TRUE(lease);
+ ASSERT_TRUE(lease->getContext());
+
+ // Make user the user-context content is correct.
+ std::string expected_context = "{ \"ISC\": { \"relay-agent-info\": {"
+ " \"sub-options\":"
+ " \"0x010A63686172746572393530\" } } }";
+ stringstream ss;
+ ss << *(lease->getContext());
+ ASSERT_EQ(expected_context, ss.str());
+}
+
+TEST_F(DORATest, storeExtendedInfoEnabled) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ storeExtendedInfoEnabled();
+}
+
+TEST_F(DORATest, storeExtendedInfoEnabledMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ storeExtendedInfoEnabled();
+}
+
+void
+DORATest::storeExtendedInfoDisabled() {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Use relay agent to make sure that the desired subnet is
+ // selected for our client.
+ client.useRelay(true, IOAddress("192.0.2.20"), IOAddress("192.0.2.21"));
+ // Specify client identifier.
+ client.includeClientId("01:11:22:33:44:55:66");
+ // Set the circuit id to make relay-agent-info more interesting.
+ client.setCircuitId("charter950");
+
+ // Configure DHCP server.
+ configure(DORA_CONFIGS[16], *client.getServer());
+ // Client A performs 4-way exchange and should obtain a reserved
+ // address.
+ ASSERT_NO_THROW_LOG(client.doDORA(boost::shared_ptr<
+ IOAddress>(new IOAddress("0.0.0.0"))));
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Make sure that the client has got the lease for the reserved address.
+ ASSERT_EQ("192.0.2.10", client.config_.lease_.addr_.toText());
+
+ // The stored lease should not have extended info.
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(client.config_.lease_.addr_);
+ ASSERT_TRUE(lease);
+ ASSERT_FALSE(lease->getContext());
+}
+
+TEST_F(DORATest, storeExtendedInfoDisabled) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ storeExtendedInfoDisabled();
+}
+
+TEST_F(DORATest, storeExtendedInfoDisabledMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ storeExtendedInfoDisabled();
+}
+
+void
+DORATest::randomAllocation() {
+ // Create the base client and server configuration.
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ configure(DORA_CONFIGS[17], *client.getServer());
+
+ // Record what addresses have been allocated and in what order.
+ std::set<std::string> allocated_set;
+ std::vector<IOAddress> allocated_list;
+
+ // Simulate allocations from different clients.
+ for (auto i = 0; i < 30; ++i) {
+ // Create a client from the base client.
+ Dhcp4Client next_client(client.getServer(), Dhcp4Client::SELECTING);
+ // Run 4-way exchange.
+ ASSERT_NO_THROW(next_client.doDORA());
+ // Make sure that the server responded.
+ ASSERT_TRUE(next_client.getContext().response_);
+ auto resp = next_client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Remember allocated address uniqueness and order.
+ allocated_set.insert(next_client.config_.lease_.addr_.toText());
+ allocated_list.push_back(next_client.config_.lease_.addr_);
+ }
+ // Make sure that we have 30 distinct allocations.
+ ASSERT_EQ(30, allocated_set.size());
+ ASSERT_EQ(30, allocated_list.size());
+
+ // Make sure that the addresses are not allocated iteratively.
+ int consecutives = 0;
+ for (auto i = 1; i < allocated_list.size(); ++i) {
+ // Record the cases when the previously allocated address is
+ // lower by 1 (iterative allocation). Some cases like this are
+ // possible even with the random allocation but they should be
+ // very rare.
+ if (allocated_list[i].toUint32() == allocated_list[i-1].toUint32()+1) {
+ ++consecutives;
+ }
+ }
+ // Make sure we don't have too many allocations when previously
+ // allocated address is the current address minus one.
+ EXPECT_LT(consecutives, 10);
+}
+
+TEST_F(DORATest, randomAllocation) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ randomAllocation();
+}
+
+TEST_F(DORATest, randomAllocationMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ randomAllocation();
+}
+
+void
+DORATest::leaseCaching() {
+ // Configure a DHCP client.
+ Dhcp4Client client;
+ client.includeClientId("11:22");
+
+ // Configure a DHCP server.
+ configure(DORA_CONFIGS[18], *client.getServer());
+
+ // Obtain a lease from the server using the 4-way exchange.
+ ASSERT_NO_THROW(client.doDORA());
+
+ // Make sure that the server responded.
+ Pkt4Ptr response(client.getContext().response_);
+ ASSERT_TRUE(response);
+
+ // Make sure that the server has responded with DHCPACK.
+ EXPECT_EQ(DHCPACK, response->getType());
+
+ // Make sure that the server ID is present.
+ EXPECT_EQ("10.0.0.1", client.config_.serverid_.toText());
+
+ // Make sure that the client has got the first lease in the pool.
+ ASSERT_EQ("10.0.0.10", client.config_.lease_.addr_.toText());
+
+ // There should only be the default global statistic point.
+ checkStat("v4-lease-reuses", 1, 0);
+
+ // There should only be the default subnet statistic point.
+ checkStat("subnet[1].v4-lease-reuses", 1, 0);
+
+ // There should only be the default statistic point in the other subnet.
+ checkStat("subnet[2].v4-lease-reuses", 1, 0);
+
+ // Assume the client enters init-reboot and does a request.
+ client.setState(Dhcp4Client::INIT_REBOOT);
+ ASSERT_NO_THROW(client.doRequest());
+
+ // Make sure that the server responded.
+ response = client.getContext().response_;
+ ASSERT_TRUE(response);
+
+ // Make sure that the server has responded with DHCPACK.
+ EXPECT_EQ(DHCPACK, response->getType());
+
+ // Make sure that the server ID is present.
+ EXPECT_EQ("10.0.0.1", client.config_.serverid_.toText());
+
+ // Make sure that the client has got the same lease.
+ ASSERT_EQ("10.0.0.10", client.config_.lease_.addr_.toText());
+
+ // There should be one global lease reuse.
+ checkStat("v4-lease-reuses", 2, 1);
+
+ // There should be one subnet lease reuse.
+ checkStat("subnet[1].v4-lease-reuses", 2, 1);
+
+ // Statistics for the other subnet should not be affected.
+ checkStat("subnet[2].v4-lease-reuses", 1, 0);
+
+ // Let's say the client suddenly decides to do a full DORA.
+ ASSERT_NO_THROW(client.doDORA());
+
+ // Make sure that the server responded.
+ response = client.getContext().response_;
+ ASSERT_TRUE(response);
+
+ // Make sure that the server has responded with DHCPACK.
+ EXPECT_EQ(DHCPACK, response->getType());
+
+ // Make sure that the server ID is present.
+ EXPECT_EQ("10.0.0.1", client.config_.serverid_.toText());
+
+ // Make sure that the client has got the same lease.
+ ASSERT_EQ("10.0.0.10", client.config_.lease_.addr_.toText());
+
+ // There should be three global lease reuses (REQUEST + DISCOVER + REQUEST).
+ checkStat("v4-lease-reuses", 4, 3);
+
+ // There should be three subnet lease reuses (REQUEST + DISCOVER + REQUEST).
+ checkStat("subnet[1].v4-lease-reuses", 4, 3);
+
+ // Statistics for the other subnet should not be affected.
+ checkStat("subnet[2].v4-lease-reuses", 1, 0);
+
+ // Try to request a different address than the client has. The server
+ // should respond with DHCPNAK.
+ client.config_.lease_.addr_ = IOAddress("10.0.0.30");
+ ASSERT_NO_THROW(client.doRequest());
+
+ // Make sure that the server responded.
+ response = client.getContext().response_;
+ ASSERT_TRUE(response);
+ EXPECT_EQ(DHCPNAK, response->getType());
+
+ // Change client identifier. The server should treat the request
+ // as a request from unknown client and ignore it.
+ client.includeClientId("12:34");
+ ASSERT_NO_THROW(client.doRequest());
+ ASSERT_FALSE(client.getContext().response_);
+
+ // Global statistics should remain unchanged.
+ checkStat("v4-lease-reuses", 4, 3);
+
+ // Subnet statistics should remain unchanged.
+ checkStat("subnet[1].v4-lease-reuses", 4, 3);
+
+ // Statistics for the other subnet should certainly not be affected.
+ checkStat("subnet[2].v4-lease-reuses", 1, 0);
+}
+
+TEST_F(DORATest, leaseCaching) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ leaseCaching();
+}
+
+TEST_F(DORATest, leaseCachingMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ leaseCaching();
+}
+
+/// @brief Checks the value of a statistic.
+///
+/// @param name name of statistic to check
+/// @param expected_size expected number of statistic samples
+/// @param expected_value expected value of the latest statistic sample
+void DORATest::checkStat(string const& name,
+ size_t const expected_size,
+ int64_t const expected_value) {
+ ObservationPtr const stats(StatsMgr::instance().getObservation(name));
+ ASSERT_TRUE(stats) << "no such stat: " << name;
+ EXPECT_EQ(expected_size, stats->getSize())
+ << name << " stat has wrong size: found " << stats->getSize() << ", expected "
+ << expected_size;
+ EXPECT_EQ(expected_value, stats->getInteger().first)
+ << name << " stat has wrong value: found " << stats->getInteger().first << ", expected "
+ << expected_value;
+}
+
+// Starting tests which require MySQL backend availability. Those tests
+// will not be executed if Kea has been compiled without the
+// --with-mysql.
+#ifdef HAVE_MYSQL
+
+/// @brief Test fixture class for the test utilizing MySQL database backend.
+class DORAMySQLTest : public DORATest {
+public:
+ /// @brief Constructor.
+ ///
+ /// Recreates MySQL schema for a test.
+ DORAMySQLTest() : DORATest() {
+ // Ensure we have the proper schema with no transient data.
+ db::test::createMySQLSchema();
+ }
+
+ /// @brief Destructor.
+ ///
+ /// Destroys MySQL schema.
+ virtual ~DORAMySQLTest() {
+ // If data wipe enabled, delete transient data otherwise destroy the schema.
+ db::test::destroyMySQLSchema();
+ }
+};
+
+// Test that the client using the same hardware address but multiple
+// client identifiers will obtain multiple leases (MySQL lease database).
+TEST_F(DORAMySQLTest, multiStageBoot) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ // DORA_CONFIGS[8] to be used for server configuration.
+ testMultiStageBoot(8);
+}
+
+// Test that the client using the same hardware address but multiple
+// client identifiers will obtain multiple leases (MySQL lease database).
+TEST_F(DORAMySQLTest, multiStageBootMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ // DORA_CONFIGS[8] to be used for server configuration.
+ testMultiStageBoot(8);
+}
+
+#endif
+
+// Starting tests which require MySQL backend availability. Those tests
+// will not be executed if Kea has been compiled without the
+// --with-pgsql.
+#ifdef HAVE_PGSQL
+
+/// @brief Test fixture class for the test utilizing PostgreSQL database backend.
+class DORAPgSQLTest : public DORATest {
+public:
+ /// @brief Constructor.
+ ///
+ /// Recreates PgSQL schema for a test.
+ DORAPgSQLTest() : DORATest() {
+ // Ensure we have the proper schema with no transient data.
+ db::test::createPgSQLSchema();
+ }
+
+ /// @brief Destructor.
+ ///
+ /// Destroys PgSQL schema.
+ virtual ~DORAPgSQLTest() {
+ // If data wipe enabled, delete transient data otherwise destroy the schema
+ db::test::destroyPgSQLSchema();
+ }
+};
+
+// Test that the client using the same hardware address but multiple
+// client identifiers will obtain multiple leases (PostgreSQL lease database).
+TEST_F(DORAPgSQLTest, multiStageBoot) {
+ Dhcpv4SrvMTTestGuard guard(*this, false);
+ // DORA_CONFIGS[9] to be used for server configuration.
+ testMultiStageBoot(9);
+}
+
+// Test that the client using the same hardware address but multiple
+// client identifiers will obtain multiple leases (PostgreSQL lease database).
+TEST_F(DORAPgSQLTest, multiStageBootMultiThreading) {
+ Dhcpv4SrvMTTestGuard guard(*this, true);
+ // DORA_CONFIGS[9] to be used for server configuration.
+ testMultiStageBoot(9);
+}
+
+#endif
+
+} // end of anonymous namespace
diff --git a/src/bin/dhcp4/tests/fqdn_unittest.cc b/src/bin/dhcp4/tests/fqdn_unittest.cc
new file mode 100644
index 0000000..12f31cf
--- /dev/null
+++ b/src/bin/dhcp4/tests/fqdn_unittest.cc
@@ -0,0 +1,2885 @@
+// Copyright (C) 2013-2023 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <asiolink/io_address.h>
+#include <dhcp/option4_client_fqdn.h>
+#include <dhcp/option_int.h>
+#include <dhcp/option_int_array.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcp4/tests/dhcp4_client.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <dhcp_ddns/ncr_msg.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/lease_mgr.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <dhcpsrv/ncr_generator.h>
+#include <stats/stats_mgr.h>
+#include <testutils/gtest_utils.h>
+#include <util/optional.h>
+
+#include <gtest/gtest.h>
+#include <boost/scoped_ptr.hpp>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::dhcp_ddns;
+using namespace isc::stats;
+using namespace isc::util;
+
+namespace {
+
+/// @brief Set of JSON configurations used by the FQDN tests.
+const char* CONFIGS[] = {
+ // 0
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 3000,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"option-data\": [ {"
+ " \"name\": \"routers\","
+ " \"data\": \"10.0.0.200,10.0.0.201\""
+ " } ],"
+ " \"reservations\": ["
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"hostname\": \"unique-host.example.com\""
+ " }"
+ " ]"
+ " }],"
+ "\"dhcp-ddns\": {"
+ "\"enable-updates\": true,"
+ "\"qualifying-suffix\": \"\""
+ "}"
+ "}",
+ // 1
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 3000,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"option-data\": [ {"
+ " \"name\": \"routers\","
+ " \"data\": \"10.0.0.200,10.0.0.201\""
+ " } ],"
+ " \"reservations\": ["
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"hostname\": \"foobar\""
+ " }"
+ " ]"
+ " }],"
+ "\"dhcp-ddns\": {"
+ "\"enable-updates\": true,"
+ "\"qualifying-suffix\": \"fake-suffix.isc.org.\""
+ "}"
+ "}",
+ // 2
+ // Simple config with DDNS updates disabled. Note pool is one address
+ // large to ensure we get a specific address back.
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 3000,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.10\" } ]"
+ " }],"
+ "\"dhcp-ddns\": {"
+ "\"enable-updates\": false,"
+ "\"qualifying-suffix\": \"fake-suffix.isc.org.\""
+ "}"
+ "}",
+ // 3
+ // Simple config with DDNS updates enabled. Note pool is one address
+ // large to ensure we get a specific address back.
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 3000,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.10\" } ]"
+ " }],"
+ "\"dhcp-ddns\": {"
+ "\"enable-updates\": true,"
+ "\"qualifying-suffix\": \"fake-suffix.isc.org.\""
+ "}"
+ "}",
+ // 4
+ // Configuration which disables DNS updates but contains a reservation
+ // for a hostname. Reserved hostname should be assigned to a client if
+ // the client includes it in the Parameter Request List option.
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 3000,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"option-data\": [ {"
+ " \"name\": \"routers\","
+ " \"data\": \"10.0.0.200,10.0.0.201\""
+ " } ],"
+ " \"reservations\": ["
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"hostname\": \"reserved.example.com\""
+ " }"
+ " ]"
+ " }],"
+ "\"dhcp-ddns\": {"
+ "\"enable-updates\": false,"
+ "\"qualifying-suffix\": \"\""
+ "}"
+ "}",
+ // 5
+ // Configuration which disables DNS updates but contains a reservation
+ // for a hostname and the qualifying-suffix which should be appended to
+ // the reserved hostname in the Hostname option returned to a client.
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 3000,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"option-data\": [ {"
+ " \"name\": \"routers\","
+ " \"data\": \"10.0.0.200,10.0.0.201\""
+ " } ],"
+ " \"reservations\": ["
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"hostname\": \"foo-bar\""
+ " }"
+ " ]"
+ " }],"
+ "\"dhcp-ddns\": {"
+ "\"enable-updates\": false,"
+ "\"qualifying-suffix\": \"example.isc.org\""
+ "}"
+ "}",
+ // 6
+ // Configuration which enables DNS updates and hostname sanitization
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 3000,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"option-data\": [ {"
+ " \"name\": \"routers\","
+ " \"data\": \"10.0.0.200,10.0.0.201\""
+ " } ],"
+ " \"reservations\": ["
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"hostname\": \"unique-xxx-host.example.com\""
+ " }"
+ " ]"
+ " }],"
+ "\"dhcp-ddns\": {"
+ "\"enable-updates\": true,"
+ "\"hostname-char-set\" : \"[^A-Za-z0-9.-]\","
+ "\"hostname-char-replacement\" : \"x\","
+ "\"qualifying-suffix\": \"example.com\""
+ "}"
+ "}",
+ // 7
+ // Configuration with disabled DNS updates (default) and
+ // hostname sanitization defined at global scope.
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 3000,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"option-data\": [ {"
+ " \"name\": \"routers\","
+ " \"data\": \"10.0.0.200,10.0.0.201\""
+ " } ],"
+ " \"reservations\": ["
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"hostname\": \"unique-xxx-host.example.com\""
+ " }"
+ " ]"
+ " }],"
+ "\"hostname-char-set\" : \"[^A-Za-z0-9.-]\","
+ "\"hostname-char-replacement\" : \"x\""
+ "}",
+ // 8
+ // D2 enabled
+ // global ddns-send-updates is false
+ // one subnet does not enable updates
+ // one subnet does enables updates
+ "{ \"interfaces-config\": {\n"
+ " \"interfaces\": [ \"*\" ]\n"
+ "},\n"
+ "\"dhcp-ddns\": {\n"
+ "\"enable-updates\": true\n"
+ "},\n"
+ "\"ddns-send-updates\": false,\n"
+ "\"subnet4\": [ {\n"
+ " \"subnet\": \"192.0.2.0/24\",\n"
+ " \"id\": 1,\n"
+ " \"pools\": [ { \"pool\": \"192.0.2.10-192.0.2.100\" } ],\n"
+ " \"interface\": \"eth0\"\n"
+ " },\n"
+ " {\n"
+ " \"subnet\": \"192.0.3.0/24\", \n"
+ " \"id\": 2,\n"
+ " \"pools\": [ { \"pool\": \"192.0.3.10-192.0.3.100\" } ],\n"
+ " \"interface\": \"eth1\",\n"
+ " \"ddns-send-updates\": true\n"
+ " }\n"
+ "]\n"
+ "}",
+ // 9
+ // Simple config with DDNS updates enabled. Note pool is one address
+ // large to ensure we get a specific address back.
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 3000,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.10-192.0.2.10\" } ]"
+ " }],"
+ "\"dhcp-ddns\": {"
+ "\"enable-updates\": true,"
+ "\"qualifying-suffix\": \"fake-suffix.isc.org.\""
+ "}"
+ "}",
+ // 10
+ // D2 enabled
+ // shared-network with two subnets with
+ // different DDNS parameters
+ "{ \"interfaces-config\": { \n"
+ " \"interfaces\": [ \"*\" ] \n"
+ " }, \n"
+ " \"dhcp-ddns\": { \n"
+ " \"enable-updates\": true \n"
+ " }, \n"
+ " \"shared-networks\": [ \n"
+ " { \n"
+ " \"interface\": \"eth1\", \n"
+ " \"name\": \"foo\", \n"
+ " \"subnet4\": [ { \n"
+ " \"subnet\": \"192.0.2.0/24\", \n"
+ " \"id\": 1, \n"
+ " \"pools\": [ { \n"
+ " \"pool\": \"192.0.2.10 - 192.0.2.10\" \n"
+ " }], \n"
+ " \"ddns-qualifying-suffix\": \"one.example.com.\" \n"
+ " }, \n"
+ " { \n"
+ " \"subnet\": \"10.0.0.0/24\", \n"
+ " \"id\": 2, \n"
+ " \"pools\": [ { \n"
+ " \"pool\": \"10.0.0.10 - 10.0.0.10\" \n"
+ " }], \n"
+ " \"ddns-qualifying-suffix\": \"two.example.com.\" \n"
+ " }] \n"
+ " }] \n"
+ "}",
+ // 11
+ // D2 enabled
+ // offer-lifetime > 0
+ "{ \"interfaces-config\": {\n"
+ " \"interfaces\": [ \"*\" ]\n"
+ "},\n"
+ "\"valid-lifetime\": 3000,\n"
+ "\"subnet4\": [ { \n"
+ " \"subnet\": \"10.0.0.0/24\", \n"
+ " \"id\": 1,\n"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.10\" } ],\n"
+ " \"offer-lifetime\": 45,\n"
+ "} ],\n"
+ "\"dhcp-ddns\": {\n"
+ "\"enable-updates\": true,\n"
+ "\"qualifying-suffix\": \"example.com.\"\n"
+ "}\n"
+ "}",
+ // 12
+ // D2 enabled
+ // ddns-ttl-percent specfied
+ "{ \"interfaces-config\": {\n"
+ " \"interfaces\": [ \"*\" ]\n"
+ "},\n"
+ "\"valid-lifetime\": 3000,\n"
+ "\"subnet4\": [ { \n"
+ " \"subnet\": \"10.0.0.0/24\", \n"
+ " \"interface\": \"eth1\",\n"
+ " \"id\": 1,\n"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.10\" } ],\n"
+ " \"ddns-ttl-percent\": .25\n"
+ "} ],\n"
+ "\"dhcp-ddns\": {\n"
+ "\"enable-updates\": true,\n"
+ "\"qualifying-suffix\": \"example.com.\"\n"
+ "}\n"
+ "}"
+};
+
+class NameDhcpv4SrvTest : public Dhcpv4SrvTest {
+public:
+ // Reference to D2ClientMgr singleton
+ D2ClientMgr& d2_mgr_;
+
+ /// @brief Pointer to the DHCP server instance.
+ boost::shared_ptr<NakedDhcpv4Srv> srv_;
+
+ /// @brief Interface Manager's fake configuration control.
+ IfaceMgrTestConfig iface_mgr_test_config_;
+
+ // Bit Constants for turning on and off DDNS configuration options.
+ static const uint16_t OVERRIDE_NO_UPDATE = 1;
+ static const uint16_t OVERRIDE_CLIENT_UPDATE = 2;
+ static const uint16_t REPLACE_CLIENT_NAME = 4;
+
+ // Enum used to specify whether a client (packet) should include
+ // the hostname option
+ enum ClientNameFlag {
+ CLIENT_NAME_PRESENT,
+ CLIENT_NAME_NOT_PRESENT
+ };
+
+ // Enum used to specify whether the server should replace/supply
+ // the hostname or not
+ enum ReplacementFlag {
+ NAME_REPLACED,
+ NAME_NOT_REPLACED
+ };
+
+ NameDhcpv4SrvTest()
+ : Dhcpv4SrvTest(),
+ d2_mgr_(CfgMgr::instance().getD2ClientMgr()),
+ iface_mgr_test_config_(true)
+ {
+ srv_ = boost::make_shared<NakedDhcpv4Srv>(0);
+ IfaceMgr::instance().openSockets4();
+ // Config DDNS to be enabled, all controls off
+ enableD2();
+ // Let's wipe all existing statistics.
+ isc::stats::StatsMgr::instance().removeAll();
+ }
+
+ virtual ~NameDhcpv4SrvTest() {
+ // CfgMgr singleton doesn't get wiped between tests, so we'll
+ // disable D2 explicitly between tests.
+ disableD2();
+ }
+
+ /// @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,
+ isc::asiolink::IOAddress("0.0.0.0"), 0, 1024,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON)));
+
+ ASSERT_NO_THROW(CfgMgr::instance().setD2ClientConfig(cfg));
+
+ // Now we'll set the DDNS parameters at the subnet level.
+ // These should get fetched when getDdnsParams() is invoked.
+ ASSERT_TRUE(subnet_) << "enableD2 called without subnet_ set";
+ subnet_->setDdnsSendUpdates(true);
+ subnet_->setDdnsOverrideNoUpdate(mask & OVERRIDE_NO_UPDATE);
+ subnet_->setDdnsOverrideClientUpdate(mask & OVERRIDE_CLIENT_UPDATE);
+ subnet_->setDdnsReplaceClientNameMode((mask & REPLACE_CLIENT_NAME) ?
+ D2ClientConfig::RCM_WHEN_PRESENT
+ : D2ClientConfig::RCM_NEVER);
+ subnet_->setDdnsGeneratedPrefix("myhost");
+ subnet_->setDdnsQualifyingSuffix("example.com");
+ subnet_->setDdnsUseConflictResolution(true);
+
+ ASSERT_NO_THROW(srv_->startD2());
+ }
+
+ // Fetch DDNS parameter set scoped to the current subnet_.
+ DdnsParamsPtr getDdnsParams() {
+ ConstElementPtr cfg = CfgMgr::instance().getCurrentCfg()->toElement();
+ if (!subnet_) {
+ ADD_FAILURE() << "getDdnsParams() - subnet_ is empty!";
+ return (DdnsParamsPtr(new DdnsParams()));
+ }
+
+ return(CfgMgr::instance().getCurrentCfg()->getDdnsParams(subnet_));
+ }
+
+ // Create a lease to be used by various tests.
+ Lease4Ptr createLease(const isc::asiolink::IOAddress& addr,
+ const std::string& hostname,
+ const bool fqdn_fwd,
+ const bool fqdn_rev) {
+ const uint8_t hwaddr_data[] = { 0, 1, 2, 3, 4, 5, 6 };
+ HWAddrPtr hwaddr(new HWAddr(hwaddr_data, sizeof(hwaddr_data),
+ HTYPE_ETHER));
+ Lease4Ptr lease(new Lease4(addr, hwaddr,
+ &generateClientId()->getData()[0],
+ generateClientId()->getData().size(),
+ 100, time(NULL), subnet_->getID()));
+ // @todo Set this through the Lease4 constructor.
+ lease->hostname_ = hostname;
+ lease->fqdn_fwd_ = fqdn_fwd;
+ lease->fqdn_rev_ = fqdn_rev;
+
+ return (lease);
+ }
+
+ // Create an instance of the DHCPv4 Client FQDN Option.
+ Option4ClientFqdnPtr
+ createClientFqdn(const uint8_t flags,
+ const std::string& fqdn_name,
+ Option4ClientFqdn::DomainNameType fqdn_type) {
+ return (Option4ClientFqdnPtr(new Option4ClientFqdn(flags,
+ Option4ClientFqdn::
+ RCODE_CLIENT(),
+ fqdn_name,
+ fqdn_type)));
+ }
+
+ // Create an instance of the Hostname option.
+ OptionStringPtr
+ createHostname(const std::string& hostname) {
+ OptionStringPtr opt_hostname(new OptionString(Option::V4,
+ DHO_HOST_NAME,
+ hostname));
+ return (opt_hostname);
+ }
+
+ /// @brief Convenience method for generating an FQDN from an IP address.
+ ///
+ /// This is just a wrapper method around the D2ClientMgr's method for
+ /// generating domain names from the configured prefix, suffix, and a
+ /// given IP address. This is useful for verifying that fully generated
+ /// names are correct.
+ ///
+ /// @param addr IP address used in the lease.
+ /// @param trailing_dot A boolean flag which indicates whether the
+ /// trailing dot should be appended to the end of the hostname.
+ /// The default value is "true" which means that it should.
+ ///
+ /// @return An std::string contained the generated FQDN.
+ std::string generatedNameFromAddress(const IOAddress& addr,
+ const bool trailing_dot = true) {
+ return(CfgMgr::instance().getD2ClientMgr()
+ .generateFqdn(addr, *getDdnsParams(), trailing_dot));
+ }
+
+ // Get the Client FQDN Option from the given message.
+ Option4ClientFqdnPtr getClientFqdnOption(const Pkt4Ptr& pkt) {
+ return (boost::dynamic_pointer_cast<
+ Option4ClientFqdn>(pkt->getOption(DHO_FQDN)));
+ }
+
+ // get the Hostname option from the given message.
+ OptionStringPtr getHostnameOption(const Pkt4Ptr& pkt) {
+ return (boost::dynamic_pointer_cast<
+ OptionString>(pkt->getOption(DHO_HOST_NAME)));
+ }
+
+ // Create a message holding DHCPv4 Client FQDN Option.
+ Pkt4Ptr generatePktWithFqdn(const uint8_t msg_type,
+ const uint8_t fqdn_flags,
+ const std::string& fqdn_domain_name,
+ Option4ClientFqdn::DomainNameType fqdn_type,
+ const bool include_prl,
+ const bool include_clientid = true) {
+ Pkt4Ptr pkt = Pkt4Ptr(new Pkt4(msg_type, 1234));
+ pkt->setRemoteAddr(IOAddress("192.0.2.3"));
+ pkt->setIface("eth1");
+ pkt->setIndex(ETH1_INDEX);
+ // For DISCOVER we don't include server id, because client broadcasts
+ // the message to all servers.
+ if (msg_type != DHCPDISCOVER) {
+ pkt->addOption(srv_->getServerID());
+ }
+
+ if (include_clientid) {
+ pkt->addOption(generateClientId());
+ }
+
+ // Create Client FQDN Option with the specified flags and
+ // domain-name.
+ pkt->addOption(createClientFqdn(fqdn_flags, fqdn_domain_name,
+ fqdn_type));
+
+ // Control whether or not to request that server returns the FQDN
+ // option. Server may be configured to always return it or return
+ // only in case client requested it.
+ if (include_prl) {
+ OptionUint8ArrayPtr option_prl =
+ OptionUint8ArrayPtr(new OptionUint8Array(Option::V4,
+ DHO_DHCP_PARAMETER_REQUEST_LIST));
+ option_prl->addValue(DHO_FQDN);
+ }
+ return (pkt);
+ }
+
+ // Create a message holding a Hostname option.
+ Pkt4Ptr generatePktWithHostname(const uint8_t msg_type,
+ const std::string& hostname) {
+
+ Pkt4Ptr pkt = Pkt4Ptr(new Pkt4(msg_type, 1234));
+ pkt->setRemoteAddr(IOAddress("192.0.2.3"));
+ // For DISCOVER we don't include server id, because client broadcasts
+ // the message to all servers.
+ if (msg_type != DHCPDISCOVER) {
+ pkt->addOption(srv_->getServerID());
+ }
+
+ pkt->addOption(generateClientId());
+
+ // Create Client FQDN Option with the specified flags and
+ // domain-name.
+ pkt->addOption(createHostname(hostname));
+
+ return (pkt);
+
+ }
+
+ // Create a message holding an empty Hostname option.
+ Pkt4Ptr generatePktWithEmptyHostname(const uint8_t msg_type) {
+
+ Pkt4Ptr pkt = Pkt4Ptr(new Pkt4(msg_type, 1234));
+ pkt->setRemoteAddr(IOAddress("192.0.2.3"));
+ // For DISCOVER we don't include server id, because client broadcasts
+ // the message to all servers.
+ if (msg_type != DHCPDISCOVER) {
+ pkt->addOption(srv_->getServerID());
+ }
+
+ pkt->addOption(generateClientId());
+
+ // Create Hostname option.
+ std::string hostname(" ");
+ OptionPtr opt = createHostname(hostname);
+ opt->setData(hostname.begin(), hostname.begin());
+ pkt->addOption(opt);
+
+ return (pkt);
+
+ }
+
+ // Create a message holding of a given type
+ Pkt4Ptr generatePkt(const uint8_t msg_type) {
+ Pkt4Ptr pkt = Pkt4Ptr(new Pkt4(msg_type, 1234));
+ pkt->setRemoteAddr(IOAddress("192.0.2.3"));
+ // For DISCOVER we don't include server id, because client broadcasts
+ // the message to all servers.
+ if (msg_type != DHCPDISCOVER) {
+ pkt->addOption(srv_->getServerID());
+ }
+
+ pkt->addOption(generateClientId());
+ return (pkt);
+ }
+
+ // Test that server generates the appropriate FQDN option in response to
+ // client's FQDN option.
+ void testProcessFqdn(const Pkt4Ptr& query, const uint8_t exp_flags,
+ const std::string& exp_domain_name,
+ Option4ClientFqdn::DomainNameType
+ exp_domain_type = Option4ClientFqdn::FULL) {
+ ASSERT_TRUE(getClientFqdnOption(query));
+
+ Pkt4Ptr answer;
+ if (query->getType() == DHCPDISCOVER) {
+ answer.reset(new Pkt4(DHCPOFFER, 1234));
+
+ } else {
+ answer.reset(new Pkt4(DHCPACK, 1234));
+
+ }
+ Dhcpv4Exchange ex = createExchange(query);
+ ASSERT_NO_THROW(srv_->processClientName(ex));
+
+ Option4ClientFqdnPtr fqdn = getClientFqdnOption(ex.getResponse());
+ ASSERT_TRUE(fqdn);
+
+ checkFqdnFlags(ex.getResponse(), exp_flags);
+
+ EXPECT_EQ(exp_domain_name, fqdn->getDomainName());
+ EXPECT_EQ(exp_domain_type, fqdn->getDomainNameType());
+
+ }
+
+ // Test that the server's processes the hostname (or lack thereof)
+ // in a client request correctly, according to the replace-client-name
+ // mode configuration parameter. We include hostname sanitizer to ensure
+ // it does not interfere with name replacement.
+ //
+ // @param mode - value to use for replace-client-name
+ // @param client_name_flag - specifies whether or not the client request
+ // should contain a hostname option
+ // @param exp_replacement_flag - specifies whether or not the server is
+ // expected to replace (or supply) the hostname in its response
+ void testReplaceClientNameMode(const char* mode,
+ enum ClientNameFlag client_name_flag,
+ enum ReplacementFlag exp_replacement_flag) {
+ // Configuration "template" with a replaceable mode parameter
+ const char* config_template =
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 3000,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"192.0.2.10-192.0.2.10\" } ]"
+ " }],"
+ "\"dhcp-ddns\": {"
+ " \"enable-updates\": true,"
+ " \"qualifying-suffix\": \"fake-suffix.isc.org.\","
+ " \"hostname-char-set\": \"[^A-Za-z0-9.-]\","
+ " \"hostname-char-replacement\": \"x\","
+ " \"replace-client-name\": \"%s\""
+ "}}";
+
+ // Create the configuration and configure the server
+ char config_buf[1024];
+ snprintf(config_buf, 1024, config_template, mode);
+ ASSERT_NO_THROW(configure(config_buf, *srv_)) << "configuration failed";
+
+ // Build our client packet
+ Pkt4Ptr query;
+ if (client_name_flag == CLIENT_NAME_PRESENT) {
+ ASSERT_NO_THROW(query = generatePktWithHostname(DHCPREQUEST,
+ "my.example.com."));
+ } else {
+ ASSERT_NO_THROW(query = generatePkt(DHCPREQUEST));
+ }
+
+ // Run the packet through the server, extracting the hostname option
+ // from the response. If the option isn't present the returned pointer
+ // will be null.
+ OptionStringPtr hostname;
+ ASSERT_NO_THROW(
+ hostname = processHostname(query,
+ client_name_flag == CLIENT_NAME_PRESENT)
+ ) << "processHostname throw an exception";
+
+ // Verify the contents (or lack thereof) of the hostname
+ if (exp_replacement_flag == NAME_REPLACED) {
+ ASSERT_TRUE(hostname)
+ << "No host name, it should have the replacement name \".\"";
+ EXPECT_EQ(".", hostname->getValue());
+ } else {
+ if (client_name_flag == CLIENT_NAME_PRESENT) {
+ ASSERT_TRUE(hostname)
+ << "No host name, expected original from client";
+ EXPECT_EQ("my.example.com.", hostname->getValue());
+ } else {
+ ASSERT_FALSE(hostname)
+ << "Host name is: " << hostname
+ << ", it should have been null";
+ }
+ }
+ }
+
+ /// @brief Checks the packet's FQDN option flags against a given mask
+ ///
+ /// @param pkt IPv4 packet whose FQDN flags should be checked.
+ /// @param exp_flags Bit mask of flags that are expected to be true.
+ void checkFqdnFlags(const Pkt4Ptr& pkt, const uint8_t exp_flags) {
+ Option4ClientFqdnPtr fqdn = getClientFqdnOption(pkt);
+ ASSERT_TRUE(fqdn);
+
+ const bool flag_n = (exp_flags & Option4ClientFqdn::FLAG_N) != 0;
+ const bool flag_s = (exp_flags & Option4ClientFqdn::FLAG_S) != 0;
+ const bool flag_o = (exp_flags & Option4ClientFqdn::FLAG_O) != 0;
+ const bool flag_e = (exp_flags & Option4ClientFqdn::FLAG_E) != 0;
+
+ EXPECT_EQ(flag_n, fqdn->getFlag(Option4ClientFqdn::FLAG_N));
+ EXPECT_EQ(flag_s, fqdn->getFlag(Option4ClientFqdn::FLAG_S));
+ EXPECT_EQ(flag_o, fqdn->getFlag(Option4ClientFqdn::FLAG_O));
+ EXPECT_EQ(flag_e, fqdn->getFlag(Option4ClientFqdn::FLAG_E));
+ }
+
+ /// @brief Invokes Dhcpv4Srv::processHostname on the given packet
+ ///
+ /// Processes the Hostname option in the client's message and returns
+ /// the hostname option which would be sent to the client. It will
+ /// return empty if the hostname option is not to be included
+ /// server's response.
+ /// @param query - client packet to process
+ /// @param must_have_host - flag indicating whether or not the client
+ /// packet must contain the hostname option
+ ///
+ /// @return a pointer to the hostname option constructed by the server
+ OptionStringPtr processHostname(const Pkt4Ptr& query,
+ bool must_have_host = true) {
+ if (!getHostnameOption(query) && must_have_host) {
+ ADD_FAILURE() << "Hostname option not carried in the query";
+ }
+
+ Pkt4Ptr answer;
+ if (query->getType() == DHCPDISCOVER) {
+ answer.reset(new Pkt4(DHCPOFFER, 1234));
+
+ } else {
+ answer.reset(new Pkt4(DHCPACK, 1234));
+
+ }
+
+ Dhcpv4Exchange ex = createExchange(query);
+ if (!ex.getContext()->subnet_) {
+ ADD_FAILURE() << "createExchange did not select a subnet";
+ }
+
+ srv_->processClientName(ex);
+
+ OptionStringPtr hostname = getHostnameOption(ex.getResponse());
+ return (hostname);
+
+ }
+
+ ///@brief Verify that NameChangeRequest holds valid values.
+ ///
+ /// Pulls the NCR from the top of the send queue and checks its content
+ /// against a number of expected parameters.
+ ///
+ /// @param type - expected NCR change type, CHG_ADD or CHG_REMOVE
+ /// @param reverse - flag indicating whether or not the NCR specifies
+ /// reverse change
+ /// @param forward - flag indication whether or not the NCR specifies
+ /// forward change
+ /// @param addr - expected lease address in the NCR
+ /// @param fqdn - expected FQDN in the NCR
+ /// @param dhcid - expected DHCID in the NCR (comparison is performed only
+ /// if the value supplied is not empty):w
+ /// @param cltt - cltt value from the lease the NCR for which the NCR
+ /// was generated expected value for
+ /// @param lifetime - lease's valid lifetime from which NCR ttl was
+ /// generated
+ /// @param not_strict_expire_check - when true the comparison of the NCR
+ /// lease expiration time is conducted as greater than or equal to rather
+ /// equal to CLTT plus lease ttl .
+ /// @param exp_use_cr expected value of conflict resolution flag
+ void verifyNameChangeRequest(const isc::dhcp_ddns::NameChangeType type,
+ const bool reverse, const bool forward,
+ const std::string& addr,
+ const std::string& fqdn,
+ const std::string& dhcid,
+ const time_t cltt,
+ const uint16_t valid_lft,
+ const bool not_strict_expire_check = false,
+ const bool exp_use_cr = true,
+ Optional<double> ttl_percent = Optional<double>()) {
+ 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());
+ }
+
+ // In some cases, the test doesn't have access to the last transmission
+ // time for the particular client. In such cases, the test can use the
+ // current time as cltt but the it may not check the lease expiration
+ // time for equality but rather check that the lease expiration time
+ // is not greater than the current time + lease lifetime.
+ uint32_t ttl = calculateDdnsTtl(valid_lft, ttl_percent);
+ if (not_strict_expire_check) {
+ EXPECT_GE(cltt + ttl, ncr->getLeaseExpiresOn());
+ } else {
+ EXPECT_EQ(cltt + ttl, ncr->getLeaseExpiresOn());
+ }
+
+ EXPECT_EQ(ttl, ncr->getLeaseLength());
+ EXPECT_EQ(isc::dhcp_ddns::ST_NEW, ncr->getStatus());
+ EXPECT_EQ(exp_use_cr, ncr->useConflictResolution());
+
+ // Process the message off the queue
+ ASSERT_NO_THROW(d2_mgr_.runReadyIO());
+ }
+
+ /// @brief Tests processing a request with the given client flags
+ ///
+ /// This method creates a request with its FQDN flags set to the given
+ /// value and submits it to the server for processing. It then checks
+ /// the following:
+ /// 1. Did the server generate an ACK with the correct FQDN flags
+ /// 2. If the server should have generated an NCR, did it? and If
+ /// so was it correct?
+ ///
+ /// @param client_flags Mask of client FQDN flags which are true
+ /// @param response_flags Mask of expected FQDN flags in the response
+ void flagVsConfigScenario(const uint8_t client_flags,
+ const uint8_t response_flags) {
+ // Create fake interfaces and open fake sockets.
+ IfaceMgrTestConfig iface_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ Pkt4Ptr req = generatePktWithFqdn(DHCPREQUEST, client_flags,
+ "myhost.example.com.",
+ Option4ClientFqdn::FULL, true);
+
+ // Process the request.
+ Pkt4Ptr reply;
+ ASSERT_NO_THROW(reply = srv_->processRequest(req));
+
+ // Verify the response and flags.
+ checkResponse(reply, DHCPACK, 1234);
+ checkFqdnFlags(reply, response_flags);
+
+ // NCRs cannot be sent to the d2_mgr unless updates are enabled.
+ if (d2_mgr_.ddnsEnabled()) {
+ // There should be an NCR if response S flag is 1 or N flag is 0.
+ bool exp_fwd = (response_flags & Option4ClientFqdn::FLAG_S);
+ bool exp_rev = (!(response_flags & Option4ClientFqdn::FLAG_N));
+ if (!exp_fwd && !exp_rev) {
+ 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,
+ exp_rev, exp_fwd,
+ reply->getYiaddr().toText(),
+ "myhost.example.com.",
+ "", // empty DHCID means don't check it
+ time(NULL) + subnet_->getValid(),
+ subnet_->getValid(), true);
+ }
+ }
+ }
+
+
+ /// @brief Checks the value of an integer statistic for a given subnet.
+ ///
+ /// @param subnet_id identifier of a subnet for which the statistic should be checked
+ /// @param name statistic name (e.g. "assigned-addresses", "total-addresses", ...)
+ /// @param exp_value expected value of the statistic
+ ///
+ /// @return Number of assigned addresses for a subnet.
+ void checkSubnetStat(const SubnetID& subnet_id, const std::string& name, int64_t exp_value) const {
+ // Retrieve statistics name, e.g. subnet[1234].assigned-addresses.
+ std::string stats_name = StatsMgr::generateName("subnet", subnet_id, name);
+ ObservationPtr obs = StatsMgr::instance().getObservation(stats_name);
+ ASSERT_TRUE(obs) << "cannot find: " << stats_name;
+ EXPECT_EQ(exp_value, obs->getInteger().first);
+ stats_name = StatsMgr::generateName("subnet", subnet_id, StatsMgr::generateName("pool", 0, name));
+ obs = StatsMgr::instance().getObservation(stats_name);
+ ASSERT_TRUE(obs) << "cannot find: " << stats_name;
+ EXPECT_EQ(exp_value, obs->getInteger().first);
+ }
+};
+
+// Tests the following scenario:
+// - Updates are enabled
+// - All overrides are off
+// - Client requests forward update (N = 0, S = 1)
+//
+// Server should perform the update:
+// - Response flags should N = 0, S = 1, O = 0
+// - Should queue an NCR
+TEST_F(NameDhcpv4SrvTest, updatesEnabled) {
+ flagVsConfigScenario((Option4ClientFqdn::FLAG_E |
+ Option4ClientFqdn::FLAG_S),
+ (Option4ClientFqdn::FLAG_E |
+ Option4ClientFqdn::FLAG_S));
+}
+
+// Tests the following scenario
+// - Updates are disabled
+// - Client requests forward update (N = 0, S = 1)
+//
+// Server should NOT perform updates:
+// - Response flags should N = 1, S = 0, O = 1
+// - Should not queue any NCRs
+TEST_F(NameDhcpv4SrvTest, updatesDisabled) {
+ disableD2();
+ flagVsConfigScenario((Option4ClientFqdn::FLAG_E |
+ Option4ClientFqdn::FLAG_S),
+ (Option4ClientFqdn::FLAG_E |
+ Option4ClientFqdn::FLAG_N |
+ Option4ClientFqdn::FLAG_O));
+}
+
+// Tests the following scenario:
+// - Updates are enabled
+// - All overrides are off.
+// - Client requests no updates (N = 1, S = 0)
+//
+// Server should NOT perform updates:
+// - Response flags should N = 1, S = 0, O = 0
+// - Should not queue any NCRs
+TEST_F(NameDhcpv4SrvTest, respectNoUpdate) {
+ flagVsConfigScenario((Option4ClientFqdn::FLAG_E |
+ Option4ClientFqdn::FLAG_N),
+ (Option4ClientFqdn::FLAG_E |
+ Option4ClientFqdn::FLAG_N));
+}
+
+// Tests the following scenario:
+// - Updates are enabled
+// - override-no-update is on
+// - Client requests no updates (N = 1, S = 0)
+//
+// Server should override "no update" request and perform updates:
+// - Response flags should be N = 0, S = 1, O = 1
+// - Should queue an NCR
+TEST_F(NameDhcpv4SrvTest, overrideNoUpdate) {
+ enableD2(OVERRIDE_NO_UPDATE);
+ flagVsConfigScenario((Option4ClientFqdn::FLAG_E |
+ Option4ClientFqdn::FLAG_N),
+ (Option4ClientFqdn::FLAG_E |
+ Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_O));
+}
+
+// Tests the following scenario:
+// - Updates are enabled
+// - All overrides are off.
+// - Client requests delegation (N = 0, S = 0)
+//
+// Server should respect client's delegation request and NOT do updates:
+
+// - Response flags should be N = 0, S = 0, O = 0
+// - Should not queue any NCRs
+TEST_F(NameDhcpv4SrvTest, respectClientDelegation) {
+
+ flagVsConfigScenario(Option4ClientFqdn::FLAG_E,
+ Option4ClientFqdn::FLAG_E);
+}
+
+// Tests the following scenario:
+// - Updates are enabled
+// - override-client-update is on.
+// - Client requests delegation (N = 0, S = 0)
+//
+// Server should override client's delegation request and do updates:
+// - Response flags should be N = 0, S = 1, O = 1
+// - Should queue an NCR
+TEST_F(NameDhcpv4SrvTest, overrideClientDelegation) {
+ // Turn on override-client-update.
+ enableD2(OVERRIDE_CLIENT_UPDATE);
+
+ flagVsConfigScenario(Option4ClientFqdn::FLAG_E,
+ (Option4ClientFqdn::FLAG_E |
+ Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_O));
+}
+
+// Test that server processes the Hostname option sent by a client and
+// responds with the Hostname option to confirm that the server has
+// taken responsibility for the update.
+TEST_F(NameDhcpv4SrvTest, serverUpdateHostname) {
+ Pkt4Ptr query;
+ ASSERT_NO_THROW(query = generatePktWithHostname(DHCPREQUEST,
+ "myhost.example.com."));
+ OptionStringPtr hostname;
+ ASSERT_NO_THROW(hostname = processHostname(query));
+
+ ASSERT_TRUE(hostname);
+ EXPECT_EQ("myhost.example.com.", hostname->getValue());
+}
+
+// Test that the server skips processing of a mal-formed Hostname options.
+// - First scenario the hostname has an empty label
+// - Second scenario the hostname option causes an internal parsing error
+// in dns::Name(). The content was created by fuzz testing.
+TEST_F(NameDhcpv4SrvTest, serverUpdateMalformedHostname) {
+ Pkt4Ptr query;
+
+ // Hostname should not be able to have an emtpy label.
+ ASSERT_NO_THROW(query = generatePktWithHostname(DHCPREQUEST,
+ "abc..example.com"));
+ OptionStringPtr hostname;
+ ASSERT_NO_THROW(hostname = processHostname(query));
+ EXPECT_FALSE(hostname);
+
+ // The following vector matches malformed hostname data produced by
+ // fuzz testing that causes an internal error in dns::Name parsing logic.
+ std::vector<uint8_t> badname {
+ 0xff,0xff,0x7f,0x00,0x00,0x00,0x7f,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x04,0x63,0x82,0x53,0x63,0x35,0x01,0x01,0x3d,0x07,0x01,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x19,0x0c,0x4e,0x01,0x00,0x07,0x08,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x35,0x01,0x05,0x3a,0x04,0x00,0x00,0x07,0x08,0x3b,0x04,0x00,
+ 0x00,0x2e,0x3b,0x04,0x00,0x19,0x2e,0x00,0x00,0x00,0x0a,0x00,0x12,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x0b,0x82,0x01,0xfc,0x42,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x35,0x01,0x05,0x3a,0x04,0x00,0x00,0x07,0x08,0x3b,0x04,
+ 0x00,0x00,0x2e,0x3b,0x04,0x00,0x19,0x2e,0x56,0x00,0x00,0x0a,0x00,0x12,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x0b,0x82,0x01,0xfc,0x42,0x00,0x00,0x00,0x00,0x19,0x0c,
+ 0x4e,0x01,0x05,0x3a,0x04,0xde,0x00,0x07,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x35,0x01,0x05,0x3a,0x07,0x08,0x3b,0x04,0x00,0x00,0x2e,0x3b,0x04,
+ 0x00,0x19,0x2e,0x56,0x40,0x00,0x00,0x00,0x00,0x00,0x0a,0x00,0x12,0x00,0x00,0x00,
+ 0x00,0x00,0x19,0x00,0x0b,0x82,0x01,0xfc,0x42,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0xfc,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x35,0x01,0x05,0xff,0xff,0x05,0x00,0x07,0x08,0x3b,0x04,
+ 0x00,0x00,0x2e,0x3b
+ };
+
+ std::string badnamestr(badname.begin(), badname.end());
+ ASSERT_NO_THROW(query = generatePktWithHostname(DHCPREQUEST,
+ badnamestr));
+ ASSERT_NO_THROW(hostname = processHostname(query));
+ EXPECT_FALSE(hostname);
+}
+
+// Test that the server does not see an empty Hostname option.
+// Suppressing the empty Hostname is done in libdhcp++ during
+// unpackcing, so technically we don't need this test but,
+// hey it's already written.
+TEST_F(NameDhcpv4SrvTest, serverUpdateEmptyHostname) {
+ Pkt4Ptr query;
+ ASSERT_NO_THROW(query = generatePktWithEmptyHostname(DHCPREQUEST));
+ OptionStringPtr hostname;
+ ASSERT_NO_THROW(hostname = processHostname(query));
+ EXPECT_FALSE(hostname);
+}
+
+// Test that server generates the fully qualified domain name for the client
+// if client supplies the partial name.
+TEST_F(NameDhcpv4SrvTest, serverUpdateForwardPartialNameFqdn) {
+ Pkt4Ptr query = generatePktWithFqdn(DHCPREQUEST,
+ Option4ClientFqdn::FLAG_E |
+ Option4ClientFqdn::FLAG_S,
+ "myhost",
+ Option4ClientFqdn::PARTIAL,
+ true);
+
+ testProcessFqdn(query,
+ Option4ClientFqdn::FLAG_E | Option4ClientFqdn::FLAG_S,
+ "myhost.example.com.");
+}
+
+// Test that server generates the fully qualified domain name for the client
+// if client supplies the unqualified name in the Hostname option.
+TEST_F(NameDhcpv4SrvTest, serverUpdateUnqualifiedHostname) {
+ Pkt4Ptr query;
+ ASSERT_NO_THROW(query = generatePktWithHostname(DHCPREQUEST, "myhost"));
+ OptionStringPtr hostname;
+ ASSERT_NO_THROW(hostname = processHostname(query));
+
+ ASSERT_TRUE(hostname);
+ EXPECT_EQ("myhost.example.com", hostname->getValue());
+}
+
+// Test that server sets empty domain-name in the FQDN option when client
+// supplied no domain-name. The domain-name is supposed to be set after the
+// lease is acquired. The domain-name is then generated from the IP address
+// assigned to a client.
+TEST_F(NameDhcpv4SrvTest, serverUpdateForwardNoNameFqdn) {
+ Pkt4Ptr query = generatePktWithFqdn(DHCPREQUEST,
+ Option4ClientFqdn::FLAG_E |
+ Option4ClientFqdn::FLAG_S,
+ "",
+ Option4ClientFqdn::PARTIAL,
+ true);
+
+ testProcessFqdn(query,
+ Option4ClientFqdn::FLAG_E | Option4ClientFqdn::FLAG_S,
+ "", Option4ClientFqdn::PARTIAL);
+}
+
+// Test that exactly one NameChangeRequest is generated when the new lease
+// has been acquired (old lease is NULL).
+TEST_F(NameDhcpv4SrvTest, createNameChangeRequestsNewLease) {
+ Lease4Ptr lease = createLease(IOAddress("192.0.2.3"), "myhost.example.com.",
+ true, true);
+ Lease4Ptr old_lease;
+
+ ASSERT_TRUE(getDdnsParams()->getEnableUpdates());
+
+ ASSERT_NO_THROW(srv_->createNameChangeRequests(lease, old_lease, *getDdnsParams()));
+ ASSERT_EQ(1, d2_mgr_.getQueueSize());
+
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ "192.0.2.3", "myhost.example.com.",
+ "00010132E91AA355CFBB753C0F0497A5A940436965"
+ "B68B6D438D98E680BF10B09F3BCF",
+ lease->cltt_, 100);
+}
+
+// Verify that conflict resolution is disabled in the NCR when it is
+// disabled for the lease's subnet.
+TEST_F(NameDhcpv4SrvTest, noConflictResolution) {
+ Lease4Ptr lease = createLease(IOAddress("192.0.2.3"), "myhost.example.com.",
+ true, true);
+ Lease4Ptr old_lease;
+
+ ASSERT_TRUE(getDdnsParams()->getEnableUpdates());
+ subnet_->setDdnsUseConflictResolution(false);
+
+ ASSERT_NO_THROW(srv_->createNameChangeRequests(lease, old_lease, *getDdnsParams()));
+ ASSERT_EQ(1, d2_mgr_.getQueueSize());
+
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ "192.0.2.3", "myhost.example.com.",
+ "00010132E91AA355CFBB753C0F0497A5A940436965"
+ "B68B6D438D98E680BF10B09F3BCF",
+ lease->cltt_, 100, false, false);
+}
+
+
+// Verifies that createNameChange request only generates requests
+// if the situation dictates that it should. It checks:
+//
+// -# enable-updates true or false
+// -# update-on-renew true or false
+// -# Whether or not there is an old lease
+// -# Whether or not the FQDN has changed between old and new lease
+TEST_F(NameDhcpv4SrvTest, createNameChangeRequestsUpdateOnRenew) {
+
+ Lease4Ptr lease1 = createLease(IOAddress("192.0.2.3"), "one.example.com.",
+ true, true);
+ // Comparison should be case insensitive, so turning some of the
+ // characters of the old lease hostname to upper case should not
+ // trigger NCRs.
+ Lease4Ptr lease2 = createLease(IOAddress("192.0.2.3"),
+ "two.example.com.", true, true);
+ struct Scenario {
+ std::string description_;
+ bool send_updates_;
+ bool update_on_renew_;
+ Lease4Ptr old_lease_;
+ Lease4Ptr new_lease_;
+ size_t remove_;
+ size_t add_;
+ };
+
+ // Mnemonic constants.
+ const bool send_updates = true;
+ const bool update_on_renew = true;
+ const size_t remove = 1;
+ const size_t add = 1;
+
+ const std::vector<Scenario> scenarios = {
+ {
+ "#1 update-on-renew false, no old lease",
+ send_updates, !update_on_renew, Lease4Ptr(), lease1, !remove, add
+ },
+ {
+ "#2 update-on-renew false, no change in fqdn",
+ send_updates, !update_on_renew, lease1, lease1, !remove, !add
+ },
+ {
+ "#3 update-on-renew is false, change in fqdn",
+ send_updates, !update_on_renew, lease1, lease2, remove, add
+ },
+ {
+ "#4 update-on-renew is true, no old lease",
+ send_updates, update_on_renew, Lease4Ptr(), lease1, !remove, add
+ },
+ {
+ "#5 update-on-renew is true, no change in fqdn",
+ send_updates, update_on_renew, lease1, lease1, remove, add
+ },
+ {
+ "#6 update-on-renew is true, change in fqdn",
+ send_updates, update_on_renew, lease1, lease2, remove, add
+ },
+ // All prior scenarios test with send-updates true. We really
+ // only need one with it false.
+ {
+ "#7 send-updates false, update-on-renew is true, change in fqdn",
+ !send_updates, update_on_renew, lease1, lease2, !remove, !add
+ }
+ };
+
+ // Iterate over test scenarios.
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE(scenario.description_); {
+ // Set and verify DDNS params flags
+ subnet_->setDdnsSendUpdates(scenario.send_updates_);
+ subnet_->setDdnsUpdateOnRenew(scenario.update_on_renew_);
+
+ ASSERT_EQ(scenario.send_updates_, getDdnsParams()->getEnableUpdates());
+ ASSERT_EQ(scenario.update_on_renew_, getDdnsParams()->getUpdateOnRenew());
+
+ // Call createNameChangeRequests()
+ ASSERT_NO_THROW(srv_->createNameChangeRequests(scenario.new_lease_,
+ scenario.old_lease_,
+ *getDdnsParams()));
+ // Verify queue count is correct.
+ ASSERT_EQ((scenario.remove_ + scenario.add_), d2_mgr_.getQueueSize());
+
+ // If we expect a remove, check it.
+ if (scenario.remove_ > 0) {
+ // Verify NCR content
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, true,
+ scenario.old_lease_->addr_.toText(),
+ scenario.old_lease_->hostname_, "", time(NULL),
+ scenario.old_lease_->valid_lft_, true);
+ }
+
+ // If we expect an add, check it.
+ if (scenario.add_ > 0) {
+ // Verify NCR content
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ scenario.new_lease_->addr_.toText(),
+ scenario.new_lease_->hostname_, "", time(NULL),
+ scenario.new_lease_->valid_lft_, true);
+ }
+ }
+ }
+}
+
+// Test that the OFFER message generated as a result of the DISCOVER message
+// processing will not result in generation of the NameChangeRequests.
+TEST_F(NameDhcpv4SrvTest, processDiscover) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ Pkt4Ptr req = generatePktWithFqdn(DHCPDISCOVER, Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_E,
+ "myhost.example.com.",
+ Option4ClientFqdn::FULL, true);
+
+ Pkt4Ptr reply;
+ ASSERT_NO_THROW(reply = srv_->processDiscover(req));
+ checkResponse(reply, DHCPOFFER, 1234);
+
+ EXPECT_EQ(0, d2_mgr_.getQueueSize());
+}
+
+// Test that server generates client's hostname from the IP address assigned
+// to it when DHCPv4 Client FQDN option specifies an empty domain-name.
+TEST_F(NameDhcpv4SrvTest, processRequestFqdnEmptyDomainName) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ Pkt4Ptr req = generatePktWithFqdn(DHCPREQUEST, Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_E,
+ "", Option4ClientFqdn::PARTIAL, true);
+
+ Pkt4Ptr reply;
+ ASSERT_NO_THROW(reply = srv_->processRequest(req));
+
+ checkResponse(reply, DHCPACK, 1234);
+
+ // Verify that there is one NameChangeRequest generated.
+ ASSERT_EQ(1, d2_mgr_.getQueueSize());
+
+ // The hostname is generated from the IP address acquired (yiaddr).
+ std::string hostname = generatedNameFromAddress(reply->getYiaddr());
+
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ reply->getYiaddr().toText(), hostname,
+ "", // empty DHCID forces that it is not checked
+ time(NULL) + subnet_->getValid(),
+ subnet_->getValid(), true);
+
+ req = generatePktWithFqdn(DHCPREQUEST, Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_E,
+ "", Option4ClientFqdn::PARTIAL, true);
+
+ ASSERT_NO_THROW(reply = srv_->processRequest(req));
+
+ checkResponse(reply, DHCPACK, 1234);
+
+ // Verify that there are no NameChangeRequests generated.
+ ASSERT_EQ(0, d2_mgr_.getQueueSize());
+}
+
+// Test that server generates client's hostname from the IP address assigned
+// to it when DHCPv4 Client FQDN option specifies an empty domain-name AND
+// ddns updates are disabled.
+TEST_F(NameDhcpv4SrvTest, processRequestEmptyDomainNameDisabled) {
+ // Create fake interfaces and open fake sockets.
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ disableD2();
+ Pkt4Ptr req = generatePktWithFqdn(DHCPREQUEST, Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_E,
+ "", Option4ClientFqdn::PARTIAL, true);
+ Pkt4Ptr reply;
+ ASSERT_NO_THROW(reply = srv_->processRequest(req));
+
+ checkResponse(reply, DHCPACK, 1234);
+
+ Option4ClientFqdnPtr fqdn = getClientFqdnOption(reply);
+ ASSERT_TRUE(fqdn);
+
+ // The hostname is generated from the IP address acquired (yiaddr).
+ std::string hostname = generatedNameFromAddress(reply->getYiaddr());
+
+ EXPECT_EQ(hostname, fqdn->getDomainName());
+ EXPECT_EQ(Option4ClientFqdn::FULL, fqdn->getDomainNameType());
+}
+
+
+// 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, processRequestTopLevelHostname) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ Pkt4Ptr req = generatePktWithHostname(DHCPREQUEST, ".");
+ // Set interface for the incoming packet. The server requires it to
+ // generate client id.
+ req->setIface("eth1");
+ req->setIndex(ETH1_INDEX);
+
+ Pkt4Ptr reply;
+ ASSERT_NO_THROW(reply = srv_->processRequest(req));
+
+ checkResponse(reply, DHCPACK, 1234);
+
+ // Verify that there is one NameChangeRequest generated.
+ ASSERT_EQ(1, d2_mgr_.getQueueSize());
+
+ // The hostname is generated from the IP address acquired (yiaddr).
+ std::string hostname = generatedNameFromAddress(reply->getYiaddr());
+
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ reply->getYiaddr().toText(), hostname,
+ "", // empty DHCID forces that it is not checked
+ time(NULL), subnet_->getValid(), true);
+}
+
+// Test that client may send two requests, each carrying FQDN option with
+// a different domain-name. Server should use existing lease for the second
+// request but modify the DNS entries for the lease according to the contents
+// of the FQDN sent in the second request.
+TEST_F(NameDhcpv4SrvTest, processTwoRequestsFqdn) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ Pkt4Ptr req1 = generatePktWithFqdn(DHCPREQUEST, Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_E,
+ "myhost.example.com.",
+ Option4ClientFqdn::FULL, true);
+
+ Pkt4Ptr reply;
+ ASSERT_NO_THROW(reply = srv_->processRequest(req1));
+
+ checkResponse(reply, DHCPACK, 1234);
+
+ // Verify that there is one NameChangeRequest generated.
+ ASSERT_EQ(1, d2_mgr_.getQueueSize());
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ reply->getYiaddr().toText(), "myhost.example.com.",
+ "00010132E91AA355CFBB753C0F0497A5A940436"
+ "965B68B6D438D98E680BF10B09F3BCF",
+ time(NULL), subnet_->getValid(), true);
+
+ // Create another Request message but with a different FQDN. Server
+ // should generate two NameChangeRequests: one to remove existing entry,
+ // another one to add new entry with updated domain-name.
+ Pkt4Ptr req2 = generatePktWithFqdn(DHCPREQUEST, Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_E,
+ "otherhost.example.com.",
+ Option4ClientFqdn::FULL, true);
+
+ ASSERT_NO_THROW(reply = srv_->processRequest(req2));
+
+ checkResponse(reply, DHCPACK, 1234);
+
+ // There should be two NameChangeRequests. Verify that they are valid.
+ ASSERT_EQ(2, d2_mgr_.getQueueSize());
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, true,
+ reply->getYiaddr().toText(),
+ "myhost.example.com.",
+ "00010132E91AA355CFBB753C0F0497A5A940436"
+ "965B68B6D438D98E680BF10B09F3BCF",
+ time(NULL), subnet_->getValid(), true);
+
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ reply->getYiaddr().toText(),
+ "otherhost.example.com.",
+ "000101A5AEEA7498BD5AD9D3BF600E49FF39A7E3"
+ "AFDCE8C3D0E53F35CC584DD63C89CA",
+ time(NULL), subnet_->getValid(), true);
+}
+
+// Test that client may send two requests, each carrying Hostname option with
+// a different name. Server should use existing lease for the second request
+// but modify the DNS entries for the lease according to the contents of the
+// Hostname sent in the second request.
+TEST_F(NameDhcpv4SrvTest, processTwoRequestsHostname) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Case in a hostname should be ignored.
+ Pkt4Ptr req1 = generatePktWithHostname(DHCPREQUEST, "Myhost.Example.Com.");
+
+ // Set interface for the incoming packet. The server requires it to
+ // generate client id.
+ req1->setIface("eth1");
+ req1->setIndex(ETH1_INDEX);
+
+ Pkt4Ptr reply;
+ ASSERT_NO_THROW(reply = srv_->processRequest(req1));
+
+ checkResponse(reply, DHCPACK, 1234);
+
+ // Verify that there is one NameChangeRequest generated.
+ ASSERT_EQ(1, d2_mgr_.getQueueSize());
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ reply->getYiaddr().toText(), "myhost.example.com.",
+ "00010132E91AA355CFBB753C0F0497A5A940436"
+ "965B68B6D438D98E680BF10B09F3BCF",
+ time(NULL), subnet_->getValid(), true);
+
+ // Create another Request message but with a different Hostname. Server
+ // should generate two NameChangeRequests: one to remove existing entry,
+ // another one to add new entry with updated domain-name.
+ Pkt4Ptr req2 = generatePktWithHostname(DHCPREQUEST, "otherhost");
+
+ // Set interface for the incoming packet. The server requires it to
+ // generate client id.
+ req2->setIface("eth1");
+ req2->setIndex(ETH1_INDEX);
+
+ ASSERT_NO_THROW(reply = srv_->processRequest(req2));
+
+ checkResponse(reply, DHCPACK, 1234);
+
+ // There should be two NameChangeRequests. Verify that they are valid.
+ ASSERT_EQ(2, d2_mgr_.getQueueSize());
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, true,
+ reply->getYiaddr().toText(),
+ "myhost.example.com.",
+ "00010132E91AA355CFBB753C0F0497A5A940436"
+ "965B68B6D438D98E680BF10B09F3BCF",
+ time(NULL), subnet_->getValid(), true);
+
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ reply->getYiaddr().toText(),
+ "otherhost.example.com.",
+ "000101A5AEEA7498BD5AD9D3BF600E49FF39A7E3"
+ "AFDCE8C3D0E53F35CC584DD63C89CA",
+ time(NULL), subnet_->getValid(), true);
+}
+
+// Test that client may send two requests, each carrying the same FQDN option.
+// Server should renew existing lease for the second request without generating
+// any NCRs.
+TEST_F(NameDhcpv4SrvTest, processRequestRenewFqdn) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ Pkt4Ptr req1 = generatePktWithFqdn(DHCPREQUEST, Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_E,
+ "myhost.example.com.",
+ Option4ClientFqdn::FULL, true);
+
+ Pkt4Ptr reply;
+ ASSERT_NO_THROW(reply = srv_->processRequest(req1));
+
+ checkResponse(reply, DHCPACK, 1234);
+
+ // Verify that there is one NameChangeRequest generated.
+ ASSERT_EQ(1, d2_mgr_.getQueueSize());
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ reply->getYiaddr().toText(), "myhost.example.com.",
+ "00010132E91AA355CFBB753C0F0497A5A940436"
+ "965B68B6D438D98E680BF10B09F3BCF",
+ time(NULL), subnet_->getValid(), true);
+
+ // Create another Request message with the same FQDN. Case changes in the
+ // hostname should be ignored. Server should generate no NameChangeRequests.
+ Pkt4Ptr req2 = generatePktWithFqdn(DHCPREQUEST, Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_E,
+ "Myhost.Example.Com.",
+ Option4ClientFqdn::FULL, true);
+
+ ASSERT_NO_THROW(reply = srv_->processRequest(req2));
+
+ checkResponse(reply, DHCPACK, 1234);
+
+ // There should be no NameChangeRequests.
+ ASSERT_EQ(0, d2_mgr_.getQueueSize());
+}
+
+// Test that client may send two requests, each carrying the same hostname
+// option. Server should renew existing lease for the second request without
+// generating any NCRs.
+TEST_F(NameDhcpv4SrvTest, processRequestRenewHostname) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ Pkt4Ptr req1 = generatePktWithHostname(DHCPREQUEST, "myhost.example.com.");
+
+ // Set interface for the incoming packet. The server requires it to
+ // generate client id.
+ req1->setIface("eth1");
+ req1->setIndex(ETH1_INDEX);
+
+ Pkt4Ptr reply;
+ ASSERT_NO_THROW(reply = srv_->processRequest(req1));
+
+ checkResponse(reply, DHCPACK, 1234);
+
+ // Verify that there is one NameChangeRequest generated.
+ ASSERT_EQ(1, d2_mgr_.getQueueSize());
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ reply->getYiaddr().toText(), "myhost.example.com.",
+ "00010132E91AA355CFBB753C0F0497A5A940436"
+ "965B68B6D438D98E680BF10B09F3BCF",
+ time(NULL), subnet_->getValid(), true);
+
+ // Create another Request message with the same Hostname. Case changes in the
+ // hostname should be ignored. Server should generate no NameChangeRequests.
+ Pkt4Ptr req2 = generatePktWithHostname(DHCPREQUEST, "Myhost.Example.Com.");
+
+ // Set interface for the incoming packet. The server requires it to
+ // generate client id.
+ req2->setIface("eth1");
+ req2->setIndex(ETH1_INDEX);
+
+ ASSERT_NO_THROW(reply = srv_->processRequest(req2));
+
+ checkResponse(reply, DHCPACK, 1234);
+
+ // There should be no NameChangeRequests.
+ ASSERT_EQ(0, d2_mgr_.getQueueSize());
+}
+
+// Test that when a release message is sent for a previously acquired lease,
+// DDNS updates are enabled and the server generates a NameChangeRequest
+// to remove entries corresponding to the released (deleted) lease.
+TEST_F(NameDhcpv4SrvTest, processRequestRelease) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ CfgMgr::instance().getCurrentCfg()->getCfgExpiration()->setFlushReclaimedTimerWaitTime(0);
+ CfgMgr::instance().getCurrentCfg()->getCfgExpiration()->setHoldReclaimedTime(0);
+
+ // Verify the updates are enabled.
+ ASSERT_TRUE(CfgMgr::instance().ddnsEnabled());
+
+ // Create and process a lease request so we have a lease to release.
+ Pkt4Ptr req = generatePktWithFqdn(DHCPREQUEST, Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_E,
+ "myhost.example.com.",
+ Option4ClientFqdn::FULL, true);
+ Pkt4Ptr reply;
+ ASSERT_NO_THROW(reply = srv_->processRequest(req));
+ checkResponse(reply, DHCPACK, 1234);
+
+ // Verify that there is one NameChangeRequest generated for lease.
+ ASSERT_EQ(1, d2_mgr_.getQueueSize());
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ reply->getYiaddr().toText(), "myhost.example.com.",
+ "00010132E91AA355CFBB753C0F0497A5A940436"
+ "965B68B6D438D98E680BF10B09F3BCF",
+ time(NULL), subnet_->getValid(), true);
+
+ // Create and process the Release message.
+ Pkt4Ptr rel = Pkt4Ptr(new Pkt4(DHCPRELEASE, 1234));
+ rel->setCiaddr(reply->getYiaddr());
+ rel->setRemoteAddr(IOAddress("192.0.2.3"));
+ rel->addOption(generateClientId());
+ rel->addOption(srv_->getServerID());
+ ASSERT_NO_THROW(srv_->processRelease(rel));
+
+ // The lease has been removed, so there should be a NameChangeRequest to
+ // remove corresponding DNS entries.
+ ASSERT_EQ(1, d2_mgr_.getQueueSize());
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, true,
+ reply->getYiaddr().toText(), "myhost.example.com.",
+ "00010132E91AA355CFBB753C0F0497A5A940436"
+ "965B68B6D438D98E680BF10B09F3BCF",
+ time(NULL), subnet_->getValid(), true);
+}
+
+// Test that when a release message is sent for a previously acquired lease,
+// DDNS updates are enabled and the server does not generate a NameChangeRequest
+// to remove entries corresponding to the released (expired) lease.
+TEST_F(NameDhcpv4SrvTest, processRequestReleaseNoDelete) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Verify the updates are enabled.
+ ASSERT_TRUE(CfgMgr::instance().ddnsEnabled());
+
+ // Create and process a lease request so we have a lease to release.
+ Pkt4Ptr req = generatePktWithFqdn(DHCPREQUEST, Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_E,
+ "myhost.example.com.",
+ Option4ClientFqdn::FULL, true);
+ Pkt4Ptr reply;
+ ASSERT_NO_THROW(reply = srv_->processRequest(req));
+ checkResponse(reply, DHCPACK, 1234);
+
+ // Verify that there is one NameChangeRequest generated for lease.
+ ASSERT_EQ(1, d2_mgr_.getQueueSize());
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ reply->getYiaddr().toText(), "myhost.example.com.",
+ "00010132E91AA355CFBB753C0F0497A5A940436"
+ "965B68B6D438D98E680BF10B09F3BCF",
+ time(NULL), subnet_->getValid(), true);
+
+ // Create and process the Release message.
+ Pkt4Ptr rel = Pkt4Ptr(new Pkt4(DHCPRELEASE, 1234));
+ rel->setCiaddr(reply->getYiaddr());
+ rel->setRemoteAddr(IOAddress("192.0.2.3"));
+ rel->addOption(generateClientId());
+ rel->addOption(srv_->getServerID());
+ ASSERT_NO_THROW(srv_->processRelease(rel));
+
+ // The lease has been expired, so there should not be a NameChangeRequest to
+ // remove corresponding DNS entries.
+ ASSERT_EQ(0, d2_mgr_.getQueueSize());
+}
+
+// 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);
+ IfaceMgr::instance().openSockets4();
+
+ // Disable DDNS.
+ disableD2();
+ ASSERT_FALSE(CfgMgr::instance().ddnsEnabled());
+
+ // Create and process a lease request so we have a lease to release.
+ Pkt4Ptr req = generatePktWithFqdn(DHCPREQUEST, Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_E,
+ "myhost.example.com.",
+ Option4ClientFqdn::FULL, true);
+ Pkt4Ptr reply;
+ ASSERT_NO_THROW(reply = srv_->processRequest(req));
+ checkResponse(reply, DHCPACK, 1234);
+
+ // Create and process the Release message.
+ Pkt4Ptr rel = Pkt4Ptr(new Pkt4(DHCPRELEASE, 1234));
+ rel->setCiaddr(reply->getYiaddr());
+ rel->setRemoteAddr(IOAddress("192.0.2.3"));
+ rel->addOption(generateClientId());
+ rel->addOption(srv_->getServerID());
+ ASSERT_NO_THROW(srv_->processRelease(rel));
+}
+
+// This test verifies that the server sends the FQDN option to the client
+// with the reserved hostname.
+TEST_F(NameDhcpv4SrvTest, fqdnReservation) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Use HW address that matches the reservation entry in the configuration.
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+ // Configure DHCP server.
+ configure(CONFIGS[0], *client.getServer());
+ // Make sure that DDNS is enabled.
+ ASSERT_TRUE(CfgMgr::instance().ddnsEnabled());
+ ASSERT_NO_THROW(client.getServer()->startD2());
+ // Include the Client FQDN option.
+ ASSERT_NO_THROW(client.includeFQDN(Option4ClientFqdn::FLAG_S | Option4ClientFqdn::FLAG_E,
+ "client-name", Option4ClientFqdn::PARTIAL));
+ // Send the DHCPDISCOVER.
+ ASSERT_NO_THROW(client.doDiscover());
+
+ // Make sure that the server responded.
+ Pkt4Ptr resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPOFFER, static_cast<int>(resp->getType()));
+
+ // Obtain the FQDN option sent in the response and make sure that the server
+ // has used the hostname reserved for this client.
+ Option4ClientFqdnPtr fqdn;
+ fqdn = boost::dynamic_pointer_cast<Option4ClientFqdn>(resp->getOption(DHO_FQDN));
+ ASSERT_TRUE(fqdn);
+ EXPECT_EQ("unique-host.example.com.", fqdn->getDomainName());
+
+ // When receiving DHCPDISCOVER, no NCRs should be generated.
+ EXPECT_EQ(0, d2_mgr_.getQueueSize());
+
+ // Now send the DHCPREQUEST with including the FQDN option.
+ ASSERT_NO_THROW(client.doRequest());
+ resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Once again check that the FQDN is as expected.
+ fqdn = boost::dynamic_pointer_cast<Option4ClientFqdn>(resp->getOption(DHO_FQDN));
+ ASSERT_TRUE(fqdn);
+ EXPECT_EQ("unique-host.example.com.", fqdn->getDomainName());
+
+ {
+ SCOPED_TRACE("Verify the correctness of the NCR for the"
+ "unique-host.example.com");
+
+ // Because this is a new lease, there should be one NCR which adds the
+ // new DNS entry.
+ ASSERT_EQ(1, CfgMgr::instance().getD2ClientMgr().getQueueSize());
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ resp->getYiaddr().toText(),
+ "unique-host.example.com.",
+ "000001B6547DCC62E44C4D1A42D0A05B149EA1168"
+ "01A9481A98E3A876A9E0D261F8326",
+ time(NULL), subnet_->getValid(), true);
+ }
+
+ // And that this FQDN has been stored in the lease database.
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(client.config_.lease_.addr_);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("unique-host.example.com.", lease->hostname_);
+
+ // Reconfigure DHCP server to use a different hostname for the client.
+ configure(CONFIGS[1], *client.getServer());
+ // Make sure that DDNS is enabled.
+ ASSERT_TRUE(CfgMgr::instance().ddnsEnabled());
+ ASSERT_NO_THROW(client.getServer()->startD2());
+
+ // Client is in the renewing state.
+ client.setState(Dhcp4Client::RENEWING);
+ client.doRequest();
+ resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // The new FQDN should contain a different name this time.
+ fqdn = boost::dynamic_pointer_cast<Option4ClientFqdn>(resp->getOption(DHO_FQDN));
+ ASSERT_TRUE(fqdn);
+ EXPECT_EQ("foobar.fake-suffix.isc.org.", fqdn->getDomainName());
+
+ // And the lease in the lease database should also contain this new FQDN.
+ lease = LeaseMgrFactory::instance().getLease4(client.config_.lease_.addr_);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("foobar.fake-suffix.isc.org.", lease->hostname_);
+
+ // Now there should be two name NCRs. One that removes the previous entry
+ // and the one that adds a new entry for the new hostname.
+ ASSERT_EQ(2, CfgMgr::instance().getD2ClientMgr().getQueueSize());
+
+ {
+ SCOPED_TRACE("Verify the correctness of the CHG_REMOVE NCR for the "
+ "unique-host.example.com");
+
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, true,
+ resp->getYiaddr().toText(),
+ "unique-host.example.com.",
+ "000001B6547DCC62E44C4D1A42D0A05B149EA1168"
+ "01A9481A98E3A876A9E0D261F8326",
+ time(NULL), subnet_->getValid(), true);
+ }
+
+ {
+ SCOPED_TRACE("Verify the correctness of the CHG_ADD NCR for the "
+ "foobar.fake-suffix.isc.org");
+
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ resp->getYiaddr().toText(),
+ "foobar.fake-suffix.isc.org.",
+ "0000017C29B3C236344924E448E247F3FD56C7E9"
+ "167B3397B1305FB664C160B967CE1F",
+ time(NULL), subnet_->getValid(), true);
+ }
+}
+
+// This test verifies that the server sends the Hostname option to the client
+// with the reserved hostname.
+TEST_F(NameDhcpv4SrvTest, hostnameReservation) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Use HW address that matches the reservation entry in the configuration.
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+ // Configure DHCP server.
+ configure(CONFIGS[0], *client.getServer());
+ // Make sure that DDNS is enabled.
+ ASSERT_TRUE(CfgMgr::instance().ddnsEnabled());
+ ASSERT_NO_THROW(client.getServer()->startD2());
+ // Include the Hostname option.
+ ASSERT_NO_THROW(client.includeHostname("client-name"));
+
+ // Send the DHCPDISCOVER
+ ASSERT_NO_THROW(client.doDiscover());
+
+ // Make sure that the server responded.
+ Pkt4Ptr resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPOFFER, static_cast<int>(resp->getType()));
+
+ // Obtain the Hostname option sent in the response and make sure that the server
+ // has used the hostname reserved for this client.
+ OptionStringPtr hostname;
+ hostname = boost::dynamic_pointer_cast<OptionString>(resp->getOption(DHO_HOST_NAME));
+ ASSERT_TRUE(hostname);
+ EXPECT_EQ("unique-host.example.com", hostname->getValue());
+
+ // Now send the DHCPREQUEST with including the Hostname option.
+ ASSERT_NO_THROW(client.doRequest());
+ resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Once again check that the Hostname is as expected.
+ hostname = boost::dynamic_pointer_cast<OptionString>(resp->getOption(DHO_HOST_NAME));
+ ASSERT_TRUE(hostname);
+ EXPECT_EQ("unique-host.example.com", hostname->getValue());
+
+ // And that this hostname has been stored in the lease database.
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(client.config_.lease_.addr_);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("unique-host.example.com", lease->hostname_);
+
+ // Because this is a new lease, there should be one NCR which adds the
+ // new DNS entry.
+ ASSERT_EQ(1, CfgMgr::instance().getD2ClientMgr().getQueueSize());
+ {
+ SCOPED_TRACE("Verify the correctness of the NCR for the"
+ "unique-host.example.com");
+
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ resp->getYiaddr().toText(),
+ "unique-host.example.com.",
+ "000001B6547DCC62E44C4D1A42D0A05B149EA1168"
+ "01A9481A98E3A876A9E0D261F8326",
+ time(NULL), subnet_->getValid(), true);
+ }
+
+ // Reconfigure DHCP server to use a different hostname for the client.
+ configure(CONFIGS[1], *client.getServer());
+ // Make sure that DDNS is enabled.
+ ASSERT_TRUE(CfgMgr::instance().ddnsEnabled());
+ ASSERT_NO_THROW(client.getServer()->startD2());
+
+ // Client is in the renewing state.
+ client.setState(Dhcp4Client::RENEWING);
+ client.doRequest();
+ resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // The new hostname should be different than previously.
+ hostname = boost::dynamic_pointer_cast<OptionString>(resp->getOption(DHO_HOST_NAME));
+ ASSERT_TRUE(hostname);
+ EXPECT_EQ("foobar.fake-suffix.isc.org", hostname->getValue());
+
+ // And the lease in the lease database should also contain this new FQDN.
+ lease = LeaseMgrFactory::instance().getLease4(client.config_.lease_.addr_);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("foobar.fake-suffix.isc.org", lease->hostname_);
+
+ // Now there should be two name NCRs. One that removes the previous entry
+ // and the one that adds a new entry for the new hostname.
+ ASSERT_EQ(2, CfgMgr::instance().getD2ClientMgr().getQueueSize());
+ {
+ SCOPED_TRACE("Verify the correctness of the CHG_REMOVE NCR for the "
+ "unique-host.example.com");
+
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, true,
+ resp->getYiaddr().toText(),
+ "unique-host.example.com.",
+ "000001B6547DCC62E44C4D1A42D0A05B149EA1168"
+ "01A9481A98E3A876A9E0D261F8326",
+ time(NULL), subnet_->getValid(), true);
+ }
+
+ {
+ SCOPED_TRACE("Verify the correctness of the CHG_ADD NCR for the "
+ "foobar.fake-suffix.isc.org");
+
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ resp->getYiaddr().toText(),
+ "foobar.fake-suffix.isc.org.",
+ "0000017C29B3C236344924E448E247F3FD56C7E9"
+ "167B3397B1305FB664C160B967CE1F",
+ time(NULL), subnet_->getValid(), true);
+ }
+}
+
+// This test verifies that the server sends the Hostname option to the client
+// with hostname reservation and which included hostname option code in the
+// Parameter Request List.
+TEST_F(NameDhcpv4SrvTest, hostnameReservationPRL) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Use HW address that matches the reservation entry in the configuration.
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+ // Configure DHCP server.
+ configure(CONFIGS[4], *client.getServer());
+ // Make sure that DDNS is enabled.
+ ASSERT_FALSE(CfgMgr::instance().ddnsEnabled());
+ // Request Hostname option.
+ ASSERT_NO_THROW(client.requestOption(DHO_HOST_NAME));
+
+ // Send the DHCPDISCOVER
+ ASSERT_NO_THROW(client.doDiscover());
+
+ // Make sure that the server responded.
+ Pkt4Ptr resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPOFFER, static_cast<int>(resp->getType()));
+
+ // Obtain the Hostname option sent in the response and make sure that the server
+ // has used the hostname reserved for this client.
+ OptionStringPtr hostname;
+ hostname = boost::dynamic_pointer_cast<OptionString>(resp->getOption(DHO_HOST_NAME));
+ ASSERT_TRUE(hostname);
+ EXPECT_EQ("reserved.example.com", hostname->getValue());
+
+ // Now send the DHCPREQUEST with including the Hostname option.
+ ASSERT_NO_THROW(client.doRequest());
+ resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Once again check that the Hostname is as expected.
+ hostname = boost::dynamic_pointer_cast<OptionString>(resp->getOption(DHO_HOST_NAME));
+ ASSERT_TRUE(hostname);
+ EXPECT_EQ("reserved.example.com", hostname->getValue());
+}
+
+// This test verifies that the server sends the Hostname option to the client
+// with partial hostname reservation and with the global qualifying-suffix set.
+TEST_F(NameDhcpv4SrvTest, hostnameReservationNoDNSQualifyingSuffix) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Use HW address that matches the reservation entry in the configuration.
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+ // Configure DHCP server.
+ configure(CONFIGS[5], *client.getServer());
+ // Make sure that DDNS is enabled.
+ ASSERT_FALSE(CfgMgr::instance().ddnsEnabled());
+ // Include the Hostname option.
+ ASSERT_NO_THROW(client.includeHostname("client-name"));
+
+ // Send the DHCPDISCOVER
+ ASSERT_NO_THROW(client.doDiscover());
+
+ // Make sure that the server responded.
+ Pkt4Ptr resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPOFFER, static_cast<int>(resp->getType()));
+
+ // Obtain the Hostname option sent in the response and make sure that the server
+ // has used the hostname reserved for this client.
+ OptionStringPtr hostname;
+ hostname = boost::dynamic_pointer_cast<OptionString>(resp->getOption(DHO_HOST_NAME));
+ ASSERT_TRUE(hostname);
+ EXPECT_EQ("foo-bar.example.isc.org", hostname->getValue());
+
+ // Now send the DHCPREQUEST with including the Hostname option.
+ ASSERT_NO_THROW(client.doRequest());
+ resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Once again check that the Hostname is as expected.
+ hostname = boost::dynamic_pointer_cast<OptionString>(resp->getOption(DHO_HOST_NAME));
+ ASSERT_TRUE(hostname);
+ EXPECT_EQ("foo-bar.example.isc.org", hostname->getValue());
+}
+
+// Test verifies that the server properly generates a FQDN when the client
+// FQDN name is blank, whether or not DDNS updates are enabled. It also
+// verifies that the lease is only in the database following a DHCPREQUEST and
+// that the lease contains the generated FQDN.
+TEST_F(NameDhcpv4SrvTest, emptyFqdn) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ isc::asiolink::IOAddress expected_address("10.0.0.10");
+ std::string expected_fqdn("myhost-10-0-0-10.fake-suffix.isc.org.");
+
+ // Load a configuration with DDNS updates disabled
+ configure(CONFIGS[2], *client.getServer());
+ ASSERT_FALSE(CfgMgr::instance().ddnsEnabled());
+
+ // Include the Client FQDN option.
+ ASSERT_NO_THROW(client.includeFQDN((Option4ClientFqdn::FLAG_S
+ | Option4ClientFqdn::FLAG_E),
+ "", Option4ClientFqdn::PARTIAL));
+
+ // Send the DHCPDISCOVER
+ ASSERT_NO_THROW(client.doDiscover());
+
+ // Make sure that the server responded.
+ Pkt4Ptr resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPOFFER, static_cast<int>(resp->getType()));
+
+ // Make sure the response FQDN has the generated name and FQDN flags are
+ // correct for updated disabled.
+ Option4ClientFqdnPtr fqdn;
+ fqdn = boost::dynamic_pointer_cast<Option4ClientFqdn>(resp->getOption(DHO_FQDN));
+ ASSERT_TRUE(fqdn);
+ EXPECT_EQ(expected_fqdn, fqdn->getDomainName());
+ checkFqdnFlags(resp, (Option4ClientFqdn::FLAG_N |
+ Option4ClientFqdn::FLAG_E |
+ Option4ClientFqdn::FLAG_O));
+
+ // Make sure the lease is NOT in the database.
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(IOAddress(expected_address));
+ ASSERT_FALSE(lease);
+
+ // Now test with updates enabled
+ configure(CONFIGS[3], *client.getServer());
+ ASSERT_TRUE(CfgMgr::instance().ddnsEnabled());
+ ASSERT_NO_THROW(client.getServer()->startD2());
+
+ // Send the DHCPDISCOVER
+ ASSERT_NO_THROW(client.doDiscover());
+
+ // Make sure that the server responded.
+ resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPOFFER, static_cast<int>(resp->getType()));
+
+ // Make sure the response FQDN has the generated name and FQDN flags are
+ // correct for updates enabled.
+ fqdn = boost::dynamic_pointer_cast<Option4ClientFqdn>(resp->getOption(DHO_FQDN));
+ ASSERT_TRUE(fqdn);
+ EXPECT_EQ(expected_fqdn, fqdn->getDomainName());
+ checkFqdnFlags(resp, (Option4ClientFqdn::FLAG_E |
+ Option4ClientFqdn::FLAG_S));
+
+ // Make sure the lease is NOT in the database.
+ lease = LeaseMgrFactory::instance().getLease4(IOAddress(expected_address));
+ ASSERT_FALSE(lease);
+
+ // Do a DORA and verify that the lease exists and the name is correct.
+ ASSERT_NO_THROW(client.doDORA());
+
+ // Make sure that the server responded.
+ resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Make sure the response FQDN has the generated name and FQDN flags are
+ // correct for updates enabled.
+ fqdn = boost::dynamic_pointer_cast<Option4ClientFqdn>(resp->getOption(DHO_FQDN));
+ ASSERT_TRUE(fqdn);
+ EXPECT_EQ(expected_fqdn, fqdn->getDomainName());
+ checkFqdnFlags(resp, (Option4ClientFqdn::FLAG_E |
+ Option4ClientFqdn::FLAG_S));
+
+ // Make sure the lease is in the database and hostname is correct.
+ lease = LeaseMgrFactory::instance().getLease4(IOAddress(expected_address));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(expected_fqdn, lease->hostname_);
+}
+
+// Verifies that the replace-client-name behavior is correct for each of
+// the supported modes.
+TEST_F(NameDhcpv4SrvTest, replaceClientNameModeTest) {
+
+ testReplaceClientNameMode("never",
+ CLIENT_NAME_NOT_PRESENT, NAME_NOT_REPLACED);
+ testReplaceClientNameMode("never",
+ CLIENT_NAME_PRESENT, NAME_NOT_REPLACED);
+
+ testReplaceClientNameMode("always",
+ CLIENT_NAME_NOT_PRESENT, NAME_REPLACED);
+ testReplaceClientNameMode("always",
+ CLIENT_NAME_PRESENT, NAME_REPLACED);
+
+ testReplaceClientNameMode("when-present",
+ CLIENT_NAME_NOT_PRESENT, NAME_NOT_REPLACED);
+ testReplaceClientNameMode("when-present",
+ CLIENT_NAME_PRESENT, NAME_REPLACED);
+
+ testReplaceClientNameMode("when-not-present",
+ CLIENT_NAME_NOT_PRESENT, NAME_REPLACED);
+ testReplaceClientNameMode("when-not-present",
+ CLIENT_NAME_PRESENT, NAME_NOT_REPLACED);
+}
+
+// Verifies that default hostname-char-set sanitizes Hostname option
+// values received from clients.
+TEST_F(NameDhcpv4SrvTest, sanitizeHostDefault) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ // Configure DHCP server.
+ configure(CONFIGS[2], *client.getServer());
+
+ // Make sure that DDNS is not enabled.
+ ASSERT_FALSE(CfgMgr::instance().ddnsEnabled());
+
+ struct Scenario {
+ std::string description_;
+ std::string original_;
+ std::string sanitized_;
+ };
+
+ std::vector<Scenario> scenarios = {
+ {
+ "unqualified host name with invalid characters",
+ "one-&$_-host",
+ "one--host.fake-suffix.isc.org"
+ },
+ {
+ "qualified host name with invalid characters",
+ "two--host.other.org",
+ "two--host.other.org"
+ },
+ {
+ "unqualified host name with all valid characters",
+ "three-ok-host",
+ "three-ok-host.fake-suffix.isc.org"
+ },
+ {
+ "qualified host name with valid characters",
+ "four-ok-host.other.org",
+ "four-ok-host.other.org"
+ },
+ {
+ "qualified host name with nuls",
+ std::string("four-ok-host\000.other.org",23),
+ "four-ok-host.other.org"
+ }
+ };
+
+ Pkt4Ptr resp;
+ OptionStringPtr hostname;
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE((scenario).description_);
+ {
+ // Set the hostname option.
+ ASSERT_NO_THROW(client.includeHostname((scenario).original_));
+
+ // Send the DHCPDISCOVER and make sure that the server responded.
+ ASSERT_NO_THROW(client.doDiscover());
+ resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPOFFER, static_cast<int>(resp->getType()));
+
+ // Make sure the response hostname is what we expect.
+ hostname = boost::dynamic_pointer_cast<OptionString>(resp->getOption(DHO_HOST_NAME));
+ ASSERT_TRUE(hostname);
+ EXPECT_EQ((scenario).sanitized_, hostname->getValue());
+ }
+ }
+}
+
+
+// Verifies that setting hostname-char-set sanitizes Hostname option
+// values received from clients.
+TEST_F(NameDhcpv4SrvTest, sanitizeHost) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ // Configure DHCP server.
+ configure(CONFIGS[6], *client.getServer());
+
+ // Make sure that DDNS is enabled.
+ ASSERT_TRUE(CfgMgr::instance().ddnsEnabled());
+ ASSERT_NO_THROW(client.getServer()->startD2());
+
+ struct Scenario {
+ std::string description_;
+ std::string original_;
+ std::string sanitized_;
+ };
+
+ std::vector<Scenario> scenarios = {
+ {
+ "unqualified host name with invalid characters",
+ "one-&$_-host",
+ "one-xxx-host.example.com"
+ },
+ {
+ "qualified host name with invalid characters",
+ "two-&$_-host.other.org",
+ "two-xxx-host.other.org"
+ },
+ {
+ "unqualified host name with all valid characters",
+ "three-ok-host",
+ "three-ok-host.example.com"
+ },
+ {
+ "qualified host name with valid characters",
+ "four-ok-host.other.org",
+ "four-ok-host.other.org"
+ },
+ {
+ "qualified host name with nuls",
+ std::string("four-ok-host\000.other.org",23),
+ "four-ok-hostx.other.org"
+ }
+ };
+
+ Pkt4Ptr resp;
+ OptionStringPtr hostname;
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE((scenario).description_);
+ {
+ // Set the hostname option.
+ ASSERT_NO_THROW_LOG(client.includeHostname((scenario).original_));
+
+ // Send the DHCPDISCOVER and make sure that the server responded.
+ ASSERT_NO_THROW(client.doDiscover());
+ resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPOFFER, static_cast<int>(resp->getType()));
+
+ // Make sure the response hostname is what we expect.
+ hostname = boost::dynamic_pointer_cast<OptionString>(resp->getOption(DHO_HOST_NAME));
+ ASSERT_TRUE(hostname);
+ EXPECT_EQ((scenario).sanitized_, hostname->getValue());
+ }
+ }
+}
+
+// Verifies that setting global hostname-char-set sanitizes Hostname option
+// values received from clients.
+TEST_F(NameDhcpv4SrvTest, sanitizeHostGlobal) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ // Configure DHCP server.
+ configure(CONFIGS[7], *client.getServer());
+
+ // Make sure that DDNS is not enabled.
+ ASSERT_FALSE(CfgMgr::instance().ddnsEnabled());
+
+ struct Scenario {
+ std::string description_;
+ std::string original_;
+ std::string sanitized_;
+ };
+
+ std::vector<Scenario> scenarios = {
+ {
+ "unqualified host name with invalid characters",
+ "one-&$_-host",
+ "one-xxx-host"
+ },
+ {
+ "qualified host name with invalid characters",
+ "two-&$_-host.other.org",
+ "two-xxx-host.other.org"
+ },
+ {
+ "unqualified host name with all valid characters",
+ "three-ok-host",
+ "three-ok-host"
+ },
+ {
+ "qualified host name with valid characters",
+ "four-ok-host.other.org",
+ "four-ok-host.other.org"
+ }
+ };
+
+ Pkt4Ptr resp;
+ OptionStringPtr hostname;
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE((scenario).description_);
+ {
+ // Set the hostname option.
+ ASSERT_NO_THROW(client.includeHostname((scenario).original_));
+
+ // Send the DHCPDISCOVER and make sure that the server responded.
+ ASSERT_NO_THROW(client.doDiscover());
+ resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPOFFER, static_cast<int>(resp->getType()));
+
+ // Make sure the response hostname is what we expect.
+ hostname = boost::dynamic_pointer_cast<OptionString>(resp->getOption(DHO_HOST_NAME));
+ ASSERT_TRUE(hostname);
+ EXPECT_EQ((scenario).sanitized_, hostname->getValue());
+ }
+ }
+}
+
+// Verifies that setting hostname-char-set sanitizes FQDN option
+// values received from clients.
+TEST_F(NameDhcpv4SrvTest, sanitizeFqdn) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ // Configure DHCP server.
+ configure(CONFIGS[6], *client.getServer());
+
+ // Make sure that DDNS is enabled.
+ ASSERT_TRUE(CfgMgr::instance().ddnsEnabled());
+ ASSERT_NO_THROW(client.getServer()->startD2());
+
+ struct Scenario {
+ std::string description_;
+ std::string original_;
+ Option4ClientFqdn::DomainNameType name_type_;
+ std::string sanitized_;
+ };
+
+ std::vector<Scenario> scenarios = {
+ {
+ "unqualified FQDN with invalid characters",
+ "one-&*_-host",
+ Option4ClientFqdn::PARTIAL,
+ "one-xxx-host.example.com."
+ },
+ {
+ "qualified FQDN with invalid characters",
+ "two-&*_-host.other.org",
+ Option4ClientFqdn::FULL,
+ "two-xxx-host.other.org."
+ },
+ {
+ "unqualified FQDN name with all valid characters",
+ "three-ok-host",
+ Option4ClientFqdn::PARTIAL,
+ "three-ok-host.example.com."
+ },
+ {
+ "qualified FQDN name with valid characters",
+ "four-ok-host.other.org",
+ Option4ClientFqdn::FULL,
+ "four-ok-host.other.org."
+ }
+ };
+
+ Pkt4Ptr resp;
+ Option4ClientFqdnPtr fqdn;
+ for (auto scenario = scenarios.begin(); scenario != scenarios.end(); ++scenario) {
+ SCOPED_TRACE((*scenario).description_);
+ {
+ // Set the hostname option.
+ ASSERT_NO_THROW(client.includeHostname((*scenario).original_));
+ ASSERT_NO_THROW(client.includeFQDN(0, (*scenario).original_, (*scenario).name_type_));
+
+ // Send the DHCPDISCOVER and make sure that the server responded.
+ ASSERT_NO_THROW(client.doDiscover());
+ resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPOFFER, static_cast<int>(resp->getType()));
+
+ // Make sure the response fqdn is what we expect.
+ fqdn = boost::dynamic_pointer_cast<Option4ClientFqdn>(resp->getOption(DHO_FQDN));
+ ASSERT_TRUE(fqdn);
+ EXPECT_EQ((*scenario).sanitized_, fqdn->getDomainName());
+ }
+ }
+}
+
+// Verifies that setting global hostname-char-set sanitizes FQDN option
+// values received from clients.
+TEST_F(NameDhcpv4SrvTest, sanitizeFqdnGlobal) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ // Configure DHCP server.
+ configure(CONFIGS[7], *client.getServer());
+
+ // Make sure that DDNS is not enabled.
+ ASSERT_FALSE(CfgMgr::instance().ddnsEnabled());
+
+ struct Scenario {
+ std::string description_;
+ std::string original_;
+ Option4ClientFqdn::DomainNameType name_type_;
+ std::string sanitized_;
+ };
+
+ std::vector<Scenario> scenarios = {
+ {
+ "unqualified FQDN with invalid characters",
+ "one-&*_-host",
+ Option4ClientFqdn::PARTIAL,
+ "one-xxx-host."
+ },
+ {
+ "qualified FQDN with invalid characters",
+ "two-&*_-host.other.org",
+ Option4ClientFqdn::FULL,
+ "two-xxx-host.other.org."
+ },
+ {
+ "unqualified FQDN name with all valid characters",
+ "three-ok-host",
+ Option4ClientFqdn::PARTIAL,
+ "three-ok-host."
+ },
+ {
+ "qualified FQDN name with valid characters",
+ "four-ok-host.other.org",
+ Option4ClientFqdn::FULL,
+ "four-ok-host.other.org."
+ },
+ {
+ "qualified FQDN name with nuls",
+ std::string("four-ok-host.ot\000\000her.org", 24),
+ Option4ClientFqdn::FULL,
+ "four-ok-host.otxxher.org."
+ }
+ };
+
+ Pkt4Ptr resp;
+ Option4ClientFqdnPtr fqdn;
+ for (auto scenario = scenarios.begin(); scenario != scenarios.end(); ++scenario) {
+ SCOPED_TRACE((*scenario).description_);
+ {
+ // Set the hostname option.
+ ASSERT_NO_THROW(client.includeHostname((*scenario).original_));
+ ASSERT_NO_THROW(client.includeFQDN(0, (*scenario).original_, (*scenario).name_type_));
+
+ // Send the DHCPDISCOVER and make sure that the server responded.
+ ASSERT_NO_THROW(client.doDiscover());
+ resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPOFFER, static_cast<int>(resp->getType()));
+
+ // Make sure the response fqdn is what we expect.
+ fqdn = boost::dynamic_pointer_cast<Option4ClientFqdn>(resp->getOption(DHO_FQDN));
+ ASSERT_TRUE(fqdn);
+ EXPECT_EQ((*scenario).sanitized_, fqdn->getDomainName());
+ }
+ }
+}
+
+// Verifies that scoped ddns-parameter handling.
+// Specifically that D2 can be enabled with sending updates
+// disabled globally, and enabled at the subnet level.
+TEST_F(NameDhcpv4SrvTest, ddnsScopeTest) {
+ Dhcp4Client client1(Dhcp4Client::SELECTING);
+ client1.setIfaceName("eth0");
+ client1.setIfaceIndex(ETH0_INDEX);
+
+ // Load a configuration with D2 enabled
+ ASSERT_NO_FATAL_FAILURE(configure(CONFIGS[8], *client1.getServer()));
+ ASSERT_TRUE(CfgMgr::instance().ddnsEnabled());
+
+ // Include the Client FQDN option.
+ ASSERT_NO_THROW(client1.includeFQDN((Option4ClientFqdn::FLAG_S
+ | Option4ClientFqdn::FLAG_E),
+ "client1.example.com.",
+ Option4ClientFqdn::FULL));
+
+ // Now send the DHCPREQUEST with including the FQDN option.
+ ASSERT_NO_THROW(client1.doDORA());
+ Pkt4Ptr resp = client1.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Check that the response FQDN is as expected.
+ Option4ClientFqdnPtr fqdn;
+ fqdn = boost::dynamic_pointer_cast<Option4ClientFqdn>(resp->getOption(DHO_FQDN));
+ ASSERT_TRUE(fqdn);
+ EXPECT_EQ("client1.example.com.", fqdn->getDomainName());
+
+ // ddns-send-updates for subnet 1 should be off, so we should NOT have an NRC.
+ ASSERT_EQ(0, CfgMgr::instance().getD2ClientMgr().getQueueSize());
+
+ // Now let's try with a client on subnet 2.
+ Dhcp4Client client2(Dhcp4Client::SELECTING);
+ client2.setIfaceName("eth1");
+ client2.setIfaceIndex(ETH1_INDEX);
+
+ // Include the Client FQDN option.
+ ASSERT_NO_THROW(client2.includeFQDN((Option4ClientFqdn::FLAG_S
+ | Option4ClientFqdn::FLAG_E),
+ "two.example.com.",
+ Option4ClientFqdn::FULL));
+
+ ASSERT_NO_THROW(client2.doDORA());
+ resp = client2.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Check that the response FQDN is as expected.
+ fqdn = boost::dynamic_pointer_cast<Option4ClientFqdn>(resp->getOption(DHO_FQDN));
+ ASSERT_TRUE(fqdn);
+ EXPECT_EQ("two.example.com.", fqdn->getDomainName());
+
+ // ddns-send-updates for subnet 2 are enabled, verify the NCR is correct.
+ ASSERT_EQ(1, CfgMgr::instance().getD2ClientMgr().getQueueSize());
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ resp->getYiaddr().toText(),
+ "two.example.com.", "",
+ time(NULL), 7200, true);
+}
+
+// Verifies that when reusing an expired lease, whether or not it is given to its
+// original owner or not, appropriate DNS updates are done if needed.
+TEST_F(NameDhcpv4SrvTest, processReuseExpired) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Configure with a one address pool with DDNS enabled.
+ configure(CONFIGS[9], *srv_);
+
+ struct Scenario {
+ std::string label_;
+ std::string client_id1_;
+ std::string dhcid1_;
+ std::string hostname1_;
+
+ std::string client_id2_;
+ std::string dhcid2_;
+ std::string hostname2_;
+ bool expect_remove_;
+ bool expect_add_;
+ };
+
+ std::string cid1 = "11:11:11:11";
+ std::string dhcid1 = "0001013904F56A5BD4E926EB6BC36C825CEA1159FF2AFBE28E4391E67CC040F6A35785";
+ std::string cid2 = "22:22:22:22";
+ std::string dhcid2 = "000101459343356AC37A73A372ECE989F9C4397E7FBBD6658239EA4B3B77B6B904A46F";
+ bool remove = true;
+ bool add = true;
+
+ std::vector<Scenario> scenarios = {
+ {
+ "same client, hostname added",
+ cid1, dhcid1, "",
+ cid1, dhcid1, "one.example.com.",
+ !remove, add
+ },
+ {
+ "same client, same host",
+ cid1, dhcid1, "one.example.com.",
+ cid1, dhcid1, "one.example.com.",
+ remove, add
+ },
+ {
+ "same client, hostname removed",
+ cid1, dhcid1, "one.example.com.",
+ cid1, dhcid1, "",
+ remove, !add
+ },
+ {
+ "different client, hostname added",
+ cid1, dhcid1, "",
+ cid2, dhcid2, "two.example.com.",
+ !remove, add
+ },
+ {
+ "different client, different host",
+ cid1, dhcid1, "one.example.com.",
+ cid2, dhcid2, "two.example.com.",
+ remove, add
+ },
+ {
+ "different client, hostname removed",
+ cid1, dhcid1, "one.example.com.",
+ cid2, dhcid2, "",
+ remove, !add
+ }
+ };
+
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE(scenario.label_);
+ {
+ // Create the original leasing client.
+ boost::shared_ptr<Dhcp4Client> client1(new Dhcp4Client(srv_, Dhcp4Client::SELECTING));
+ client1->setIfaceName("eth1");
+ client1->setIfaceIndex(ETH1_INDEX);
+ client1->includeClientId(scenario.client_id1_);
+ if (!scenario.hostname1_.empty()) {
+ ASSERT_NO_THROW(client1->includeHostname(scenario.hostname1_));
+ }
+
+ ASSERT_NO_THROW(client1->doDORA());
+ Pkt4Ptr resp = client1->getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Verify that there is one NameChangeRequest generated.
+ if (scenario.hostname1_.empty()) {
+ ASSERT_EQ(0, d2_mgr_.getQueueSize());
+ } else {
+ ASSERT_EQ(1, d2_mgr_.getQueueSize());
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD,
+ true, true,
+ resp->getYiaddr().toText(), scenario.hostname1_,
+ scenario.dhcid1_,
+ time(NULL), subnet_->getValid(), true);
+ }
+
+ // Expire the lease
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(resp->getYiaddr());
+ ASSERT_TRUE(lease);
+ lease->cltt_ = 0;
+ ASSERT_NO_THROW(LeaseMgrFactory::instance().updateLease4(lease));
+ lease = LeaseMgrFactory::instance().getLease4(resp->getYiaddr());
+ ASSERT_TRUE(lease->expired());
+
+ // Create the requesting/returning client.
+ boost::shared_ptr<Dhcp4Client> client2;
+ if (scenario.client_id1_ == scenario.client_id2_) {
+ client2 = client1;
+ } else {
+ client2.reset(new Dhcp4Client(srv_, Dhcp4Client::SELECTING));
+ client2->setIfaceName("eth1");
+ client2->setIfaceIndex(ETH1_INDEX);
+ client2->includeClientId(scenario.client_id2_);
+ }
+
+ ASSERT_NO_THROW(client2->includeHostname(scenario.hostname2_));
+
+ ASSERT_NO_THROW(client2->doDORA());
+ resp = client2->getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Verify that there is one NameChangeRequest generated.
+ size_t expected_count = (scenario.expect_remove_ ? 1 : 0) +
+ (scenario.expect_add_ ? 1 : 0);
+
+ ASSERT_EQ(expected_count, d2_mgr_.getQueueSize());
+ if (scenario.expect_remove_) {
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE,
+ true, true,
+ resp->getYiaddr().toText(), scenario.hostname1_,
+ scenario.dhcid1_,
+ time(NULL), subnet_->getValid(), true);
+ }
+
+ if (scenario.expect_add_) {
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD,
+ true, true,
+ resp->getYiaddr().toText(), scenario.hostname2_,
+ scenario.dhcid2_,
+ time(NULL), subnet_->getValid(), true);
+ }
+
+ bool deleted = false;
+ ASSERT_NO_THROW(deleted = LeaseMgrFactory::instance().deleteLease(lease));
+ ASSERT_TRUE(deleted);
+ }
+ }
+}
+
+// Verifies that the DDNS parameters used for a lease in subnet in
+// shared-network belong to lease's subnet. This checks that we
+// get the right results even when the allocation engine changes the
+// subnet choice.
+TEST_F(NameDhcpv4SrvTest, ddnsSharedNetworkTest) {
+ // Load a configuration with D2 enabled
+ ASSERT_NO_FATAL_FAILURE(configure(CONFIGS[10], *srv_));
+ ASSERT_TRUE(CfgMgr::instance().ddnsEnabled());
+
+ // Create a client and get a lease.
+ Dhcp4Client client1(srv_, Dhcp4Client::SELECTING);
+ client1.setIfaceName("eth1");
+ client1.setIfaceIndex(ETH1_INDEX);
+ ASSERT_NO_THROW(client1.includeHostname("client1"));
+
+ // Do the DORA.
+ ASSERT_NO_THROW(client1.doDORA());
+ Pkt4Ptr resp = client1.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Obtain the Hostname option sent in the response and make sure that the server
+ // has used the hostname reserved for this client.
+ OptionStringPtr hostname;
+ hostname = boost::dynamic_pointer_cast<OptionString>(resp->getOption(DHO_HOST_NAME));
+ ASSERT_TRUE(hostname);
+ EXPECT_EQ("client1.one.example.com", hostname->getValue());
+
+ // Make sure the lease is in the database and hostname is correct.
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(IOAddress("192.0.2.10"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("client1.one.example.com", lease->hostname_);
+
+ // Verify that an NCR was generated correctly.
+ ASSERT_EQ(1, CfgMgr::instance().getD2ClientMgr().getQueueSize());
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ resp->getYiaddr().toText(),
+ "client1.one.example.com.", "",
+ time(NULL), 7200, true);
+
+ // Now let's try with a second client. The first subnet is full so we should
+ // end up on the second subnet.
+ Dhcp4Client client2(srv_, Dhcp4Client::SELECTING);
+ client2.setIfaceName("eth1");
+ client2.setIfaceIndex(ETH1_INDEX);
+ ASSERT_NO_THROW(client2.includeHostname("client2"));
+
+ ASSERT_NO_THROW(client2.doDORA());
+ resp = client2.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Obtain the Hostname option sent in the response and make sure that the server
+ // has used the hostname reserved for this client.
+ hostname = boost::dynamic_pointer_cast<OptionString>(resp->getOption(DHO_HOST_NAME));
+ ASSERT_TRUE(hostname);
+ EXPECT_EQ("client2.two.example.com", hostname->getValue());
+
+ // Verify the NCR is there and correct.
+ ASSERT_EQ(1, CfgMgr::instance().getD2ClientMgr().getQueueSize());
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ resp->getYiaddr().toText(),
+ "client2.two.example.com.", "",
+ time(NULL), 7200, true);
+
+ // Make sure the lease is in the database and hostname is correct.
+ lease = LeaseMgrFactory::instance().getLease4(IOAddress("10.0.0.10"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("client2.two.example.com", lease->hostname_);
+}
+
+// Verifies the basic behavior for a DORA cycle when offer-lifetime is greater
+// than zero.
+TEST_F(NameDhcpv4SrvTest, withOfferLifetime) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Use HW address that matches the reservation entry in the configuration.
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+ // Configure DHCP server.
+ configure(CONFIGS[11], *client.getServer());
+
+ // Fetch the subnet.
+ Subnet4Ptr subnet = CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->
+ selectSubnet(IOAddress("10.0.0.10"));
+ ASSERT_TRUE(subnet);
+
+ // Make sure that DDNS is enabled.
+ ASSERT_TRUE(CfgMgr::instance().ddnsEnabled());
+ ASSERT_NO_THROW(client.getServer()->startD2());
+ // Include the Client FQDN option.
+ ASSERT_NO_THROW(client.includeFQDN(Option4ClientFqdn::FLAG_S | Option4ClientFqdn::FLAG_E,
+ "client-name", Option4ClientFqdn::PARTIAL));
+
+ checkSubnetStat(subnet->getID(), "total-addresses", 1);
+ checkSubnetStat(subnet->getID(), "cumulative-assigned-addresses", 0);
+ checkSubnetStat(subnet->getID(), "assigned-addresses", 0);
+
+ // Send the DHCPDISCOVER.
+ ASSERT_NO_THROW(client.doDiscover());
+
+ // Make sure that the server responded.
+ Pkt4Ptr resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPOFFER, static_cast<int>(resp->getType()));
+
+ // Lifetime in the OFFER should be valid lifetime.
+ OptionUint32Ptr opt = boost::dynamic_pointer_cast<
+ OptionUint32>(resp->getOption(DHO_DHCP_LEASE_TIME));
+ ASSERT_TRUE(opt);
+ EXPECT_EQ(subnet->getValid(), opt->getValue());
+
+ // Obtain the FQDN option sent in the response and make sure that the server
+ // has used the client supplied hostname for this client.
+ Option4ClientFqdnPtr fqdn;
+ fqdn = boost::dynamic_pointer_cast<Option4ClientFqdn>(resp->getOption(DHO_FQDN));
+ ASSERT_TRUE(fqdn);
+ EXPECT_EQ("client-name.example.com.", fqdn->getDomainName());
+
+ // When receiving DHCPDISCOVER, no NCRs should be generated.
+ EXPECT_EQ(0, d2_mgr_.getQueueSize());
+
+ // Make sure the lease was created with offer-lifetime, fqdn flags = false,
+ // and the FQDN.
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(IOAddress("10.0.0.10"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(subnet->getOfferLft(), lease->valid_lft_);
+ EXPECT_FALSE(lease->fqdn_fwd_);
+ EXPECT_FALSE(lease->fqdn_rev_);
+ EXPECT_EQ("client-name.example.com.", lease->hostname_);
+
+ // Verify assigned stats were incremented.
+ checkSubnetStat(subnet->getID(), "total-addresses", 1);
+ checkSubnetStat(subnet->getID(), "cumulative-assigned-addresses", 1);
+ checkSubnetStat(subnet->getID(), "assigned-addresses", 1);
+
+ // Now send the DHCPREQUEST with including the FQDN option.
+ ASSERT_NO_THROW(client.doRequest());
+ resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Lifetime in the DHCPACK should be valid lifetime.
+ opt = boost::dynamic_pointer_cast<OptionUint32>(resp->getOption(DHO_DHCP_LEASE_TIME));
+ ASSERT_TRUE(opt);
+ EXPECT_EQ(subnet->getValid(), opt->getValue());
+
+ // Once again check that the FQDN is as expected.
+ fqdn = boost::dynamic_pointer_cast<Option4ClientFqdn>(resp->getOption(DHO_FQDN));
+ ASSERT_TRUE(fqdn);
+ EXPECT_EQ("client-name.example.com.", fqdn->getDomainName());
+
+ // There should be one NCR which adds the new DNS entry.
+ ASSERT_EQ(1, CfgMgr::instance().getD2ClientMgr().getQueueSize());
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ resp->getYiaddr().toText(),
+ "client-name.example.com.",
+ "0000011E5D6FA61FCBAC969FF4EF0EBCA3FDE554E"
+ "B020A13F44859F30A108793564A97",
+ time(NULL), subnet->getValid(), true);
+
+ // And that this FQDN has been stored in the lease database.
+ lease = LeaseMgrFactory::instance().getLease4(client.config_.lease_.addr_);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(subnet->getValid(), lease->valid_lft_);
+ EXPECT_TRUE(lease->fqdn_fwd_);
+ EXPECT_TRUE(lease->fqdn_rev_);
+ EXPECT_EQ("client-name.example.com.", lease->hostname_);
+
+ // Verify assigned states did not incremented again.
+ checkSubnetStat(subnet->getID(), "total-addresses", 1);
+ checkSubnetStat(subnet->getID(), "cumulative-assigned-addresses", 1);
+ checkSubnetStat(subnet->getID(), "assigned-addresses", 1);
+}
+
+// Verifies the DNS TTL when ttl percent is specified
+// than zero.
+TEST_F(NameDhcpv4SrvTest, withDdnsTtlPercent) {
+ // Load a configuration with D2 enabled and ddns-ttl-percent
+ ASSERT_NO_FATAL_FAILURE(configure(CONFIGS[12], *srv_));
+ ASSERT_TRUE(CfgMgr::instance().ddnsEnabled());
+
+ // Create a client and get a lease.
+ Dhcp4Client client1(srv_, Dhcp4Client::SELECTING);
+ client1.setIfaceName("eth1");
+ client1.setIfaceIndex(ETH1_INDEX);
+ ASSERT_NO_THROW(client1.includeHostname("client1"));
+
+ // Do the DORA.
+ ASSERT_NO_THROW(client1.doDORA());
+ Pkt4Ptr resp = client1.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Obtain the Hostname option sent in the response and make sure that the server
+ // has used the hostname reserved for this client.
+ OptionStringPtr hostname;
+ hostname = boost::dynamic_pointer_cast<OptionString>(resp->getOption(DHO_HOST_NAME));
+ ASSERT_TRUE(hostname);
+ EXPECT_EQ("client1.example.com", hostname->getValue());
+
+ // Make sure the lease is in the database and hostname is correct.
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(IOAddress("10.0.0.10"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("client1.example.com", lease->hostname_);
+
+ // Verify that an NCR was generated correctly.
+ ASSERT_EQ(1, CfgMgr::instance().getD2ClientMgr().getQueueSize());
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ resp->getYiaddr().toText(),
+ "client1.example.com.", "",
+ time(NULL), lease->valid_lft_, true, true, Optional<double>(.25));
+}
+
+} // end of anonymous namespace
diff --git a/src/bin/dhcp4/tests/get_config_unittest.cc b/src/bin/dhcp4/tests/get_config_unittest.cc
new file mode 100644
index 0000000..9ae42ad
--- /dev/null
+++ b/src/bin/dhcp4/tests/get_config_unittest.cc
@@ -0,0 +1,12481 @@
+// Copyright (C) 2017-2023 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <cc/command_interpreter.h>
+#include <cc/data.h>
+#include <cc/simple_parser.h>
+#include <cc/cfg_to_element.h>
+#include <testutils/user_context_utils.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/parsers/simple_parser4.h>
+#include <dhcp4/dhcp4_srv.h>
+#include <dhcp4/ctrl_dhcp4_srv.h>
+#include <dhcp4/json_config_parser.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <dhcp4/tests/get_config_unittest.h>
+#include <testutils/gtest_utils.h>
+
+#include <boost/algorithm/string.hpp>
+#include <gtest/gtest.h>
+
+#include <iostream>
+#include <string>
+#include <sstream>
+#include <list>
+
+using namespace isc::config;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::test;
+
+namespace {
+
+/// @name How to fill configurations
+///
+/// Copy get_config_unittest.cc.skel into get_config_unittest.cc
+///
+/// For the extracted configurations define the EXTRACT_CONFIG and
+/// recompile this file. Run dhcp4_unittests on Dhcp4ParserTest
+/// redirecting the standard error to a temporary file, e.g. by
+/// @code
+/// ./dhcp4_unittests --gtest_filter="Dhcp4Parser*" > /dev/null 2> x
+/// @endcode
+///
+/// Update EXTRACTED_CONFIGS with the file content
+///
+/// When configurations have been extracted the corresponding unparsed
+/// configurations must be generated. To do that define GENERATE_ACTION
+/// and recompile this file. Run dhcp4_unittests on Dhcp4GetConfigTest
+/// redirecting the standard error to a temporary file, e.g. by
+/// @code
+/// ./dhcp4_unittests --gtest_filter="Dhcp4GetConfig*" > /dev/null 2> u
+/// @endcode
+///
+/// Update UNPARSED_CONFIGS with the file content, recompile this file
+/// without EXTRACT_CONFIG and GENERATE_ACTION.
+///
+/// @note Check for failures at each step!
+/// @note The tests of this file do not check if configs returned
+/// by @ref isc::dhcp::CfgToElement::ToElement() are complete.
+/// This has to be done manually.
+///
+///@{
+/// @brief extracted configurations
+const char* EXTRACTED_CONFIGS[] = {
+ // CONFIGURATION 0
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [ ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 1
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 2
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 3
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"max-valid-lifetime\": 5000,\n"
+" \"min-valid-lifetime\": 3000,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 4
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1024,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" },\n"
+" {\n"
+" \"id\": 100,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.3.101 - 192.0.3.150\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.3.0/24\"\n"
+" },\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.4.101 - 192.0.4.150\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.4.0/24\"\n"
+" },\n"
+" {\n"
+" \"id\": 34,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.5.101 - 192.0.5.150\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.5.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 5
+"{\n"
+" \"boot-file-name\": \"bar\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"next-server\": \"1.2.3.4\",\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"server-hostname\": \"foo\",\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 6
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"boot-file-name\": \"bar\",\n"
+" \"id\": 1,\n"
+" \"next-server\": \"1.2.3.4\",\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"server-hostname\": \"foo\",\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 7
+"{\n"
+" \"boot-file-name\": \"nofile\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"next-server\": \"192.0.0.1\",\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"server-hostname\": \"nohost\",\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"boot-file-name\": \"bootfile.efi\",\n"
+" \"id\": 1,\n"
+" \"next-server\": \"1.2.3.4\",\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"server-hostname\": \"some-name.example.org\",\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 8
+"{\n"
+" \"echo-client-id\": false,\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 9
+"{\n"
+" \"echo-client-id\": true,\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 10
+"{\n"
+" \"compatibility\": {\n"
+" \"exclude-first-last-24\": true,\n"
+" \"ignore-dhcp-server-identifier\": true,\n"
+" \"ignore-rai-link-selection\": true,\n"
+" \"lenient-option-parsing\": true\n"
+" },\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 11
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"match-client-id\": true,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" },\n"
+" {\n"
+" \"id\": 2,\n"
+" \"match-client-id\": false,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.3.1 - 192.0.3.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.3.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 12
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"match-client-id\": false,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" },\n"
+" {\n"
+" \"id\": 2,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.3.1 - 192.0.3.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.3.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 13
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"authoritative\": true,\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" },\n"
+" {\n"
+" \"authoritative\": false,\n"
+" \"id\": 2,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.3.1 - 192.0.3.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.3.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 14
+"{\n"
+" \"authoritative\": true,\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"authoritative\": false,\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" },\n"
+" {\n"
+" \"id\": 2,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.3.1 - 192.0.3.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.3.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 15
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"max-valid-lifetime\": 5000,\n"
+" \"min-valid-lifetime\": 3000,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 5,\n"
+" \"min-valid-lifetime\": 3,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2,\n"
+" \"renew-timer\": 1,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"valid-lifetime\": 4\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 16
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.0/28\"\n"
+" },\n"
+" {\n"
+" \"pool\": \"192.0.2.200-192.0.2.255\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" },\n"
+" {\n"
+" \"id\": 2,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.3.0/25\"\n"
+" },\n"
+" {\n"
+" \"pool\": \"192.0.3.128/25\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.3.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 17
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.128/28\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 18
+"{\n"
+" \"option-def\": [\n"
+" {\n"
+" \"code\": 100,\n"
+" \"name\": \"foo\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"ipv4-address\"\n"
+" }\n"
+" ]\n"
+" }\n",
+ // CONFIGURATION 19
+"{\n"
+" \"option-def\": [\n"
+" {\n"
+" \"code\": 100,\n"
+" \"name\": \"foo\",\n"
+" \"record-types\": \"uint16, ipv4-address, ipv6-address, string\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"record\"\n"
+" }\n"
+" ]\n"
+" }\n",
+ // CONFIGURATION 20
+"{\n"
+" \"option-def\": [\n"
+" {\n"
+" \"code\": 100,\n"
+" \"name\": \"foo\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"uint32\"\n"
+" },\n"
+" {\n"
+" \"code\": 101,\n"
+" \"name\": \"foo-2\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"ipv4-address\"\n"
+" }\n"
+" ]\n"
+" }\n",
+ // CONFIGURATION 21
+"{\n"
+" \"option-def\": [\n"
+" {\n"
+" \"array\": true,\n"
+" \"code\": 100,\n"
+" \"name\": \"foo\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"uint32\"\n"
+" }\n"
+" ]\n"
+" }\n",
+ // CONFIGURATION 22
+"{\n"
+" \"option-def\": [\n"
+" {\n"
+" \"code\": 100,\n"
+" \"encapsulate\": \"sub-opts-space\",\n"
+" \"name\": \"foo\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"uint32\"\n"
+" }\n"
+" ]\n"
+" }\n",
+ // CONFIGURATION 23
+"{\n"
+" \"option-def\": [\n"
+" {\n"
+" \"code\": 109,\n"
+" \"name\": \"foo\",\n"
+" \"space\": \"dhcp4\",\n"
+" \"type\": \"string\"\n"
+" }\n"
+" ]\n"
+" }\n",
+ // CONFIGURATION 24
+"{\n"
+" \"option-def\": [\n"
+" {\n"
+" \"code\": 170,\n"
+" \"name\": \"unassigned-option-170\",\n"
+" \"space\": \"dhcp4\",\n"
+" \"type\": \"string\"\n"
+" }\n"
+" ]\n"
+" }\n",
+ // CONFIGURATION 25
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"option-data\": [\n"
+" {\n"
+" \"csv-format\": false,\n"
+" \"data\": \"ABCDEF0105\",\n"
+" \"name\": \"dhcp-message\"\n"
+" },\n"
+" {\n"
+" \"csv-format\": false,\n"
+" \"data\": \"01\",\n"
+" \"name\": \"default-ip-ttl\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 26
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"option-data\": [\n"
+" {\n"
+" \"csv-format\": false,\n"
+" \"data\": \"ABCDEF0105\",\n"
+" \"name\": \"dhcp-message\"\n"
+" },\n"
+" {\n"
+" \"csv-format\": false,\n"
+" \"data\": \"01\",\n"
+" \"name\": \"default-ip-ttl\"\n"
+" }\n"
+" ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 27
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"option-data\": [\n"
+" {\n"
+" \"csv-format\": false,\n"
+" \"data\": \"ABCDEF0105\",\n"
+" \"name\": \"dhcp-message\"\n"
+" },\n"
+" {\n"
+" \"data\": \"1234\",\n"
+" \"name\": \"foo\",\n"
+" \"space\": \"isc\"\n"
+" }\n"
+" ],\n"
+" \"option-def\": [\n"
+" {\n"
+" \"code\": 56,\n"
+" \"name\": \"foo\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"uint32\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 28
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"option-data\": [\n"
+" {\n"
+" \"data\": \"1234\",\n"
+" \"name\": \"foo\",\n"
+" \"space\": \"isc\"\n"
+" },\n"
+" {\n"
+" \"data\": \"192.168.2.1\",\n"
+" \"name\": \"foo2\",\n"
+" \"space\": \"isc\"\n"
+" }\n"
+" ],\n"
+" \"option-def\": [\n"
+" {\n"
+" \"code\": 1,\n"
+" \"name\": \"foo\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"uint32\"\n"
+" },\n"
+" {\n"
+" \"code\": 2,\n"
+" \"name\": \"foo2\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"ipv4-address\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 29
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"option-data\": [\n"
+" {\n"
+" \"data\": \"11\",\n"
+" \"name\": \"base-option\"\n"
+" },\n"
+" {\n"
+" \"data\": \"1234\",\n"
+" \"name\": \"foo\",\n"
+" \"space\": \"isc\"\n"
+" },\n"
+" {\n"
+" \"data\": \"192.168.2.1\",\n"
+" \"name\": \"foo2\",\n"
+" \"space\": \"isc\"\n"
+" }\n"
+" ],\n"
+" \"option-def\": [\n"
+" {\n"
+" \"code\": 222,\n"
+" \"encapsulate\": \"isc\",\n"
+" \"name\": \"base-option\",\n"
+" \"space\": \"dhcp4\",\n"
+" \"type\": \"uint8\"\n"
+" },\n"
+" {\n"
+" \"code\": 1,\n"
+" \"name\": \"foo\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"uint32\"\n"
+" },\n"
+" {\n"
+" \"code\": 2,\n"
+" \"name\": \"foo2\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"ipv4-address\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 3000\n"
+" }\n",
+ // CONFIGURATION 30
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"option-data\": [\n"
+" {\n"
+" \"csv-format\": false,\n"
+" \"data\": \"AB\",\n"
+" \"name\": \"dhcp-message\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"option-data\": [\n"
+" {\n"
+" \"csv-format\": false,\n"
+" \"data\": \"ABCDEF0105\",\n"
+" \"name\": \"dhcp-message\"\n"
+" },\n"
+" {\n"
+" \"csv-format\": false,\n"
+" \"data\": \"01\",\n"
+" \"name\": \"default-ip-ttl\"\n"
+" }\n"
+" ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 31
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"option-data\": [\n"
+" {\n"
+" \"csv-format\": false,\n"
+" \"data\": \"0102030405060708090A\",\n"
+" \"name\": \"dhcp-message\"\n"
+" }\n"
+" ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" },\n"
+" {\n"
+" \"id\": 2,\n"
+" \"option-data\": [\n"
+" {\n"
+" \"csv-format\": false,\n"
+" \"data\": \"FF\",\n"
+" \"name\": \"default-ip-ttl\"\n"
+" }\n"
+" ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.3.101 - 192.0.3.150\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.3.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 32
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [\n"
+" {\n"
+" \"csv-format\": false,\n"
+" \"data\": \"ABCDEF0105\",\n"
+" \"name\": \"dhcp-message\"\n"
+" },\n"
+" {\n"
+" \"csv-format\": false,\n"
+" \"data\": \"01\",\n"
+" \"name\": \"default-ip-ttl\"\n"
+" }\n"
+" ],\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 33
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [\n"
+" {\n"
+" \"csv-format\": false,\n"
+" \"data\": \"ABCDEF0105\",\n"
+" \"name\": \"dhcp-message\"\n"
+" }\n"
+" ],\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" },\n"
+" {\n"
+" \"option-data\": [\n"
+" {\n"
+" \"csv-format\": false,\n"
+" \"data\": \"01\",\n"
+" \"name\": \"default-ip-ttl\"\n"
+" }\n"
+" ],\n"
+" \"pool\": \"192.0.2.200 - 192.0.2.250\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 34
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"option-data\": [\n"
+" {\n"
+" \"data\": \"true, 10.0.0.3, 127.0.0.1\",\n"
+" \"name\": \"slp-directory-agent\"\n"
+" },\n"
+" {\n"
+" \"data\": \"false, \",\n"
+" \"name\": \"slp-service-scope\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 35
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"option-data\": [\n"
+" {\n"
+" \"data\": \"1234\",\n"
+" \"name\": \"foo\",\n"
+" \"space\": \"vendor-encapsulated-options-space\"\n"
+" },\n"
+" {\n"
+" \"data\": \"192.168.2.1\",\n"
+" \"name\": \"foo2\",\n"
+" \"space\": \"vendor-encapsulated-options-space\"\n"
+" }\n"
+" ],\n"
+" \"option-def\": [\n"
+" {\n"
+" \"code\": 1,\n"
+" \"name\": \"foo\",\n"
+" \"space\": \"vendor-encapsulated-options-space\",\n"
+" \"type\": \"uint32\"\n"
+" },\n"
+" {\n"
+" \"code\": 2,\n"
+" \"name\": \"foo2\",\n"
+" \"space\": \"vendor-encapsulated-options-space\",\n"
+" \"type\": \"ipv4-address\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 36
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"option-data\": [\n"
+" {\n"
+" \"csv-format\": false,\n"
+" \"name\": \"vendor-encapsulated-options\"\n"
+" },\n"
+" {\n"
+" \"data\": \"1234\",\n"
+" \"name\": \"foo\",\n"
+" \"space\": \"vendor-encapsulated-options-space\"\n"
+" },\n"
+" {\n"
+" \"code\": 2,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"192.168.2.1\",\n"
+" \"name\": \"foo2\",\n"
+" \"space\": \"vendor-encapsulated-options-space\"\n"
+" }\n"
+" ],\n"
+" \"option-def\": [\n"
+" {\n"
+" \"code\": 1,\n"
+" \"name\": \"foo\",\n"
+" \"space\": \"vendor-encapsulated-options-space\",\n"
+" \"type\": \"uint32\"\n"
+" },\n"
+" {\n"
+" \"code\": 2,\n"
+" \"name\": \"foo2\",\n"
+" \"space\": \"vendor-encapsulated-options-space\",\n"
+" \"type\": \"ipv4-address\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 3000\n"
+" }\n",
+ // CONFIGURATION 37
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"option-data\": [\n"
+" {\n"
+" \"code\": 100,\n"
+" \"csv-format\": false,\n"
+" \"data\": \"ABCDEF0105\",\n"
+" \"name\": \"option-one\",\n"
+" \"space\": \"vendor-4491\"\n"
+" },\n"
+" {\n"
+" \"code\": 100,\n"
+" \"csv-format\": false,\n"
+" \"data\": \"1234\",\n"
+" \"name\": \"option-two\",\n"
+" \"space\": \"vendor-1234\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1-192.0.2.10\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 38
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"option-data\": [\n"
+" {\n"
+" \"code\": 100,\n"
+" \"data\": \"this is a string vendor-opt\",\n"
+" \"name\": \"foo\",\n"
+" \"space\": \"vendor-4491\"\n"
+" }\n"
+" ],\n"
+" \"option-def\": [\n"
+" {\n"
+" \"code\": 100,\n"
+" \"name\": \"foo\",\n"
+" \"space\": \"vendor-4491\",\n"
+" \"type\": \"string\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 39
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"eth0\", \"eth1\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 40
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"eth0\", \"*\", \"eth1\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 41
+"{\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": true,\n"
+" \"max-queue-size\": 2048,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"192.168.2.2\",\n"
+" \"sender-port\": 778,\n"
+" \"server-ip\": \"192.168.2.1\",\n"
+" \"server-port\": 777\n"
+" },\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 42
+"{\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": true,\n"
+" \"generated-prefix\": \"test.prefix\",\n"
+" \"hostname-char-replacement\": \"x\",\n"
+" \"hostname-char-set\": \"[^A-Z]\",\n"
+" \"max-queue-size\": 2048,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"override-client-update\": true,\n"
+" \"override-no-update\": true,\n"
+" \"qualifying-suffix\": \"test.suffix.\",\n"
+" \"replace-client-name\": \"when-present\",\n"
+" \"sender-ip\": \"192.168.2.2\",\n"
+" \"sender-port\": 778,\n"
+" \"server-ip\": \"192.168.2.1\",\n"
+" \"server-port\": 777\n"
+" },\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 43
+"{\n"
+" \"ddns-generated-prefix\": \"global.prefix\",\n"
+" \"ddns-override-client-update\": true,\n"
+" \"ddns-override-no-update\": true,\n"
+" \"ddns-qualifying-suffix\": \"global.suffix.\",\n"
+" \"ddns-replace-client-name\": \"always\",\n"
+" \"ddns-send-updates\": false,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": true,\n"
+" \"generated-prefix\": \"d2.prefix\",\n"
+" \"hostname-char-replacement\": \"z\",\n"
+" \"hostname-char-set\": \"[^0-9]\",\n"
+" \"max-queue-size\": 2048,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"override-client-update\": false,\n"
+" \"override-no-update\": false,\n"
+" \"qualifying-suffix\": \"d2.suffix.\",\n"
+" \"replace-client-name\": \"when-present\",\n"
+" \"sender-ip\": \"192.168.2.2\",\n"
+" \"sender-port\": 778,\n"
+" \"server-ip\": \"192.168.2.1\",\n"
+" \"server-port\": 777\n"
+" },\n"
+" \"hostname-char-replacement\": \"x\",\n"
+" \"hostname-char-set\": \"[^A-Z]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 44
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2,\n"
+" \"relay\": {\n"
+" \"ip-address\": \"192.0.2.123\"\n"
+" },\n"
+" \"renew-timer\": 1,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"valid-lifetime\": 4\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 45
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ \"192.0.3.123\", \"192.0.3.124\" ]\n"
+" },\n"
+" \"renew-timer\": 1,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"valid-lifetime\": 4\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 46
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"client-class\": \"alpha\",\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" },\n"
+" {\n"
+" \"client-class\": \"beta\",\n"
+" \"id\": 2,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.3.101 - 192.0.3.150\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.3.0/24\"\n"
+" },\n"
+" {\n"
+" \"client-class\": \"gamma\",\n"
+" \"id\": 3,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.4.101 - 192.0.4.150\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.4.0/24\"\n"
+" },\n"
+" {\n"
+" \"id\": 4,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.5.101 - 192.0.5.150\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.5.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 47
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"client-class\": \"alpha\",\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" },\n"
+" {\n"
+" \"client-class\": \"beta\",\n"
+" \"pool\": \"192.0.3.101 - 192.0.3.150\"\n"
+" },\n"
+" {\n"
+" \"client-class\": \"gamma\",\n"
+" \"pool\": \"192.0.4.101 - 192.0.4.150\"\n"
+" },\n"
+" {\n"
+" \"pool\": \"192.0.5.101 - 192.0.5.150\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.0.0/16\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 48
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 123,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"reservations\": [ ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" },\n"
+" {\n"
+" \"id\": 234,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.3.101 - 192.0.3.150\"\n"
+" }\n"
+" ],\n"
+" \"reservations\": [\n"
+" {\n"
+" \"duid\": \"01:02:03:04:05:06:07:08:09:0A\",\n"
+" \"hostname\": \"\",\n"
+" \"ip-address\": \"192.0.3.112\",\n"
+" \"option-data\": [\n"
+" {\n"
+" \"data\": \"192.0.3.15\",\n"
+" \"name\": \"name-servers\"\n"
+" },\n"
+" {\n"
+" \"data\": \"32\",\n"
+" \"name\": \"default-ip-ttl\"\n"
+" }\n"
+" ]\n"
+" },\n"
+" {\n"
+" \"hostname\": \"\",\n"
+" \"hw-address\": \"01:02:03:04:05:06\",\n"
+" \"ip-address\": \"192.0.3.120\",\n"
+" \"option-data\": [\n"
+" {\n"
+" \"data\": \"192.0.3.95\",\n"
+" \"name\": \"name-servers\"\n"
+" },\n"
+" {\n"
+" \"data\": \"11\",\n"
+" \"name\": \"default-ip-ttl\"\n"
+" }\n"
+" ]\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.3.0/24\"\n"
+" },\n"
+" {\n"
+" \"id\": 542,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.4.101 - 192.0.4.150\"\n"
+" }\n"
+" ],\n"
+" \"reservations\": [\n"
+" {\n"
+" \"duid\": \"0A:09:08:07:06:05:04:03:02:01\",\n"
+" \"hostname\": \"\",\n"
+" \"ip-address\": \"192.0.4.101\",\n"
+" \"option-data\": [\n"
+" {\n"
+" \"data\": \"192.0.4.11\",\n"
+" \"name\": \"name-servers\"\n"
+" },\n"
+" {\n"
+" \"data\": \"95\",\n"
+" \"name\": \"default-ip-ttl\"\n"
+" }\n"
+" ]\n"
+" },\n"
+" {\n"
+" \"circuit-id\": \"060504030201\",\n"
+" \"hostname\": \"\",\n"
+" \"ip-address\": \"192.0.4.102\"\n"
+" },\n"
+" {\n"
+" \"client-id\": \"05:01:02:03:04:05:06\",\n"
+" \"hostname\": \"\",\n"
+" \"ip-address\": \"192.0.4.103\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.4.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 49
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"option-def\": [\n"
+" {\n"
+" \"code\": 100,\n"
+" \"name\": \"foo\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"uint32\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 234,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.3.101 - 192.0.3.150\"\n"
+" }\n"
+" ],\n"
+" \"reservations\": [\n"
+" {\n"
+" \"duid\": \"01:02:03:04:05:06:07:08:09:0A\",\n"
+" \"ip-address\": \"192.0.3.112\",\n"
+" \"option-data\": [\n"
+" {\n"
+" \"data\": \"123\",\n"
+" \"name\": \"foo\",\n"
+" \"space\": \"isc\"\n"
+" }\n"
+" ]\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.3.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 50
+"{\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.1.0/24\"\n"
+" }\n"
+" ],\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"subnet\": \"192.0.1.0/24\"\n"
+" },\n"
+" {\n"
+" \"id\": 2,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-out-of-pool\": true,\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" },\n"
+" {\n"
+" \"id\": 3,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.3.0/24\"\n"
+" }\n"
+" ],\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": false,\n"
+" \"subnet\": \"192.0.3.0/24\"\n"
+" },\n"
+" {\n"
+" \"id\": 4,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.4.0/24\"\n"
+" }\n"
+" ],\n"
+" \"reservations-global\": true,\n"
+" \"reservations-in-subnet\": false,\n"
+" \"subnet\": \"192.0.4.0/24\"\n"
+" },\n"
+" {\n"
+" \"id\": 5,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.5.0/24\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.5.0/24\"\n"
+" },\n"
+" {\n"
+" \"id\": 6,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.6.0/24\"\n"
+" }\n"
+" ],\n"
+" \"reservations-global\": true,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"subnet\": \"192.0.6.0/24\"\n"
+" },\n"
+" {\n"
+" \"id\": 7,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.7.0/24\"\n"
+" }\n"
+" ],\n"
+" \"reservations-global\": true,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-out-of-pool\": true,\n"
+" \"subnet\": \"192.0.7.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 51
+"{\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-out-of-pool\": true,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" },\n"
+" {\n"
+" \"id\": 2,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.3.0/24\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.3.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 52
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"subnet4\": [ ]\n"
+" }\n",
+ // CONFIGURATION 53
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"subnet4\": [ ]\n"
+" }\n",
+ // CONFIGURATION 54
+"{\n"
+" \"decline-probation-period\": 12345,\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"subnet4\": [ ]\n"
+" }\n",
+ // CONFIGURATION 55
+"{\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 35,\n"
+" \"hold-reclaimed-time\": 1800,\n"
+" \"max-reclaim-leases\": 50,\n"
+" \"max-reclaim-time\": 100,\n"
+" \"reclaim-timer-wait-time\": 20,\n"
+" \"unwarned-reclaim-cycles\": 10\n"
+" },\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"subnet4\": [ ]\n"
+" }\n",
+ // CONFIGURATION 56
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 57
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-subnet\": \"2001:db8::123/45\",\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 58
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"ethX\",\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 59
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"ethX\",\n"
+" \"4o6-subnet\": \"2001:db8::543/21\",\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 60
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface-id\": \"vlan123\",\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 61
+"{\n"
+" \"client-classes\": [\n"
+" {\n"
+" \"name\": \"one\"\n"
+" },\n"
+" {\n"
+" \"name\": \"two\"\n"
+" },\n"
+" {\n"
+" \"name\": \"three\"\n"
+" }\n"
+" ],\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 62
+"{\n"
+" \"client-classes\": [\n"
+" {\n"
+" \"max-valid-lifetime\": 3000,\n"
+" \"min-valid-lifetime\": 1000,\n"
+" \"name\": \"one\",\n"
+" \"valid-lifetime\": 2000\n"
+" },\n"
+" {\n"
+" \"name\": \"two\"\n"
+" }\n"
+" ],\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ]\n"
+" }\n",
+ // CONFIGURATION 63
+"{\n"
+" \"client-classes\": [\n"
+" {\n"
+" \"max-valid-lifetime\": 3000,\n"
+" \"min-valid-lifetime\": 1000,\n"
+" \"name\": \"one\",\n"
+" \"template-test\": \"''\",\n"
+" \"valid-lifetime\": 2000\n"
+" },\n"
+" {\n"
+" \"name\": \"two\",\n"
+" \"template-test\": \"''\"\n"
+" }\n"
+" ],\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ]\n"
+" }\n",
+ // CONFIGURATION 64
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.0/28\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 65
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.0/28\",\n"
+" \"user-context\": { }\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 66
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.0/28\",\n"
+" \"user-context\": {\n"
+" \"bool-param\": true,\n"
+" \"integer-param\": 42,\n"
+" \"string-param\": \"Sagittarius\"\n"
+" }\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 67
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.0 - 192.0.2.15\",\n"
+" \"user-context\": {\n"
+" \"bool-param\": true,\n"
+" \"integer-param\": 42,\n"
+" \"string-param\": \"Sagittarius\"\n"
+" }\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 68
+"{\n"
+" \"hosts-databases\": [\n"
+" {\n"
+" \"name\": \"keatest1\",\n"
+" \"password\": \"keatest\",\n"
+" \"type\": \"mysql\",\n"
+" \"user\": \"keatest\"\n"
+" },\n"
+" {\n"
+" \"name\": \"keatest2\",\n"
+" \"password\": \"keatest\",\n"
+" \"type\": \"mysql\",\n"
+" \"user\": \"keatest\"\n"
+" }\n"
+" ],\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 69
+"{\n"
+" \"client-classes\": [\n"
+" {\n"
+" \"name\": \"all\",\n"
+" \"test\": \"'' == ''\",\n"
+" \"user-context\": {\n"
+" \"comment\": \"match all\"\n"
+" }\n"
+" },\n"
+" {\n"
+" \"name\": \"none\"\n"
+" },\n"
+" {\n"
+" \"name\": \"both\",\n"
+" \"user-context\": {\n"
+" \"comment\": \"a comment\",\n"
+" \"version\": 1\n"
+" }\n"
+" }\n"
+" ],\n"
+" \"control-socket\": {\n"
+" \"socket-name\": \"/tmp/kea4-ctrl-socket\",\n"
+" \"socket-type\": \"unix\",\n"
+" \"user-context\": {\n"
+" \"comment\": \"Indirect comment\"\n"
+" }\n"
+" },\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"user-context\": {\n"
+" \"comment\": \"No dynamic DNS\"\n"
+" }\n"
+" },\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false,\n"
+" \"user-context\": {\n"
+" \"comment\": \"Use wildcard\"\n"
+" }\n"
+" },\n"
+" \"option-data\": [\n"
+" {\n"
+" \"csv-format\": false,\n"
+" \"data\": \"ABCDEF0105\",\n"
+" \"name\": \"dhcp-message\",\n"
+" \"user-context\": {\n"
+" \"comment\": \"Set option value\"\n"
+" }\n"
+" }\n"
+" ],\n"
+" \"option-def\": [\n"
+" {\n"
+" \"code\": 100,\n"
+" \"name\": \"foo\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"ipv4-address\",\n"
+" \"user-context\": {\n"
+" \"comment\": \"An option definition\"\n"
+" }\n"
+" }\n"
+" ],\n"
+" \"shared-networks\": [\n"
+" {\n"
+" \"name\": \"foo\",\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 100,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.1.1-192.0.1.10\",\n"
+" \"user-context\": {\n"
+" \"comment\": \"A pool\"\n"
+" }\n"
+" }\n"
+" ],\n"
+" \"reservations\": [\n"
+" {\n"
+" \"hostname\": \"foo.example.com\",\n"
+" \"hw-address\": \"AA:BB:CC:DD:EE:FF\",\n"
+" \"option-data\": [\n"
+" {\n"
+" \"data\": \"example.com\",\n"
+" \"name\": \"domain-name\",\n"
+" \"user-context\": {\n"
+" \"comment\": \"An option in a reservation\"\n"
+" }\n"
+" }\n"
+" ],\n"
+" \"user-context\": {\n"
+" \"comment\": \"A host reservation\"\n"
+" }\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.1.0/24\",\n"
+" \"user-context\": {\n"
+" \"comment\": \"A subnet\"\n"
+" }\n"
+" }\n"
+" ],\n"
+" \"user-context\": {\n"
+" \"comment\": \"A shared network\"\n"
+" }\n"
+" }\n"
+" ],\n"
+" \"user-context\": {\n"
+" \"comment\": \"A DHCPv4 server\"\n"
+" }\n"
+" }\n",
+ // CONFIGURATION 70
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [\n"
+" {\n"
+" \"duid\": \"01:02:03:04:05:06:07:08:09:0A\",\n"
+" \"hostname\": \"global1\",\n"
+" \"ip-address\": \"192.0.200.1\",\n"
+" \"option-data\": [\n"
+" {\n"
+" \"data\": \"192.0.3.15\",\n"
+" \"name\": \"name-servers\"\n"
+" },\n"
+" {\n"
+" \"data\": \"32\",\n"
+" \"name\": \"default-ip-ttl\"\n"
+" }\n"
+" ]\n"
+" },\n"
+" {\n"
+" \"hostname\": \"global2\",\n"
+" \"hw-address\": \"01:02:03:04:05:06\",\n"
+" \"option-data\": [\n"
+" {\n"
+" \"data\": \"192.0.3.95\",\n"
+" \"name\": \"name-servers\"\n"
+" },\n"
+" {\n"
+" \"data\": \"11\",\n"
+" \"name\": \"default-ip-ttl\"\n"
+" }\n"
+" ]\n"
+" }\n"
+" ],\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 123,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"reservations\": [ ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" },\n"
+" {\n"
+" \"id\": 542,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.4.101 - 192.0.4.150\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.4.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 71
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"shared-networks\": [\n"
+" {\n"
+" \"calculate-tee-times\": true,\n"
+" \"name\": \"foo\",\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 100,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.1.1-192.0.1.10\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.1.0/24\",\n"
+" \"t1-percent\": 0.45,\n"
+" \"t2-percent\": 0.65\n"
+" },\n"
+" {\n"
+" \"id\": 200,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1-192.0.2.10\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.4,\n"
+" \"t2-percent\": 0.75\n"
+" }\n"
+" ],\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 300,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.3.0 - 192.0.3.15\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.3.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 72
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"store-extended-info\": true,\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" },\n"
+" {\n"
+" \"id\": 2,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.3.1 - 192.0.3.100\"\n"
+" }\n"
+" ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.3.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 73
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"store-extended-info\": true,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"id\": 1,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\"\n"
+" },\n"
+" {\n"
+" \"id\": 2,\n"
+" \"pools\": [\n"
+" {\n"
+" \"pool\": \"192.0.3.1 - 192.0.3.100\"\n"
+" }\n"
+" ],\n"
+" \"subnet\": \"192.0.3.0/24\"\n"
+" }\n"
+" ],\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 74
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"statistic-default-sample-age\": 5,\n"
+" \"statistic-default-sample-count\": 10,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 75
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"subnet4\": [ ]\n"
+" }\n",
+ // CONFIGURATION 76
+"{\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 1024,\n"
+" \"thread-pool-size\": 48\n"
+" },\n"
+" \"subnet4\": [ ]\n"
+" }\n"
+};
+
+/// @brief unparsed configurations
+const char* UNPARSED_CONFIGS[] = {
+ // CONFIGURATION 0
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [ ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 1
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 2
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 3
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"max-valid-lifetime\": 5000,\n"
+" \"min-valid-lifetime\": 3000,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 5000,\n"
+" \"min-valid-lifetime\": 3000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 4
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.4.101-192.0.4.150\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.4.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" },\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 34,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.5.101-192.0.5.150\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.5.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" },\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 100,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.3.101-192.0.3.150\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.3.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" },\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1024,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 5
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"bar\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"1.2.3.4\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"foo\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 6
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"boot-file-name\": \"bar\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"next-server\": \"1.2.3.4\",\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"server-hostname\": \"foo\",\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 7
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"nofile\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"192.0.0.1\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"nohost\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"boot-file-name\": \"bootfile.efi\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"next-server\": \"1.2.3.4\",\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"server-hostname\": \"some-name.example.org\",\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 8
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": false,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 9
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 10
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"compatibility\": {\n"
+" \"exclude-first-last-24\": true,\n"
+" \"ignore-dhcp-server-identifier\": true,\n"
+" \"ignore-rai-link-selection\": true,\n"
+" \"lenient-option-parsing\": true\n"
+" },\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 11
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"match-client-id\": true,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" },\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 2,\n"
+" \"match-client-id\": false,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.3.1-192.0.3.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.3.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 12
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"match-client-id\": false,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" },\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 2,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.3.1-192.0.3.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.3.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 13
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": true,\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" },\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 2,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.3.1-192.0.3.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.3.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 14
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": true,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" },\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 2,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.3.1-192.0.3.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.3.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 15
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"max-valid-lifetime\": 5000,\n"
+" \"min-valid-lifetime\": 3000,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 5,\n"
+" \"min-valid-lifetime\": 3,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 16
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.0/28\"\n"
+" },\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.200-192.0.2.255\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" },\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 2,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.3.0/25\"\n"
+" },\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.3.128/25\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.3.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 17
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.128/28\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 18
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [\n"
+" {\n"
+" \"array\": false,\n"
+" \"code\": 100,\n"
+" \"encapsulate\": \"\",\n"
+" \"name\": \"foo\",\n"
+" \"record-types\": \"\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"ipv4-address\"\n"
+" }\n"
+" ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [ ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 7200\n"
+" }\n",
+ // CONFIGURATION 19
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [\n"
+" {\n"
+" \"array\": false,\n"
+" \"code\": 100,\n"
+" \"encapsulate\": \"\",\n"
+" \"name\": \"foo\",\n"
+" \"record-types\": \"uint16, ipv4-address, ipv6-address, string\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"record\"\n"
+" }\n"
+" ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [ ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 7200\n"
+" }\n",
+ // CONFIGURATION 20
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [\n"
+" {\n"
+" \"array\": false,\n"
+" \"code\": 100,\n"
+" \"encapsulate\": \"\",\n"
+" \"name\": \"foo\",\n"
+" \"record-types\": \"\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"uint32\"\n"
+" },\n"
+" {\n"
+" \"array\": false,\n"
+" \"code\": 101,\n"
+" \"encapsulate\": \"\",\n"
+" \"name\": \"foo-2\",\n"
+" \"record-types\": \"\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"ipv4-address\"\n"
+" }\n"
+" ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [ ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 7200\n"
+" }\n",
+ // CONFIGURATION 21
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [\n"
+" {\n"
+" \"array\": true,\n"
+" \"code\": 100,\n"
+" \"encapsulate\": \"\",\n"
+" \"name\": \"foo\",\n"
+" \"record-types\": \"\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"uint32\"\n"
+" }\n"
+" ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [ ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 7200\n"
+" }\n",
+ // CONFIGURATION 22
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [\n"
+" {\n"
+" \"array\": false,\n"
+" \"code\": 100,\n"
+" \"encapsulate\": \"sub-opts-space\",\n"
+" \"name\": \"foo\",\n"
+" \"record-types\": \"\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"uint32\"\n"
+" }\n"
+" ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [ ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 7200\n"
+" }\n",
+ // CONFIGURATION 23
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [\n"
+" {\n"
+" \"array\": false,\n"
+" \"code\": 109,\n"
+" \"encapsulate\": \"\",\n"
+" \"name\": \"foo\",\n"
+" \"record-types\": \"\",\n"
+" \"space\": \"dhcp4\",\n"
+" \"type\": \"string\"\n"
+" }\n"
+" ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [ ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 7200\n"
+" }\n",
+ // CONFIGURATION 24
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [\n"
+" {\n"
+" \"array\": false,\n"
+" \"code\": 170,\n"
+" \"encapsulate\": \"\",\n"
+" \"name\": \"unassigned-option-170\",\n"
+" \"record-types\": \"\",\n"
+" \"space\": \"dhcp4\",\n"
+" \"type\": \"string\"\n"
+" }\n"
+" ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [ ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 7200\n"
+" }\n",
+ // CONFIGURATION 25
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 56,\n"
+" \"csv-format\": false,\n"
+" \"data\": \"ABCDEF0105\",\n"
+" \"name\": \"dhcp-message\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" },\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 23,\n"
+" \"csv-format\": false,\n"
+" \"data\": \"01\",\n"
+" \"name\": \"default-ip-ttl\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" }\n"
+" ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 26
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 56,\n"
+" \"csv-format\": false,\n"
+" \"data\": \"ABCDEF0105\",\n"
+" \"name\": \"dhcp-message\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" },\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 23,\n"
+" \"csv-format\": false,\n"
+" \"data\": \"01\",\n"
+" \"name\": \"default-ip-ttl\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" }\n"
+" ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 27
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 56,\n"
+" \"csv-format\": false,\n"
+" \"data\": \"ABCDEF0105\",\n"
+" \"name\": \"dhcp-message\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" },\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 56,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"1234\",\n"
+" \"name\": \"foo\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"isc\"\n"
+" }\n"
+" ],\n"
+" \"option-def\": [\n"
+" {\n"
+" \"array\": false,\n"
+" \"code\": 56,\n"
+" \"encapsulate\": \"\",\n"
+" \"name\": \"foo\",\n"
+" \"record-types\": \"\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"uint32\"\n"
+" }\n"
+" ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 28
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 1,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"1234\",\n"
+" \"name\": \"foo\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"isc\"\n"
+" },\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 2,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"192.168.2.1\",\n"
+" \"name\": \"foo2\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"isc\"\n"
+" }\n"
+" ],\n"
+" \"option-def\": [\n"
+" {\n"
+" \"array\": false,\n"
+" \"code\": 1,\n"
+" \"encapsulate\": \"\",\n"
+" \"name\": \"foo\",\n"
+" \"record-types\": \"\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"uint32\"\n"
+" },\n"
+" {\n"
+" \"array\": false,\n"
+" \"code\": 2,\n"
+" \"encapsulate\": \"\",\n"
+" \"name\": \"foo2\",\n"
+" \"record-types\": \"\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"ipv4-address\"\n"
+" }\n"
+" ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [ ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 29
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 222,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"11\",\n"
+" \"name\": \"base-option\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" },\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 1,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"1234\",\n"
+" \"name\": \"foo\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"isc\"\n"
+" },\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 2,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"192.168.2.1\",\n"
+" \"name\": \"foo2\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"isc\"\n"
+" }\n"
+" ],\n"
+" \"option-def\": [\n"
+" {\n"
+" \"array\": false,\n"
+" \"code\": 222,\n"
+" \"encapsulate\": \"isc\",\n"
+" \"name\": \"base-option\",\n"
+" \"record-types\": \"\",\n"
+" \"space\": \"dhcp4\",\n"
+" \"type\": \"uint8\"\n"
+" },\n"
+" {\n"
+" \"array\": false,\n"
+" \"code\": 1,\n"
+" \"encapsulate\": \"\",\n"
+" \"name\": \"foo\",\n"
+" \"record-types\": \"\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"uint32\"\n"
+" },\n"
+" {\n"
+" \"array\": false,\n"
+" \"code\": 2,\n"
+" \"encapsulate\": \"\",\n"
+" \"name\": \"foo2\",\n"
+" \"record-types\": \"\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"ipv4-address\"\n"
+" }\n"
+" ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 3000,\n"
+" \"min-valid-lifetime\": 3000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 3000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 3000\n"
+" }\n",
+ // CONFIGURATION 30
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 56,\n"
+" \"csv-format\": false,\n"
+" \"data\": \"AB\",\n"
+" \"name\": \"dhcp-message\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" }\n"
+" ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 56,\n"
+" \"csv-format\": false,\n"
+" \"data\": \"ABCDEF0105\",\n"
+" \"name\": \"dhcp-message\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" },\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 23,\n"
+" \"csv-format\": false,\n"
+" \"data\": \"01\",\n"
+" \"name\": \"default-ip-ttl\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" }\n"
+" ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 31
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 56,\n"
+" \"csv-format\": false,\n"
+" \"data\": \"0102030405060708090A\",\n"
+" \"name\": \"dhcp-message\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" }\n"
+" ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" },\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 2,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 23,\n"
+" \"csv-format\": false,\n"
+" \"data\": \"FF\",\n"
+" \"name\": \"default-ip-ttl\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" }\n"
+" ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.3.101-192.0.3.150\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.3.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 32
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 56,\n"
+" \"csv-format\": false,\n"
+" \"data\": \"ABCDEF0105\",\n"
+" \"name\": \"dhcp-message\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" },\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 23,\n"
+" \"csv-format\": false,\n"
+" \"data\": \"01\",\n"
+" \"name\": \"default-ip-ttl\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" }\n"
+" ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 33
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 56,\n"
+" \"csv-format\": false,\n"
+" \"data\": \"ABCDEF0105\",\n"
+" \"name\": \"dhcp-message\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" }\n"
+" ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" },\n"
+" {\n"
+" \"option-data\": [\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 23,\n"
+" \"csv-format\": false,\n"
+" \"data\": \"01\",\n"
+" \"name\": \"default-ip-ttl\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" }\n"
+" ],\n"
+" \"pool\": \"192.0.2.200-192.0.2.250\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 34
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 78,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"true, 10.0.0.3, 127.0.0.1\",\n"
+" \"name\": \"slp-directory-agent\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" },\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 79,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"false, \",\n"
+" \"name\": \"slp-service-scope\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" }\n"
+" ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 35
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 1,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"1234\",\n"
+" \"name\": \"foo\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"vendor-encapsulated-options-space\"\n"
+" },\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 2,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"192.168.2.1\",\n"
+" \"name\": \"foo2\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"vendor-encapsulated-options-space\"\n"
+" }\n"
+" ],\n"
+" \"option-def\": [\n"
+" {\n"
+" \"array\": false,\n"
+" \"code\": 1,\n"
+" \"encapsulate\": \"\",\n"
+" \"name\": \"foo\",\n"
+" \"record-types\": \"\",\n"
+" \"space\": \"vendor-encapsulated-options-space\",\n"
+" \"type\": \"uint32\"\n"
+" },\n"
+" {\n"
+" \"array\": false,\n"
+" \"code\": 2,\n"
+" \"encapsulate\": \"\",\n"
+" \"name\": \"foo2\",\n"
+" \"record-types\": \"\",\n"
+" \"space\": \"vendor-encapsulated-options-space\",\n"
+" \"type\": \"ipv4-address\"\n"
+" }\n"
+" ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [ ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 36
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 43,\n"
+" \"csv-format\": false,\n"
+" \"data\": \"0104000004D20204C0A80201\",\n"
+" \"name\": \"vendor-encapsulated-options\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" },\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 1,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"1234\",\n"
+" \"name\": \"foo\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"vendor-encapsulated-options-space\"\n"
+" },\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 2,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"192.168.2.1\",\n"
+" \"name\": \"foo2\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"vendor-encapsulated-options-space\"\n"
+" }\n"
+" ],\n"
+" \"option-def\": [\n"
+" {\n"
+" \"array\": false,\n"
+" \"code\": 1,\n"
+" \"encapsulate\": \"\",\n"
+" \"name\": \"foo\",\n"
+" \"record-types\": \"\",\n"
+" \"space\": \"vendor-encapsulated-options-space\",\n"
+" \"type\": \"uint32\"\n"
+" },\n"
+" {\n"
+" \"array\": false,\n"
+" \"code\": 2,\n"
+" \"encapsulate\": \"\",\n"
+" \"name\": \"foo2\",\n"
+" \"record-types\": \"\",\n"
+" \"space\": \"vendor-encapsulated-options-space\",\n"
+" \"type\": \"ipv4-address\"\n"
+" }\n"
+" ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 3000,\n"
+" \"min-valid-lifetime\": 3000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 3000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 3000\n"
+" }\n",
+ // CONFIGURATION 37
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 100,\n"
+" \"csv-format\": false,\n"
+" \"data\": \"1234\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"vendor-1234\"\n"
+" },\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 100,\n"
+" \"csv-format\": false,\n"
+" \"data\": \"ABCDEF0105\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"vendor-4491\"\n"
+" }\n"
+" ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.10\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 38
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 100,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"this is a string vendor-opt\",\n"
+" \"name\": \"foo\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"vendor-4491\"\n"
+" }\n"
+" ],\n"
+" \"option-def\": [\n"
+" {\n"
+" \"array\": false,\n"
+" \"code\": 100,\n"
+" \"encapsulate\": \"\",\n"
+" \"name\": \"foo\",\n"
+" \"record-types\": \"\",\n"
+" \"space\": \"vendor-4491\",\n"
+" \"type\": \"string\"\n"
+" }\n"
+" ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 39
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"eth0\", \"eth1\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [ ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 40
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\", \"eth0\", \"eth1\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [ ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 41
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": true,\n"
+" \"max-queue-size\": 2048,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"192.168.2.2\",\n"
+" \"sender-port\": 778,\n"
+" \"server-ip\": \"192.168.2.1\",\n"
+" \"server-port\": 777\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 42
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"test.prefix\",\n"
+" \"ddns-override-client-update\": true,\n"
+" \"ddns-override-no-update\": true,\n"
+" \"ddns-qualifying-suffix\": \"test.suffix.\",\n"
+" \"ddns-replace-client-name\": \"when-present\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": true,\n"
+" \"max-queue-size\": 2048,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"192.168.2.2\",\n"
+" \"sender-port\": 778,\n"
+" \"server-ip\": \"192.168.2.1\",\n"
+" \"server-port\": 777\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"x\",\n"
+" \"hostname-char-set\": \"[^A-Z]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 43
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"global.prefix\",\n"
+" \"ddns-override-client-update\": true,\n"
+" \"ddns-override-no-update\": true,\n"
+" \"ddns-qualifying-suffix\": \"global.suffix.\",\n"
+" \"ddns-replace-client-name\": \"always\",\n"
+" \"ddns-send-updates\": false,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": true,\n"
+" \"max-queue-size\": 2048,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"192.168.2.2\",\n"
+" \"sender-port\": 778,\n"
+" \"server-ip\": \"192.168.2.1\",\n"
+" \"server-port\": 777\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"x\",\n"
+" \"hostname-char-set\": \"[^A-Z]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 44
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4,\n"
+" \"min-valid-lifetime\": 4,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ \"192.0.2.123\" ]\n"
+" },\n"
+" \"renew-timer\": 1,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 45
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4,\n"
+" \"min-valid-lifetime\": 4,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ \"192.0.3.123\", \"192.0.3.124\" ]\n"
+" },\n"
+" \"renew-timer\": 1,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 46
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"client-class\": \"alpha\",\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" },\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"client-class\": \"beta\",\n"
+" \"id\": 2,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.3.101-192.0.3.150\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.3.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" },\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"client-class\": \"gamma\",\n"
+" \"id\": 3,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.4.101-192.0.4.150\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.4.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" },\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 4,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.5.101-192.0.5.150\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.5.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 47
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"client-class\": \"alpha\",\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" },\n"
+" {\n"
+" \"client-class\": \"beta\",\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.3.101-192.0.3.150\"\n"
+" },\n"
+" {\n"
+" \"client-class\": \"gamma\",\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.4.101-192.0.4.150\"\n"
+" },\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.5.101-192.0.5.150\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.0.0/16\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 48
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 123,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" },\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 234,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.3.101-192.0.3.150\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [\n"
+" {\n"
+" \"boot-file-name\": \"\",\n"
+" \"client-classes\": [ ],\n"
+" \"hostname\": \"\",\n"
+" \"hw-address\": \"01:02:03:04:05:06\",\n"
+" \"ip-address\": \"192.0.3.120\",\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 5,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"192.0.3.95\",\n"
+" \"name\": \"name-servers\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" },\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 23,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"11\",\n"
+" \"name\": \"default-ip-ttl\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" }\n"
+" ],\n"
+" \"server-hostname\": \"\"\n"
+" },\n"
+" {\n"
+" \"boot-file-name\": \"\",\n"
+" \"client-classes\": [ ],\n"
+" \"duid\": \"01:02:03:04:05:06:07:08:09:0a\",\n"
+" \"hostname\": \"\",\n"
+" \"ip-address\": \"192.0.3.112\",\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 5,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"192.0.3.15\",\n"
+" \"name\": \"name-servers\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" },\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 23,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"32\",\n"
+" \"name\": \"default-ip-ttl\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" }\n"
+" ],\n"
+" \"server-hostname\": \"\"\n"
+" }\n"
+" ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.3.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" },\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 542,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.4.101-192.0.4.150\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [\n"
+" {\n"
+" \"boot-file-name\": \"\",\n"
+" \"client-classes\": [ ],\n"
+" \"client-id\": \"05010203040506\",\n"
+" \"hostname\": \"\",\n"
+" \"ip-address\": \"192.0.4.103\",\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"server-hostname\": \"\"\n"
+" },\n"
+" {\n"
+" \"boot-file-name\": \"\",\n"
+" \"circuit-id\": \"060504030201\",\n"
+" \"client-classes\": [ ],\n"
+" \"hostname\": \"\",\n"
+" \"ip-address\": \"192.0.4.102\",\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"server-hostname\": \"\"\n"
+" },\n"
+" {\n"
+" \"boot-file-name\": \"\",\n"
+" \"client-classes\": [ ],\n"
+" \"duid\": \"0a:09:08:07:06:05:04:03:02:01\",\n"
+" \"hostname\": \"\",\n"
+" \"ip-address\": \"192.0.4.101\",\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 5,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"192.0.4.11\",\n"
+" \"name\": \"name-servers\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" },\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 23,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"95\",\n"
+" \"name\": \"default-ip-ttl\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" }\n"
+" ],\n"
+" \"server-hostname\": \"\"\n"
+" }\n"
+" ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.4.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 49
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [\n"
+" {\n"
+" \"array\": false,\n"
+" \"code\": 100,\n"
+" \"encapsulate\": \"\",\n"
+" \"name\": \"foo\",\n"
+" \"record-types\": \"\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"uint32\"\n"
+" }\n"
+" ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 234,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.3.101-192.0.3.150\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [\n"
+" {\n"
+" \"boot-file-name\": \"\",\n"
+" \"client-classes\": [ ],\n"
+" \"duid\": \"01:02:03:04:05:06:07:08:09:0a\",\n"
+" \"hostname\": \"\",\n"
+" \"ip-address\": \"192.0.3.112\",\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 100,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"123\",\n"
+" \"name\": \"foo\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"isc\"\n"
+" }\n"
+" ],\n"
+" \"server-hostname\": \"\"\n"
+" }\n"
+" ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.3.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 50
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.1.0/24\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.1.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" },\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 2,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-out-of-pool\": true,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" },\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 3,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.3.0/24\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": false,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.3.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" },\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 4,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.4.0/24\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"reservations-global\": true,\n"
+" \"reservations-in-subnet\": false,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.4.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" },\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 5,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.5.0/24\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.5.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" },\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 6,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.6.0/24\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"reservations-global\": true,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.6.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" },\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 7,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.7.0/24\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"reservations-global\": true,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-out-of-pool\": true,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.7.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 51
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": true,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.0/24\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" },\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 2,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.3.0/24\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.3.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 52
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [ ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 7200\n"
+" }\n",
+ // CONFIGURATION 53
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [ ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 7200\n"
+" }\n",
+ // CONFIGURATION 54
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 12345,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [ ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 7200\n"
+" }\n",
+ // CONFIGURATION 55
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 35,\n"
+" \"hold-reclaimed-time\": 1800,\n"
+" \"max-reclaim-leases\": 50,\n"
+" \"max-reclaim-time\": 100,\n"
+" \"reclaim-timer-wait-time\": 20,\n"
+" \"unwarned-reclaim-cycles\": 10\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [ ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 7200\n"
+" }\n",
+ // CONFIGURATION 56
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 57
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"2001:db8::123/45\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 58
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"ethX\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 59
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"ethX\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"2001:db8::543/21\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 60
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"vlan123\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 61
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"client-classes\": [\n"
+" {\n"
+" \"boot-file-name\": \"\",\n"
+" \"name\": \"one\",\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"server-hostname\": \"\"\n"
+" },\n"
+" {\n"
+" \"boot-file-name\": \"\",\n"
+" \"name\": \"two\",\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"server-hostname\": \"\"\n"
+" },\n"
+" {\n"
+" \"boot-file-name\": \"\",\n"
+" \"name\": \"three\",\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"server-hostname\": \"\"\n"
+" }\n"
+" ],\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 62
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"client-classes\": [\n"
+" {\n"
+" \"boot-file-name\": \"\",\n"
+" \"max-valid-lifetime\": 3000,\n"
+" \"min-valid-lifetime\": 1000,\n"
+" \"name\": \"one\",\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"server-hostname\": \"\",\n"
+" \"valid-lifetime\": 2000\n"
+" },\n"
+" {\n"
+" \"boot-file-name\": \"\",\n"
+" \"name\": \"two\",\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"server-hostname\": \"\"\n"
+" }\n"
+" ],\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 7200,\n"
+" \"min-valid-lifetime\": 7200,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 7200\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 7200\n"
+" }\n",
+ // CONFIGURATION 63
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"client-classes\": [\n"
+" {\n"
+" \"boot-file-name\": \"\",\n"
+" \"max-valid-lifetime\": 3000,\n"
+" \"min-valid-lifetime\": 1000,\n"
+" \"name\": \"one\",\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"server-hostname\": \"\",\n"
+" \"template-test\": \"''\",\n"
+" \"valid-lifetime\": 2000\n"
+" },\n"
+" {\n"
+" \"boot-file-name\": \"\",\n"
+" \"name\": \"two\",\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"server-hostname\": \"\",\n"
+" \"template-test\": \"''\"\n"
+" }\n"
+" ],\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 7200,\n"
+" \"min-valid-lifetime\": 7200,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 7200\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 7200\n"
+" }\n",
+ // CONFIGURATION 64
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.0/28\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 65
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.0/28\",\n"
+" \"user-context\": { }\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 66
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.0/28\",\n"
+" \"user-context\": {\n"
+" \"bool-param\": true,\n"
+" \"integer-param\": 42,\n"
+" \"string-param\": \"Sagittarius\"\n"
+" }\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 67
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.0/28\",\n"
+" \"user-context\": {\n"
+" \"bool-param\": true,\n"
+" \"integer-param\": 42,\n"
+" \"string-param\": \"Sagittarius\"\n"
+" }\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 68
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"hosts-databases\": [\n"
+" {\n"
+" \"name\": \"keatest1\",\n"
+" \"password\": \"keatest\",\n"
+" \"type\": \"mysql\",\n"
+" \"user\": \"keatest\"\n"
+" },\n"
+" {\n"
+" \"name\": \"keatest2\",\n"
+" \"password\": \"keatest\",\n"
+" \"type\": \"mysql\",\n"
+" \"user\": \"keatest\"\n"
+" }\n"
+" ],\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [ ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 69
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"client-classes\": [\n"
+" {\n"
+" \"boot-file-name\": \"\",\n"
+" \"name\": \"all\",\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"server-hostname\": \"\",\n"
+" \"test\": \"'' == ''\",\n"
+" \"user-context\": {\n"
+" \"comment\": \"match all\"\n"
+" }\n"
+" },\n"
+" {\n"
+" \"boot-file-name\": \"\",\n"
+" \"name\": \"none\",\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"server-hostname\": \"\"\n"
+" },\n"
+" {\n"
+" \"boot-file-name\": \"\",\n"
+" \"name\": \"both\",\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"server-hostname\": \"\",\n"
+" \"user-context\": {\n"
+" \"comment\": \"a comment\",\n"
+" \"version\": 1\n"
+" }\n"
+" }\n"
+" ],\n"
+" \"control-socket\": {\n"
+" \"socket-name\": \"/tmp/kea4-ctrl-socket\",\n"
+" \"socket-type\": \"unix\",\n"
+" \"user-context\": {\n"
+" \"comment\": \"Indirect comment\"\n"
+" }\n"
+" },\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001,\n"
+" \"user-context\": {\n"
+" \"comment\": \"No dynamic DNS\"\n"
+" }\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false,\n"
+" \"user-context\": {\n"
+" \"comment\": \"Use wildcard\"\n"
+" }\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 56,\n"
+" \"csv-format\": false,\n"
+" \"data\": \"ABCDEF0105\",\n"
+" \"name\": \"dhcp-message\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\",\n"
+" \"user-context\": {\n"
+" \"comment\": \"Set option value\"\n"
+" }\n"
+" }\n"
+" ],\n"
+" \"option-def\": [\n"
+" {\n"
+" \"array\": false,\n"
+" \"code\": 100,\n"
+" \"encapsulate\": \"\",\n"
+" \"name\": \"foo\",\n"
+" \"record-types\": \"\",\n"
+" \"space\": \"isc\",\n"
+" \"type\": \"ipv4-address\",\n"
+" \"user-context\": {\n"
+" \"comment\": \"An option definition\"\n"
+" }\n"
+" }\n"
+" ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [\n"
+" {\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"max-valid-lifetime\": 7200,\n"
+" \"min-valid-lifetime\": 7200,\n"
+" \"name\": \"foo\",\n"
+" \"option-data\": [ ],\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 100,\n"
+" \"max-valid-lifetime\": 7200,\n"
+" \"min-valid-lifetime\": 7200,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.1.1-192.0.1.10\",\n"
+" \"user-context\": {\n"
+" \"comment\": \"A pool\"\n"
+" }\n"
+" }\n"
+" ],\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"reservations\": [\n"
+" {\n"
+" \"boot-file-name\": \"\",\n"
+" \"client-classes\": [ ],\n"
+" \"hostname\": \"foo.example.com\",\n"
+" \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 15,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"example.com\",\n"
+" \"name\": \"domain-name\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\",\n"
+" \"user-context\": {\n"
+" \"comment\": \"An option in a reservation\"\n"
+" }\n"
+" }\n"
+" ],\n"
+" \"server-hostname\": \"\",\n"
+" \"user-context\": {\n"
+" \"comment\": \"A host reservation\"\n"
+" }\n"
+" }\n"
+" ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.1.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"user-context\": {\n"
+" \"comment\": \"A subnet\"\n"
+" },\n"
+" \"valid-lifetime\": 7200\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"user-context\": {\n"
+" \"comment\": \"A shared network\"\n"
+" },\n"
+" \"valid-lifetime\": 7200\n"
+" }\n"
+" ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [ ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"user-context\": {\n"
+" \"comment\": \"A DHCPv4 server\"\n"
+" },\n"
+" \"valid-lifetime\": 7200\n"
+" }\n",
+ // CONFIGURATION 70
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [\n"
+" {\n"
+" \"boot-file-name\": \"\",\n"
+" \"client-classes\": [ ],\n"
+" \"hostname\": \"global2\",\n"
+" \"hw-address\": \"01:02:03:04:05:06\",\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 5,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"192.0.3.95\",\n"
+" \"name\": \"name-servers\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" },\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 23,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"11\",\n"
+" \"name\": \"default-ip-ttl\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" }\n"
+" ],\n"
+" \"server-hostname\": \"\"\n"
+" },\n"
+" {\n"
+" \"boot-file-name\": \"\",\n"
+" \"client-classes\": [ ],\n"
+" \"duid\": \"01:02:03:04:05:06:07:08:09:0a\",\n"
+" \"hostname\": \"global1\",\n"
+" \"ip-address\": \"192.0.200.1\",\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 5,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"192.0.3.15\",\n"
+" \"name\": \"name-servers\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" },\n"
+" {\n"
+" \"always-send\": false,\n"
+" \"code\": 23,\n"
+" \"csv-format\": true,\n"
+" \"data\": \"32\",\n"
+" \"name\": \"default-ip-ttl\",\n"
+" \"never-send\": false,\n"
+" \"space\": \"dhcp4\"\n"
+" }\n"
+" ],\n"
+" \"server-hostname\": \"\"\n"
+" }\n"
+" ],\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 123,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" },\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 542,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.4.101-192.0.4.150\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.4.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 71
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [\n"
+" {\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": true,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"name\": \"foo\",\n"
+" \"option-data\": [ ],\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 100,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.1.1-192.0.1.10\"\n"
+" }\n"
+" ],\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.1.0/24\",\n"
+" \"t1-percent\": 0.45,\n"
+" \"t2-percent\": 0.65,\n"
+" \"valid-lifetime\": 4000\n"
+" },\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": true,\n"
+" \"id\": 200,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.10\"\n"
+" }\n"
+" ],\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.4,\n"
+" \"t2-percent\": 0.75,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.4,\n"
+" \"t2-percent\": 0.75,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 300,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.3.0/28\"\n"
+" }\n"
+" ],\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.3.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 72
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": true,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" },\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 2,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.3.1-192.0.3.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.3.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 73
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": true,\n"
+" \"subnet4\": [\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 1,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.2.1-192.0.2.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": false,\n"
+" \"subnet\": \"192.0.2.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" },\n"
+" {\n"
+" \"4o6-interface\": \"\",\n"
+" \"4o6-interface-id\": \"\",\n"
+" \"4o6-subnet\": \"\",\n"
+" \"allocator\": \"iterative\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"id\": 2,\n"
+" \"max-valid-lifetime\": 4000,\n"
+" \"min-valid-lifetime\": 4000,\n"
+" \"option-data\": [ ],\n"
+" \"pools\": [\n"
+" {\n"
+" \"option-data\": [ ],\n"
+" \"pool\": \"192.0.3.1-192.0.3.100\"\n"
+" }\n"
+" ],\n"
+" \"rebind-timer\": 2000,\n"
+" \"relay\": {\n"
+" \"ip-addresses\": [ ]\n"
+" },\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations\": [ ],\n"
+" \"store-extended-info\": true,\n"
+" \"subnet\": \"192.0.3.0/24\",\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n"
+" ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 74
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"rebind-timer\": 2000,\n"
+" \"renew-timer\": 1000,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 5,\n"
+" \"statistic-default-sample-count\": 10,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [ ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 4000\n"
+" }\n",
+ // CONFIGURATION 75
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 64,\n"
+" \"thread-pool-size\": 0\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [ ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 7200\n"
+" }\n",
+ // CONFIGURATION 76
+"{\n"
+" \"allocator\": \"iterative\",\n"
+" \"authoritative\": false,\n"
+" \"boot-file-name\": \"\",\n"
+" \"calculate-tee-times\": false,\n"
+" \"ddns-generated-prefix\": \"myhost\",\n"
+" \"ddns-override-client-update\": false,\n"
+" \"ddns-override-no-update\": false,\n"
+" \"ddns-qualifying-suffix\": \"\",\n"
+" \"ddns-replace-client-name\": \"never\",\n"
+" \"ddns-send-updates\": true,\n"
+" \"ddns-update-on-renew\": false,\n"
+" \"ddns-use-conflict-resolution\": true,\n"
+" \"decline-probation-period\": 86400,\n"
+" \"dhcp-ddns\": {\n"
+" \"enable-updates\": false,\n"
+" \"max-queue-size\": 1024,\n"
+" \"ncr-format\": \"JSON\",\n"
+" \"ncr-protocol\": \"UDP\",\n"
+" \"sender-ip\": \"0.0.0.0\",\n"
+" \"sender-port\": 0,\n"
+" \"server-ip\": \"127.0.0.1\",\n"
+" \"server-port\": 53001\n"
+" },\n"
+" \"dhcp-queue-control\": {\n"
+" \"capacity\": 64,\n"
+" \"enable-queue\": false,\n"
+" \"queue-type\": \"kea-ring4\"\n"
+" },\n"
+" \"dhcp4o6-port\": 0,\n"
+" \"early-global-reservations-lookup\": false,\n"
+" \"echo-client-id\": true,\n"
+" \"expired-leases-processing\": {\n"
+" \"flush-reclaimed-timer-wait-time\": 25,\n"
+" \"hold-reclaimed-time\": 3600,\n"
+" \"max-reclaim-leases\": 100,\n"
+" \"max-reclaim-time\": 250,\n"
+" \"reclaim-timer-wait-time\": 10,\n"
+" \"unwarned-reclaim-cycles\": 5\n"
+" },\n"
+" \"hooks-libraries\": [ ],\n"
+" \"host-reservation-identifiers\": [ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n"
+" \"hostname-char-replacement\": \"\",\n"
+" \"hostname-char-set\": \"[^A-Za-z0-9.-]\",\n"
+" \"interfaces-config\": {\n"
+" \"interfaces\": [ \"*\" ],\n"
+" \"re-detect\": false\n"
+" },\n"
+" \"ip-reservations-unique\": true,\n"
+" \"lease-database\": {\n"
+" \"type\": \"memfile\"\n"
+" },\n"
+" \"match-client-id\": true,\n"
+" \"multi-threading\": {\n"
+" \"enable-multi-threading\": true,\n"
+" \"packet-queue-size\": 1024,\n"
+" \"thread-pool-size\": 48\n"
+" },\n"
+" \"next-server\": \"0.0.0.0\",\n"
+" \"option-data\": [ ],\n"
+" \"option-def\": [ ],\n"
+" \"parked-packet-limit\": 256,\n"
+" \"reservations-global\": false,\n"
+" \"reservations-in-subnet\": true,\n"
+" \"reservations-lookup-first\": false,\n"
+" \"reservations-out-of-pool\": false,\n"
+" \"sanity-checks\": {\n"
+" \"extended-info-checks\": \"fix\",\n"
+" \"lease-checks\": \"warn\"\n"
+" },\n"
+" \"server-hostname\": \"\",\n"
+" \"server-tag\": \"\",\n"
+" \"shared-networks\": [ ],\n"
+" \"statistic-default-sample-age\": 0,\n"
+" \"statistic-default-sample-count\": 20,\n"
+" \"store-extended-info\": false,\n"
+" \"subnet4\": [ ],\n"
+" \"t1-percent\": 0.5,\n"
+" \"t2-percent\": 0.875,\n"
+" \"valid-lifetime\": 7200\n"
+" }\n"
+};
+
+/// @brief the number of configurations
+const size_t max_config_counter = sizeof(EXTRACTED_CONFIGS) / sizeof(char*);
+///@}
+
+/// @brief the extraction counter
+///
+/// < 0 means do not extract, >= 0 means extract on extractConfig() calls
+/// and increment
+#ifdef EXTRACT_CONFIG
+int extract_count = 0;
+#else
+int extract_count = -1;
+#endif
+
+/// @brief the generate action
+/// false means do nothing, true means unparse extracted configurations
+#ifdef GENERATE_ACTION
+const bool generate_action = true;
+#else
+const bool generate_action = false;
+static_assert(max_config_counter == sizeof(UNPARSED_CONFIGS) / sizeof(char*),
+ "unparsed configurations must be generated");
+#endif
+
+/// @brief format and output a configuration
+void
+outputFormatted(const std::string& config) {
+ // pretty print it
+ ConstElementPtr json = parseDHCP4(config);
+ std::string prettier = prettyPrint(json, 4, 4);
+ // get it as a line array
+ std::list<std::string> lines;
+ boost::split(lines, prettier, boost::is_any_of("\n"));
+ // add escapes using again JSON
+ std::list<std::string> escapes;
+ while (!lines.empty()) {
+ const std::string& line = lines.front();
+ ConstElementPtr escaping = Element::create(line + "\n");
+ escapes.push_back(escaping->str());
+ lines.pop_front();
+ }
+ // output them on std::cerr
+ while (!escapes.empty()) {
+ std::cerr << "\n" << escapes.front();
+ escapes.pop_front();
+ }
+}
+
+} // namespace
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @ref isc::dhcp::test::extractConfig in the header
+void
+extractConfig(const std::string& config) {
+ // skip when disable
+ if (extract_count < 0) {
+ return;
+ }
+ // mark beginning
+ if (extract_count == 0) {
+ // header (note there is no trailer)
+ std::cerr << "/// put this after const char* EXTRACTED_CONFIGS[] = {\n";
+ } else {
+ // end of previous configuration
+ std::cerr << ",\n";
+ }
+ std::cerr << " // CONFIGURATION " << extract_count;
+ try {
+ outputFormatted(config);
+ } catch (...) {
+ // mark error
+ std::cerr << "\n//// got an error\n";
+ }
+ ++extract_count;
+}
+
+} // namespace test
+} // namespace dhcp
+} // namespace isc
+
+namespace {
+
+/// Test fixture class (code from Dhcp4ParserTest)
+class Dhcp4GetConfigTest : public ::testing::TestWithParam<size_t> {
+public:
+ Dhcp4GetConfigTest()
+ : rcode_(-1) {
+ // Open port 0 means to not do anything at all. We don't want to
+ // deal with sockets here, just check if configuration handling
+ // is sane.
+ srv_.reset(new ControlledDhcpv4Srv(0));
+ // Create fresh context.
+ resetConfiguration();
+ }
+
+ ~Dhcp4GetConfigTest() {
+ resetConfiguration();
+ };
+
+ /// @brief Parse and Execute configuration
+ ///
+ /// Parses a configuration and executes a configuration of the server.
+ /// If the operation fails, the current test will register a failure.
+ ///
+ /// @param config Configuration to parse
+ /// @param operation Operation being performed. In the case of an error,
+ /// the error text will include the string "unable to <operation>.".
+ ///
+ /// @return true if the configuration succeeded, false if not.
+ bool
+ executeConfiguration(const std::string& config, const char* operation) {
+ // clear config manager
+ CfgMgr::instance().clear();
+
+ // enable fake network interfaces
+ IfaceMgrTestConfig test_config(true);
+
+ // try JSON parser
+ ConstElementPtr json;
+ try {
+ json = parseJSON(config);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "invalid JSON for " << operation
+ << " failed with " << ex.what()
+ << " on\n" << config << "\n";
+ return (false);
+ }
+
+ // try DHCP4 parser
+ try {
+ json = parseDHCP4(config, true);
+ } catch (...) {
+ ADD_FAILURE() << "parsing failed for " << operation
+ << " on\n" << prettyPrint(json) << "\n";
+ return (false);
+ }
+
+ // try DHCP4 configure
+ ConstElementPtr status;
+ try {
+ status = configureDhcp4Server(*srv_, json);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "configure for " << operation
+ << " failed with " << ex.what()
+ << " on\n" << prettyPrint(json) << "\n";
+ return (false);
+ }
+
+ // The status object must not be NULL
+ if (!status) {
+ ADD_FAILURE() << "configure for " << operation
+ << " returned null on\n"
+ << prettyPrint(json) << "\n";
+ return (false);
+ }
+
+ // Returned value should be 0 (configuration success)
+ comment_ = parseAnswer(rcode_, status);
+ if (rcode_ != 0) {
+ string reason = "";
+ if (comment_) {
+ reason = string(" (") + comment_->stringValue() + string(")");
+ }
+ ADD_FAILURE() << "configure for " << operation
+ << " returned error code "
+ << rcode_ << reason << " on\n"
+ << prettyPrint(json) << "\n";
+ return (false);
+ }
+ return (true);
+ }
+
+ /// @brief Reset configuration database.
+ ///
+ /// This function resets configuration data base by
+ /// removing all subnets and option-data. Reset must
+ /// be performed after each test to make sure that
+ /// contents of the database do not affect result of
+ /// subsequent tests.
+ void resetConfiguration() {
+ string config = "{"
+ "\"interfaces-config\": { \"interfaces\": [ \"*\" ] },"
+ "\"valid-lifetime\": 4000, "
+ "\"subnet4\": [ ], "
+ "\"dhcp-ddns\": { \"enable-updates\" : false }, "
+ "\"option-def\": [ ], "
+ "\"option-data\": [ ] }";
+ EXPECT_TRUE(executeConfiguration(config, "reset configuration"));
+ CfgMgr::instance().clear();
+ CfgMgr::instance().setFamily(AF_INET);
+ }
+
+ boost::scoped_ptr<ControlledDhcpv4Srv> srv_; ///< DHCP4 server under test
+ int rcode_; ///< Return code from element parsing
+ ConstElementPtr comment_; ///< Reason for parse fail
+};
+
+/// Test a configuration
+TEST_P(Dhcp4GetConfigTest, run) {
+ // configurations have not been extracted yet
+ if (max_config_counter == 0) {
+ return;
+ }
+
+ // get the index of configurations to test
+ size_t config_counter = GetParam();
+
+ // emit unparsed header if wanted
+ if ((config_counter == 0) && generate_action) {
+ std::cerr << "///put this after const char* UNPARSED_CONFIGS[] = {\n";
+ }
+
+ // get the extracted configuration
+ std::string config = EXTRACTED_CONFIGS[config_counter];
+ std::ostringstream ss;
+ ss << "extracted config #" << config_counter;
+
+ // execute the extracted configuration
+ ASSERT_TRUE(executeConfiguration(config, ss.str().c_str()));
+
+ // unparse it
+ ConstSrvConfigPtr extracted = CfgMgr::instance().getStagingCfg();
+ ConstElementPtr unparsed;
+ ASSERT_NO_THROW(unparsed = extracted->toElement());
+ ConstElementPtr dhcp;
+ ASSERT_NO_THROW(dhcp = unparsed->get("Dhcp4"));
+ ASSERT_TRUE(dhcp);
+
+ // dump if wanted else check
+ std::string expected;
+ if (generate_action) {
+ if (config_counter > 0) {
+ std::cerr << ",\n";
+ }
+ std::cerr << " // CONFIGURATION " << config_counter;
+ ASSERT_NO_THROW_LOG(expected = prettyPrint(dhcp));
+ ASSERT_NO_THROW_LOG(outputFormatted(dhcp->str()));
+ } else {
+ expected = UNPARSED_CONFIGS[config_counter];
+ // get the expected config using the dhcpv4 syntax parser
+ ElementPtr jsond;
+ ASSERT_NO_THROW_LOG(jsond = parseDHCP4(expected, true));
+ ElementPtr jsonj;
+ // get the expected config using the generic JSON syntax parser
+ ASSERT_NO_THROW_LOG(jsonj = parseJSON(expected));
+ // the generic JSON parser does not handle comments
+ EXPECT_TRUE(isEquivalent(jsond, moveComments(jsonj)));
+ // check that unparsed and expected values match
+ EXPECT_TRUE(isEquivalent(dhcp, jsonj));
+ // check on pretty prints too
+ std::string current = prettyPrint(dhcp, 4, 4) + "\n";
+ EXPECT_EQ(expected, current);
+ if (expected != current) {
+ expected = current;
+ }
+ }
+
+ // execute the dhcp configuration
+ ss.str("");
+ ss << "unparsed config #" << config_counter;
+ EXPECT_TRUE(executeConfiguration(expected, ss.str().c_str()));
+
+ // is it a fixed point?
+ ConstSrvConfigPtr extracted2 = CfgMgr::instance().getStagingCfg();
+ ConstElementPtr unparsed2;
+ ASSERT_NO_THROW_LOG(unparsed2 = extracted2->toElement());
+ ASSERT_TRUE(unparsed2);
+ EXPECT_TRUE(isEquivalent(unparsed, unparsed2));
+}
+
+class IntToString {
+public:
+ std::string operator()(const testing::TestParamInfo<size_t>& n) {
+ std::ostringstream ss;
+ ss << static_cast<size_t>(n.param);
+ return (ss.str());
+ }
+};
+
+/// Define the parameterized test loop.
+#ifdef INSTANTIATE_TEST_SUITE_P
+INSTANTIATE_TEST_SUITE_P(Dhcp4GetConfigTest, Dhcp4GetConfigTest,
+ ::testing::Range(static_cast<size_t>(0),
+ max_config_counter),
+ IntToString());
+#else
+INSTANTIATE_TEST_CASE_P(Dhcp4GetConfigTest, Dhcp4GetConfigTest,
+ ::testing::Range(static_cast<size_t>(0),
+ max_config_counter),
+ IntToString());
+#endif
+} // namespace
diff --git a/src/bin/dhcp4/tests/get_config_unittest.cc.skel b/src/bin/dhcp4/tests/get_config_unittest.cc.skel
new file mode 100644
index 0000000..a25a5ab
--- /dev/null
+++ b/src/bin/dhcp4/tests/get_config_unittest.cc.skel
@@ -0,0 +1,374 @@
+// Copyright (C) 2017-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <cc/command_interpreter.h>
+#include <cc/data.h>
+#include <cc/simple_parser.h>
+#include <cc/cfg_to_element.h>
+#include <testutils/user_context_utils.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/parsers/simple_parser4.h>
+#include <dhcp4/dhcp4_srv.h>
+#include <dhcp4/ctrl_dhcp4_srv.h>
+#include <dhcp4/json_config_parser.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <dhcp4/tests/get_config_unittest.h>
+#include <testutils/gtest_utils.h>
+
+#include <boost/algorithm/string.hpp>
+#include <gtest/gtest.h>
+
+#include <iostream>
+#include <string>
+#include <sstream>
+#include <list>
+
+using namespace isc::config;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::test;
+
+namespace {
+
+/// @name How to fill configurations
+///
+/// Copy get_config_unittest.cc.skel into get_config_unittest.cc
+///
+/// For the extracted configurations define the EXTRACT_CONFIG and
+/// recompile this file. Run dhcp4_unittests on Dhcp4ParserTest
+/// redirecting the standard error to a temporary file, e.g. by
+/// @code
+/// ./dhcp4_unittests --gtest_filter="Dhcp4Parser*" > /dev/null 2> x
+/// @endcode
+///
+/// Update EXTRACTED_CONFIGS with the file content
+///
+/// When configurations have been extracted the corresponding unparsed
+/// configurations must be generated. To do that define GENERATE_ACTION
+/// and recompile this file. Run dhcp4_unittests on Dhcp4GetConfigTest
+/// redirecting the standard error to a temporary file, e.g. by
+/// @code
+/// ./dhcp4_unittests --gtest_filter="Dhcp4GetConfig*" > /dev/null 2> u
+/// @endcode
+///
+/// Update UNPARSED_CONFIGS with the file content, recompile this file
+/// without EXTRACT_CONFIG and GENERATE_ACTION.
+///
+/// @note Check for failures at each step!
+/// @note The tests of this file do not check if configs returned
+/// by @ref isc::dhcp::CfgToElement::ToElement() are complete.
+/// This has to be done manually.
+///
+///@{
+/// @brief extracted configurations
+const char* EXTRACTED_CONFIGS[] = {
+ // "to be replaced"
+};
+
+/// @brief unparsed configurations
+const char* UNPARSED_CONFIGS[] = {
+ // "to be replaced"
+};
+
+/// @brief the number of configurations
+const size_t max_config_counter = sizeof(EXTRACTED_CONFIGS) / sizeof(char*);
+///@}
+
+/// @brief the extraction counter
+///
+/// < 0 means do not extract, >= 0 means extract on extractConfig() calls
+/// and increment
+#ifdef EXTRACT_CONFIG
+int extract_count = 0;
+#else
+int extract_count = -1;
+#endif
+
+/// @brief the generate action
+/// false means do nothing, true means unparse extracted configurations
+#ifdef GENERATE_ACTION
+const bool generate_action = true;
+#else
+const bool generate_action = false;
+static_assert(max_config_counter == sizeof(UNPARSED_CONFIGS) / sizeof(char*),
+ "unparsed configurations must be generated");
+#endif
+
+/// @brief format and output a configuration
+void
+outputFormatted(const std::string& config) {
+ // pretty print it
+ ConstElementPtr json = parseDHCP4(config);
+ std::string prettier = prettyPrint(json, 4, 4);
+ // get it as a line array
+ std::list<std::string> lines;
+ boost::split(lines, prettier, boost::is_any_of("\n"));
+ // add escapes using again JSON
+ std::list<std::string> escapes;
+ while (!lines.empty()) {
+ const std::string& line = lines.front();
+ ConstElementPtr escaping = Element::create(line + "\n");
+ escapes.push_back(escaping->str());
+ lines.pop_front();
+ }
+ // output them on std::cerr
+ while (!escapes.empty()) {
+ std::cerr << "\n" << escapes.front();
+ escapes.pop_front();
+ }
+}
+
+} // namespace
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @ref isc::dhcp::test::extractConfig in the header
+void
+extractConfig(const std::string& config) {
+ // skip when disable
+ if (extract_count < 0) {
+ return;
+ }
+ // mark beginning
+ if (extract_count == 0) {
+ // header (note there is no trailer)
+ std::cerr << "/// put this after const char* EXTRACTED_CONFIGS[] = {\n";
+ } else {
+ // end of previous configuration
+ std::cerr << ",\n";
+ }
+ std::cerr << " // CONFIGURATION " << extract_count;
+ try {
+ outputFormatted(config);
+ } catch (...) {
+ // mark error
+ std::cerr << "\n//// got an error\n";
+ }
+ ++extract_count;
+}
+
+} // namespace test
+} // namespace dhcp
+} // namespace isc
+
+namespace {
+
+/// Test fixture class (code from Dhcp4ParserTest)
+class Dhcp4GetConfigTest : public ::testing::TestWithParam<size_t> {
+public:
+ Dhcp4GetConfigTest()
+ : rcode_(-1) {
+ // Open port 0 means to not do anything at all. We don't want to
+ // deal with sockets here, just check if configuration handling
+ // is sane.
+ srv_.reset(new ControlledDhcpv4Srv(0));
+ // Create fresh context.
+ resetConfiguration();
+ }
+
+ ~Dhcp4GetConfigTest() {
+ resetConfiguration();
+ };
+
+ /// @brief Parse and Execute configuration
+ ///
+ /// Parses a configuration and executes a configuration of the server.
+ /// If the operation fails, the current test will register a failure.
+ ///
+ /// @param config Configuration to parse
+ /// @param operation Operation being performed. In the case of an error,
+ /// the error text will include the string "unable to <operation>.".
+ ///
+ /// @return true if the configuration succeeded, false if not.
+ bool
+ executeConfiguration(const std::string& config, const char* operation) {
+ // clear config manager
+ CfgMgr::instance().clear();
+
+ // enable fake network interfaces
+ IfaceMgrTestConfig test_config(true);
+
+ // try JSON parser
+ ConstElementPtr json;
+ try {
+ json = parseJSON(config);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "invalid JSON for " << operation
+ << " failed with " << ex.what()
+ << " on\n" << config << "\n";
+ return (false);
+ }
+
+ // try DHCP4 parser
+ try {
+ json = parseDHCP4(config, true);
+ } catch (...) {
+ ADD_FAILURE() << "parsing failed for " << operation
+ << " on\n" << prettyPrint(json) << "\n";
+ return (false);
+ }
+
+ // try DHCP4 configure
+ ConstElementPtr status;
+ try {
+ status = configureDhcp4Server(*srv_, json);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "configure for " << operation
+ << " failed with " << ex.what()
+ << " on\n" << prettyPrint(json) << "\n";
+ return (false);
+ }
+
+ // The status object must not be NULL
+ if (!status) {
+ ADD_FAILURE() << "configure for " << operation
+ << " returned null on\n"
+ << prettyPrint(json) << "\n";
+ return (false);
+ }
+
+ // Returned value should be 0 (configuration success)
+ comment_ = parseAnswer(rcode_, status);
+ if (rcode_ != 0) {
+ string reason = "";
+ if (comment_) {
+ reason = string(" (") + comment_->stringValue() + string(")");
+ }
+ ADD_FAILURE() << "configure for " << operation
+ << " returned error code "
+ << rcode_ << reason << " on\n"
+ << prettyPrint(json) << "\n";
+ return (false);
+ }
+ return (true);
+ }
+
+ /// @brief Reset configuration database.
+ ///
+ /// This function resets configuration data base by
+ /// removing all subnets and option-data. Reset must
+ /// be performed after each test to make sure that
+ /// contents of the database do not affect result of
+ /// subsequent tests.
+ void resetConfiguration() {
+ string config = "{"
+ "\"interfaces-config\": { \"interfaces\": [ \"*\" ] },"
+ "\"valid-lifetime\": 4000, "
+ "\"subnet4\": [ ], "
+ "\"dhcp-ddns\": { \"enable-updates\" : false }, "
+ "\"option-def\": [ ], "
+ "\"option-data\": [ ] }";
+ EXPECT_TRUE(executeConfiguration(config, "reset configuration"));
+ CfgMgr::instance().clear();
+ CfgMgr::instance().setFamily(AF_INET);
+ }
+
+ boost::scoped_ptr<ControlledDhcpv4Srv> srv_; ///< DHCP4 server under test
+ int rcode_; ///< Return code from element parsing
+ ConstElementPtr comment_; ///< Reason for parse fail
+};
+
+/// Test a configuration
+TEST_P(Dhcp4GetConfigTest, run) {
+ // configurations have not been extracted yet
+ if (max_config_counter == 0) {
+ return;
+ }
+
+ // get the index of configurations to test
+ size_t config_counter = GetParam();
+
+ // emit unparsed header if wanted
+ if ((config_counter == 0) && generate_action) {
+ std::cerr << "///put this after const char* UNPARSED_CONFIGS[] = {\n";
+ }
+
+ // get the extracted configuration
+ std::string config = EXTRACTED_CONFIGS[config_counter];
+ std::ostringstream ss;
+ ss << "extracted config #" << config_counter;
+
+ // execute the extracted configuration
+ ASSERT_TRUE(executeConfiguration(config, ss.str().c_str()));
+
+ // unparse it
+ ConstSrvConfigPtr extracted = CfgMgr::instance().getStagingCfg();
+ ConstElementPtr unparsed;
+ ASSERT_NO_THROW(unparsed = extracted->toElement());
+ ConstElementPtr dhcp;
+ ASSERT_NO_THROW(dhcp = unparsed->get("Dhcp4"));
+ ASSERT_TRUE(dhcp);
+
+ // dump if wanted else check
+ std::string expected;
+ if (generate_action) {
+ if (config_counter > 0) {
+ std::cerr << ",\n";
+ }
+ std::cerr << " // CONFIGURATION " << config_counter;
+ ASSERT_NO_THROW_LOG(expected = prettyPrint(dhcp));
+ ASSERT_NO_THROW_LOG(outputFormatted(dhcp->str()));
+ } else {
+ expected = UNPARSED_CONFIGS[config_counter];
+ // get the expected config using the dhcpv4 syntax parser
+ ElementPtr jsond;
+ ASSERT_NO_THROW_LOG(jsond = parseDHCP4(expected, true));
+ ElementPtr jsonj;
+ // get the expected config using the generic JSON syntax parser
+ ASSERT_NO_THROW_LOG(jsonj = parseJSON(expected));
+ // the generic JSON parser does not handle comments
+ EXPECT_TRUE(isEquivalent(jsond, moveComments(jsonj)));
+ // check that unparsed and expected values match
+ EXPECT_TRUE(isEquivalent(dhcp, jsonj));
+ // check on pretty prints too
+ std::string current = prettyPrint(dhcp, 4, 4) + "\n";
+ EXPECT_EQ(expected, current);
+ if (expected != current) {
+ expected = current;
+ }
+ }
+
+ // execute the dhcp configuration
+ ss.str("");
+ ss << "unparsed config #" << config_counter;
+ EXPECT_TRUE(executeConfiguration(expected, ss.str().c_str()));
+
+ // is it a fixed point?
+ ConstSrvConfigPtr extracted2 = CfgMgr::instance().getStagingCfg();
+ ConstElementPtr unparsed2;
+ ASSERT_NO_THROW_LOG(unparsed2 = extracted2->toElement());
+ ASSERT_TRUE(unparsed2);
+ EXPECT_TRUE(isEquivalent(unparsed, unparsed2));
+}
+
+class IntToString {
+public:
+ std::string operator()(const testing::TestParamInfo<size_t>& n) {
+ std::ostringstream ss;
+ ss << static_cast<size_t>(n.param);
+ return (ss.str());
+ }
+};
+
+/// Define the parameterized test loop.
+#ifdef INSTANTIATE_TEST_SUITE_P
+INSTANTIATE_TEST_SUITE_P(Dhcp4GetConfigTest, Dhcp4GetConfigTest,
+ ::testing::Range(static_cast<size_t>(0),
+ max_config_counter),
+ IntToString());
+#else
+INSTANTIATE_TEST_CASE_P(Dhcp4GetConfigTest, Dhcp4GetConfigTest,
+ ::testing::Range(static_cast<size_t>(0),
+ max_config_counter),
+ IntToString());
+#endif
+} // namespace
diff --git a/src/bin/dhcp4/tests/get_config_unittest.h b/src/bin/dhcp4/tests/get_config_unittest.h
new file mode 100644
index 0000000..e1718e0
--- /dev/null
+++ b/src/bin/dhcp4/tests/get_config_unittest.h
@@ -0,0 +1,27 @@
+// Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#ifndef GET_CONFIG_UNITTEST_H
+#define GET_CONFIG_UNITTEST_H
+
+#include <string.h>
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @brief Extract a configuration as a C++ source for JSON on std::cerr
+///
+/// This function should be called when a configuration in an unit test
+/// is interesting and should be extracted. Do nothing when extract_count
+/// is negative.
+void extractConfig(const std::string& config);
+
+};
+};
+};
+
+#endif // GET_CONFIG_UNITTEST_H
diff --git a/src/bin/dhcp4/tests/hooks_unittest.cc b/src/bin/dhcp4/tests/hooks_unittest.cc
new file mode 100644
index 0000000..905ce87
--- /dev/null
+++ b/src/bin/dhcp4/tests/hooks_unittest.cc
@@ -0,0 +1,3442 @@
+// Copyright (C) 2015-2023 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <asiolink/io_address.h>
+#include <asiolink/io_service.h>
+#include <cc/command_interpreter.h>
+#include <config/command_mgr.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcp/option.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcp4/ctrl_dhcp4_srv.h>
+#include <dhcp4/json_config_parser.h>
+#include <dhcp4/tests/dhcp4_client.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <dhcp4/tests/marker_file.h>
+#include <dhcp4/tests/test_libraries.h>
+#include <hooks/server_hooks.h>
+#include <hooks/hooks_manager.h>
+#include <hooks/callout_manager.h>
+#include <stats/stats_mgr.h>
+#include <util/multi_threading_mgr.h>
+
+#include <vector>
+
+
+using namespace isc::asiolink;
+using namespace isc::config;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::hooks;
+using namespace isc::stats;
+using namespace isc::util;
+
+using namespace std;
+
+namespace {
+
+// Checks if hooks are implemented properly.
+TEST_F(Dhcpv4SrvTest, Hooks) {
+ NakedDhcpv4Srv srv(0);
+
+ // check if appropriate hooks are registered
+ int hook_index_dhcp4_srv_configured = -1;
+ int hook_index_buffer4_receive = -1;
+ int hook_index_buffer4_send = -1;
+ int hook_index_lease4_renew = -1;
+ int hook_index_lease4_release = -1;
+ int hook_index_lease4_decline = -1;
+ int hook_index_pkt4_receive = -1;
+ int hook_index_pkt4_send = -1;
+ int hook_index_select_subnet = -1;
+ int hook_index_leases4_committed = -1;
+ int hook_index_host4_identifier = -1;
+
+ // check if appropriate indexes are set
+ EXPECT_NO_THROW(hook_index_dhcp4_srv_configured = ServerHooks::getServerHooks()
+ .getIndex("dhcp4_srv_configured"));
+ EXPECT_NO_THROW(hook_index_buffer4_receive = ServerHooks::getServerHooks()
+ .getIndex("buffer4_receive"));
+ EXPECT_NO_THROW(hook_index_buffer4_send = ServerHooks::getServerHooks()
+ .getIndex("buffer4_send"));
+ EXPECT_NO_THROW(hook_index_lease4_renew = ServerHooks::getServerHooks()
+ .getIndex("lease4_renew"));
+ EXPECT_NO_THROW(hook_index_lease4_release = ServerHooks::getServerHooks()
+ .getIndex("lease4_release"));
+ EXPECT_NO_THROW(hook_index_lease4_decline = ServerHooks::getServerHooks()
+ .getIndex("lease4_decline"));
+ EXPECT_NO_THROW(hook_index_pkt4_receive = ServerHooks::getServerHooks()
+ .getIndex("pkt4_receive"));
+ EXPECT_NO_THROW(hook_index_pkt4_send = ServerHooks::getServerHooks()
+ .getIndex("pkt4_send"));
+ EXPECT_NO_THROW(hook_index_select_subnet = ServerHooks::getServerHooks()
+ .getIndex("subnet4_select"));
+ EXPECT_NO_THROW(hook_index_leases4_committed = ServerHooks::getServerHooks()
+ .getIndex("leases4_committed"));
+ EXPECT_NO_THROW(hook_index_host4_identifier = ServerHooks::getServerHooks()
+ .getIndex("host4_identifier"));
+
+ EXPECT_TRUE(hook_index_dhcp4_srv_configured > 0);
+ EXPECT_TRUE(hook_index_buffer4_receive > 0);
+ EXPECT_TRUE(hook_index_buffer4_send > 0);
+ EXPECT_TRUE(hook_index_lease4_renew > 0);
+ EXPECT_TRUE(hook_index_lease4_release > 0);
+ EXPECT_TRUE(hook_index_lease4_decline > 0);
+ EXPECT_TRUE(hook_index_pkt4_receive > 0);
+ EXPECT_TRUE(hook_index_pkt4_send > 0);
+ EXPECT_TRUE(hook_index_select_subnet > 0);
+ EXPECT_TRUE(hook_index_leases4_committed > 0);
+ EXPECT_TRUE(hook_index_host4_identifier > 0);
+}
+
+// A dummy MAC address, padded with 0s
+const uint8_t dummyChaddr[16] = {0, 1, 2, 3, 4, 5, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0 };
+
+// Let's use some creative test content here (128 chars + \0)
+const uint8_t dummyFile[] = "Lorem ipsum dolor sit amet, consectetur "
+ "adipiscing elit. Proin mollis placerat metus, at "
+ "lacinia orci ornare vitae. Mauris amet.";
+
+// Yet another type of test content (64 chars + \0)
+const uint8_t dummySname[] = "Lorem ipsum dolor sit amet, consectetur "
+ "adipiscing elit posuere.";
+
+/// @brief a class dedicated to Hooks testing in DHCPv4 server
+///
+/// This class has a number of static members, because each non-static
+/// method has implicit 'this' parameter, so it does not match callout
+/// signature and couldn't be registered. Furthermore, static methods
+/// can't modify non-static members (for obvious reasons), so many
+/// fields are declared static. It is still better to keep them as
+/// one class rather than unrelated collection of global objects.
+class HooksDhcpv4SrvTest : public Dhcpv4SrvTest {
+
+public:
+
+ /// @brief creates Dhcpv4Srv and prepares buffers for callouts
+ HooksDhcpv4SrvTest() {
+ HooksManager::setTestMode(false);
+ bool status = HooksManager::unloadLibraries();
+ if (!status) {
+ cerr << "(fixture ctor) unloadLibraries failed" << endl;
+ }
+
+ // Allocate new DHCPv4 Server
+ srv_.reset(new NakedDhcpv4Srv(0));
+
+ // Clear static buffers
+ resetCalloutBuffers();
+
+ io_service_ = boost::make_shared<IOService>();
+
+ // Reset the hook system in its original state
+ HooksManager::unloadLibraries();
+
+ // Clear statistics.
+ StatsMgr::instance().removeAll();
+ }
+
+ /// @brief destructor (deletes Dhcpv4Srv)
+ virtual ~HooksDhcpv4SrvTest() {
+ // Clear static buffers
+ resetCalloutBuffers();
+
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts("dhcp4_srv_configured");
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts("buffer4_receive");
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts("buffer4_send");
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts("pkt4_receive");
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts("pkt4_send");
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts("subnet4_select");
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts("leases4_committed");
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts("lease4_renew");
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts("lease4_release");
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts("lease4_decline");
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts("host4_identifier");
+
+ HooksManager::setTestMode(false);
+ bool status = HooksManager::unloadLibraries();
+ if (!status) {
+ cerr << "(fixture dtor) unloadLibraries failed" << endl;
+ }
+
+ // Clear statistics.
+ StatsMgr::instance().removeAll();
+ }
+
+ /// @brief creates an option with specified option code.
+ ///
+ /// This method is static, because it is used from callouts
+ /// that do not have a pointer to HooksDhcpv4SrvTest object
+ ///
+ /// @param option_code code of option to be created
+ ///
+ /// @return pointer to create option object
+ static OptionPtr createOption(uint16_t option_code) {
+
+ uint8_t payload[] = {
+ 0xa, 0xb, 0xc, 0xe, 0xf, 0x10, 0x11, 0x12, 0x13, 0x14
+ };
+
+ OptionBuffer tmp(payload, payload + sizeof(payload));
+ return OptionPtr(new Option(Option::V4, option_code, tmp));
+ }
+
+ /// @brief Generates test packet.
+ ///
+ /// Allocates and generates on-wire buffer that represents test packet, with all
+ /// fixed fields set to non-zero values. Content is not always reasonable.
+ ///
+ /// See generateTestPacket1() function that returns exactly the same packet as
+ /// Pkt4 object.
+ ///
+ /// @return pointer to allocated Pkt4 object
+ /// Returns a vector containing a DHCPv4 packet header.
+ Pkt4Ptr
+ generateSimpleDiscover() {
+
+ // That is only part of the header. It contains all "short" fields,
+ // larger fields are constructed separately.
+ uint8_t hdr[] = {
+ 1, 6, 6, 13, // op, htype, hlen, hops,
+ 0x12, 0x34, 0x56, 0x78, // transaction-id
+ 0, 42, 0x80, 0x00, // 42 secs, BROADCAST flags
+ 192, 0, 2, 1, // ciaddr
+ 1, 2, 3, 4, // yiaddr
+ 192, 0, 2, 255, // siaddr
+ 192, 0, 2, 50, // giaddr
+ };
+
+ // Initialize the vector with the header fields defined above.
+ vector<uint8_t> buf(hdr, hdr + sizeof(hdr));
+
+ // Append the large header fields.
+ copy(dummyChaddr, dummyChaddr + Pkt4::MAX_CHADDR_LEN, back_inserter(buf));
+ copy(dummySname, dummySname + Pkt4::MAX_SNAME_LEN, back_inserter(buf));
+ copy(dummyFile, dummyFile + Pkt4::MAX_FILE_LEN, back_inserter(buf));
+
+ // Should now have all the header, so check. The "static_cast" is used
+ // to get round an odd bug whereby the linker appears not to find the
+ // definition of DHCPV4_PKT_HDR_LEN if it appears within an EXPECT_EQ().
+ EXPECT_EQ(static_cast<size_t>(Pkt4::DHCPV4_PKT_HDR_LEN), buf.size());
+
+ // Add magic cookie
+ buf.push_back(0x63);
+ buf.push_back(0x82);
+ buf.push_back(0x53);
+ buf.push_back(0x63);
+
+ // Add message type DISCOVER
+ buf.push_back(static_cast<uint8_t>(DHO_DHCP_MESSAGE_TYPE));
+ buf.push_back(1); // length (just one byte)
+ buf.push_back(static_cast<uint8_t>(DHCPDISCOVER));
+
+ Pkt4Ptr dis(new Pkt4(&buf[0], buf.size()));
+ // Interface must be selected for a Discover. Server will use the interface
+ // name to select a subnet for a client. This test is using fake interfaces
+ // and the fake eth0 interface has IPv4 address matching the subnet
+ // currently configured for this test.
+ dis->setIface("eth1");
+ dis->setIndex(ETH1_INDEX);
+ return (dis);
+ }
+
+ /// @brief Checks if the state of the callout handle associated with a query
+ /// was reset after the callout invocation.
+ ///
+ /// The check includes verification if the status was set to 'continue' and
+ /// that all arguments were deleted.
+ ///
+ /// @param query pointer to the query which callout handle is associated
+ /// with.
+ void checkCalloutHandleReset(const Pkt4Ptr& query) {
+ CalloutHandlePtr callout_handle = query->getCalloutHandle();
+ ASSERT_TRUE(callout_handle);
+ EXPECT_EQ(CalloutHandle::NEXT_STEP_CONTINUE, callout_handle->getStatus());
+ EXPECT_TRUE(callout_handle->getArgumentNames().empty());
+ }
+
+ /// @brief Test callback that stores callout name and passed parameters.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ buffer4_receive_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("buffer4_receive");
+
+ callout_handle.getArgument("query4", callback_qry_pkt4_);
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+
+ if (callback_qry_pkt4_) {
+ callback_qry_options_copy_ = callback_qry_pkt4_->isCopyRetrievedOptions();
+ }
+
+ return (0);
+ }
+
+ /// @brief Test callback that changes hwaddr value.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ buffer4_receive_change_hwaddr_callout(CalloutHandle& callout_handle) {
+
+ Pkt4Ptr pkt;
+ callout_handle.getArgument("query4", pkt);
+
+ // If there is at least one option with data
+ if (pkt->data_.size() >= Pkt4::DHCPV4_PKT_HDR_LEN) {
+ // Offset of the first byte of the CHADDR field. Let's the first
+ // byte to some new value that we could later check
+ pkt->data_[28] = 0xff;
+ }
+
+ // Carry on as usual
+ return buffer4_receive_callout(callout_handle);
+ }
+
+ /// @brief Test callback that sets skip flag.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ buffer4_receive_skip_callout(CalloutHandle& callout_handle) {
+
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_SKIP);
+
+ // Carry on as usual
+ return buffer4_receive_callout(callout_handle);
+ }
+
+ /// @brief Test callback that sets drop flag.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ buffer4_receive_drop_callout(CalloutHandle& callout_handle) {
+
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_DROP);
+
+ // Carry on as usual
+ return buffer4_receive_callout(callout_handle);
+ }
+
+ /// @brief Test callback that stores callout name and passed parameters.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt4_receive_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("pkt4_receive");
+
+ callout_handle.getArgument("query4", callback_qry_pkt4_);
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+
+ if (callback_qry_pkt4_) {
+ callback_qry_options_copy_ = callback_qry_pkt4_->isCopyRetrievedOptions();
+ }
+
+ return (0);
+ }
+
+ /// @brief Test callback that changes client-id value.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt4_receive_change_clientid_callout(CalloutHandle& callout_handle) {
+
+ Pkt4Ptr pkt;
+ callout_handle.getArgument("query4", pkt);
+
+ // Get rid of the old client-id
+ pkt->delOption(DHO_DHCP_CLIENT_IDENTIFIER);
+
+ // Add a new option
+ pkt->addOption(createOption(DHO_DHCP_CLIENT_IDENTIFIER));
+
+ // Carry on as usual
+ return pkt4_receive_callout(callout_handle);
+ }
+
+ /// @brief Test callback that deletes client-id.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt4_receive_delete_clientid_callout(CalloutHandle& callout_handle) {
+
+ Pkt4Ptr pkt;
+ callout_handle.getArgument("query4", pkt);
+
+ // Get rid of the old client-id (and no HWADDR)
+ vector<uint8_t> mac;
+ pkt->delOption(DHO_DHCP_CLIENT_IDENTIFIER);
+ pkt->setHWAddr(1, 0, mac); // HWtype 1, hardware len = 0
+
+ // Carry on as usual
+ return pkt4_receive_callout(callout_handle);
+ }
+
+ /// @brief Test callback that sets skip flag.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt4_receive_skip_callout(CalloutHandle& callout_handle) {
+
+ Pkt4Ptr pkt;
+ callout_handle.getArgument("query4", pkt);
+
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_SKIP);
+
+ // Carry on as usual
+ return pkt4_receive_callout(callout_handle);
+ }
+
+ /// @brief Test callback that sets drop flag.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt4_receive_drop_callout(CalloutHandle& callout_handle) {
+
+ Pkt4Ptr pkt;
+ callout_handle.getArgument("query4", pkt);
+
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_DROP);
+
+ // Carry on as usual
+ return pkt4_receive_callout(callout_handle);
+ }
+
+ /// @brief Test callback that stores callout name and passed parameters.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt4_send_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("pkt4_send");
+
+ callout_handle.getArgument("response4", callback_resp_pkt4_);
+
+ callout_handle.getArgument("query4", callback_qry_pkt4_);
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+
+ if (callback_qry_pkt4_) {
+ callback_qry_options_copy_ = callback_qry_pkt4_->isCopyRetrievedOptions();
+ }
+
+ if (callback_resp_pkt4_) {
+ callback_resp_options_copy_ = callback_resp_pkt4_->isCopyRetrievedOptions();
+ }
+
+ return (0);
+ }
+
+ /// @brief Test callback that changes server-id.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt4_send_change_serverid_callout(CalloutHandle& callout_handle) {
+
+ Pkt4Ptr pkt;
+ callout_handle.getArgument("response4", pkt);
+
+ // Get rid of the old server-id
+ pkt->delOption(DHO_DHCP_SERVER_IDENTIFIER);
+
+ // Add a new option
+ pkt->addOption(createOption(DHO_DHCP_SERVER_IDENTIFIER));
+
+ // Carry on as usual
+ return pkt4_send_callout(callout_handle);
+ }
+
+ /// @brief Test callback that deletes server-id.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt4_send_delete_serverid_callout(CalloutHandle& callout_handle) {
+
+ Pkt4Ptr pkt;
+ callout_handle.getArgument("response4", pkt);
+
+ // Get rid of the old client-id
+ pkt->delOption(DHO_DHCP_SERVER_IDENTIFIER);
+
+ // Carry on as usual
+ return pkt4_send_callout(callout_handle);
+ }
+
+ /// @brief Test callback that sets skip flag.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt4_send_skip_callout(CalloutHandle& callout_handle) {
+
+ Pkt4Ptr pkt;
+ callout_handle.getArgument("response4", pkt);
+
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_SKIP);
+
+ // carry on as usual
+ return pkt4_send_callout(callout_handle);
+ }
+
+ /// @brief Test callback that sets drop flag.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt4_send_drop_callout(CalloutHandle& callout_handle) {
+
+ Pkt4Ptr pkt;
+ callout_handle.getArgument("response4", pkt);
+
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_DROP);
+
+ // carry on as usual
+ return pkt4_send_callout(callout_handle);
+ }
+
+ /// @brief Test callback that stores callout name and passed parameters.
+ ///
+ /// @param callout_handle handle passed by the hooks framework.
+ /// @return always 0
+ static int
+ buffer4_send_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("buffer4_send");
+
+ callout_handle.getArgument("response4", callback_resp_pkt4_);
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+
+ if (callback_resp_pkt4_) {
+ callback_resp_options_copy_ = callback_resp_pkt4_->isCopyRetrievedOptions();
+ }
+
+ return (0);
+ }
+
+ /// @brief Test callback changes the output buffer to a hardcoded value.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ buffer4_send_change_callout(CalloutHandle& callout_handle) {
+
+ Pkt4Ptr pkt;
+ callout_handle.getArgument("response4", pkt);
+
+ // modify buffer to set a different payload
+ pkt->getBuffer().clear();
+ pkt->getBuffer().writeData(dummyFile, sizeof(dummyFile));
+
+ return (0);
+ }
+
+ /// @brief Test callback that sets skip flag.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ buffer4_send_skip_callout(CalloutHandle& callout_handle) {
+
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_SKIP);
+
+ // Carry on as usual
+ return buffer4_send_callout(callout_handle);
+ }
+
+ /// @brief Test callback that sets drop flag.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ buffer4_send_drop_callout(CalloutHandle& callout_handle) {
+
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_DROP);
+
+ // carry on as usual
+ return buffer4_send_callout(callout_handle);
+ }
+
+ /// @brief Test callback that stores callout name and passed parameters.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ subnet4_select_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("subnet4_select");
+
+ callout_handle.getArgument("query4", callback_qry_pkt4_);
+ callout_handle.getArgument("subnet4", callback_subnet4_);
+ callout_handle.getArgument("subnet4collection", callback_subnet4collection_);
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+
+ if (callback_qry_pkt4_) {
+ callback_qry_options_copy_ = callback_qry_pkt4_->isCopyRetrievedOptions();
+ }
+
+ return (0);
+ }
+
+ /// @brief Test callback that picks the other subnet if possible.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ subnet4_select_different_subnet_callout(CalloutHandle& callout_handle) {
+
+ // Call the basic callout to record all passed values
+ subnet4_select_callout(callout_handle);
+
+ const Subnet4Collection* subnets;
+ Subnet4Ptr subnet;
+ callout_handle.getArgument("subnet4", subnet);
+ callout_handle.getArgument("subnet4collection", subnets);
+
+ // Let's change to a different subnet
+ if (subnets->size() > 1) {
+ subnet = *std::next(subnets->begin()); // Let's pick the other subnet
+ callout_handle.setArgument("subnet4", subnet);
+ }
+
+ return (0);
+ }
+
+ /// @brief Test callback that sets skip flag.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ subnet4_select_skip_callout(CalloutHandle& callout_handle) {
+
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_SKIP);
+
+ // Carry on as usual
+ return subnet4_select_callout(callout_handle);
+ }
+
+ /// @brief Test callback that sets drop flag.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ subnet4_select_drop_callout(CalloutHandle& callout_handle) {
+
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_DROP);
+
+ // Carry on as usual
+ return subnet4_select_callout(callout_handle);
+ }
+
+ /// @brief Test callback that stores callout name and passed parameters.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ lease4_renew_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("lease4_renew");
+
+ callout_handle.getArgument("query4", callback_qry_pkt4_);
+ callout_handle.getArgument("subnet4", callback_subnet4_);
+ callout_handle.getArgument("lease4", callback_lease4_);
+ callout_handle.getArgument("hwaddr", callback_hwaddr_);
+ callout_handle.getArgument("clientid", callback_clientid_);
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+
+ if (callback_qry_pkt4_) {
+ callback_qry_options_copy_ = callback_qry_pkt4_->isCopyRetrievedOptions();
+ }
+
+ return (0);
+ }
+
+ /// @brief Test callback that sets the skip flag.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ lease4_renew_skip_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("lease4_renew");
+
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_SKIP);
+
+ return (0);
+ }
+
+ /// @brief Test callback that stores callout name and passed parameters.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ lease4_release_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("lease4_release");
+
+ callout_handle.getArgument("query4", callback_qry_pkt4_);
+ callout_handle.getArgument("lease4", callback_lease4_);
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+
+ if (callback_qry_pkt4_) {
+ callback_qry_options_copy_ = callback_qry_pkt4_->isCopyRetrievedOptions();
+ }
+
+ return (0);
+ }
+
+ /// @brief Test callback that sets the skip flag.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ lease4_release_skip_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("lease4_release");
+
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_SKIP);
+
+ return (0);
+ }
+
+ /// @brief Test callback that sets the drop flag.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ lease4_release_drop_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("lease4_release");
+
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_DROP);
+
+ return (0);
+ }
+
+ /// @brief Test callback that stores callout name and passed parameters.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ lease4_decline_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("lease4_decline");
+ callout_handle.getArgument("query4", callback_qry_pkt4_);
+ callout_handle.getArgument("lease4", callback_lease4_);
+
+ if (callback_qry_pkt4_) {
+ callback_qry_options_copy_ = callback_qry_pkt4_->isCopyRetrievedOptions();
+ }
+
+ return (0);
+ }
+
+ /// @brief Test callback that sets the skip flag.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ lease4_decline_skip_callout(CalloutHandle& callout_handle) {
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_SKIP);
+
+ return (lease4_decline_callout(callout_handle));
+ }
+
+ /// @brief Test callback that sets the drop flag.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ lease4_decline_drop_callout(CalloutHandle& callout_handle) {
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_DROP);
+
+ return (lease4_decline_callout(callout_handle));
+ }
+
+ /// @brief Test callback that stores callout name and passed parameters.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ leases4_committed_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("leases4_committed");
+
+ callout_handle.getArgument("query4", callback_qry_pkt4_);
+
+ Lease4CollectionPtr leases4;
+ callout_handle.getArgument("leases4", leases4);
+ if (leases4->size() > 0) {
+ callback_lease4_ = leases4->at(0);
+ }
+
+ Lease4CollectionPtr deleted_leases4;
+ callout_handle.getArgument("deleted_leases4", deleted_leases4);
+ if (deleted_leases4->size() > 0) {
+ callback_deleted_lease4_ = deleted_leases4->at(0);
+ }
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+ sort(callback_argument_names_.begin(), callback_argument_names_.end());
+
+ if (callback_qry_pkt4_) {
+ callback_qry_options_copy_ = callback_qry_pkt4_->isCopyRetrievedOptions();
+ }
+
+ return (0);
+ }
+
+ /// @brief Test callback which asks the server to unpark the packet.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static void
+ leases4_committed_unpark_callout(ParkingLotHandlePtr parking_lot, Pkt4Ptr query) {
+ parking_lot->unpark(query);
+ }
+
+ /// @brief Test callback which asks the server to park the packet.
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ leases4_committed_park_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("leases4_committed");
+
+ callout_handle.getArgument("query4", callback_qry_pkt4_);
+
+ io_service_->post(std::bind(&HooksDhcpv4SrvTest::leases4_committed_unpark_callout,
+ callout_handle.getParkingLotHandlePtr(),
+ callback_qry_pkt4_));
+
+ callout_handle.getParkingLotHandlePtr()->reference(callback_qry_pkt4_);
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_PARK);
+
+ Lease4CollectionPtr leases4;
+ callout_handle.getArgument("leases4", leases4);
+ if (leases4->size() > 0) {
+ callback_lease4_ = leases4->at(0);
+ }
+
+ Lease4CollectionPtr deleted_leases4;
+ callout_handle.getArgument("deleted_leases4", deleted_leases4);
+ if (deleted_leases4->size() > 0) {
+ callback_deleted_lease4_ = deleted_leases4->at(0);
+ }
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+ sort(callback_argument_names_.begin(), callback_argument_names_.end());
+
+ if (callback_qry_pkt4_) {
+ callback_qry_options_copy_ = callback_qry_pkt4_->isCopyRetrievedOptions();
+ }
+
+ return (0);
+ }
+
+ /// @brief Test host4_identifier callback by setting identifier to "foo".
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ host4_identifier_foo_callout(CalloutHandle& handle) {
+ callback_name_ = string("host4_identifier");
+
+ // Make sure the query4 parameter is passed.
+ handle.getArgument("query4", callback_qry_pkt4_);
+
+ // Make sure id_type parameter is passed.
+ Host::IdentifierType type = Host::IDENT_FLEX;
+ handle.getArgument("id_type", type);
+
+ // Make sure id_value parameter is passed.
+ std::vector<uint8_t> id_test;
+ handle.getArgument("id_value", id_test);
+
+ // Ok, now set the identifier.
+ std::vector<uint8_t> id = { 0x66, 0x6f, 0x6f }; // foo
+ handle.setArgument("id_value", id);
+ handle.setArgument("id_type", Host::IDENT_FLEX);
+
+ return (0);
+ }
+
+ /// @brief Test host4_identifier callout by setting identifier to hwaddr.
+ ///
+ /// This callout always returns fixed HWADDR: 00:01:02:03:04:05
+ ///
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ host4_identifier_hwaddr_callout(CalloutHandle& handle) {
+ callback_name_ = string("host4_identifier");
+
+ // Make sure the query4 parameter is passed.
+ handle.getArgument("query4", callback_qry_pkt4_);
+
+ // Make sure id_type parameter is passed.
+ Host::IdentifierType type = Host::IDENT_FLEX;
+ handle.getArgument("id_type", type);
+
+ // Make sure id_value parameter is passed.
+ std::vector<uint8_t> id_test;
+ handle.getArgument("id_value", id_test);
+
+ // Ok, now set the identifier to 00:01:02:03:04:05
+ std::vector<uint8_t> id = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 };
+ handle.setArgument("id_value", id);
+ handle.setArgument("id_type", Host::IDENT_HWADDR);
+
+ return (0);
+ }
+
+ /// Resets buffers used to store data received by callouts
+ void resetCalloutBuffers() {
+ callback_name_ = string("");
+ callback_qry_pkt4_.reset();
+ callback_resp_pkt4_.reset();
+ callback_subnet4_.reset();
+ callback_lease4_.reset();
+ callback_deleted_lease4_.reset();
+ callback_hwaddr_.reset();
+ callback_clientid_.reset();
+ callback_subnet4collection_ = NULL;
+ callback_argument_names_.clear();
+ callback_qry_options_copy_ = false;
+ callback_resp_options_copy_ = false;
+ }
+
+ /// @brief Fetches the current value of the given statistic.
+ /// @param name name of the desired statistic.
+ /// @return Current value of the statistic, or zero if the
+ /// statistic is not found.
+ uint64_t getStatistic(const std::string& name) {
+ ObservationPtr stat = StatsMgr::instance().getObservation(name);
+ if (!stat) {
+ return (0);
+ }
+
+ return (stat->getInteger().first);
+ }
+
+ /// Pointer to Dhcpv4Srv that is used in tests
+ boost::shared_ptr<NakedDhcpv4Srv> srv_;
+
+ /// Pointer to the IO service used in the tests.
+ static IOServicePtr io_service_;
+
+ /// The following fields are used in testing pkt4_receive_callout
+
+ /// String name of the received callout
+ static string callback_name_;
+
+ /// Client's query Pkt4 structure returned in the callout
+ static Pkt4Ptr callback_qry_pkt4_;
+
+ /// Server's response Pkt4 structure returned in the callout
+ static Pkt4Ptr callback_resp_pkt4_;
+
+ /// Pointer to lease4 structure returned in the leases4_committed callout
+ static Lease4Ptr callback_lease4_;
+
+ /// Pointer to lease4 structure returned in the leases4_committed callout
+ static Lease4Ptr callback_deleted_lease4_;
+
+ /// Hardware address returned in the callout
+ static HWAddrPtr callback_hwaddr_;
+
+ /// Client-id returned in the callout
+ static ClientIdPtr callback_clientid_;
+
+ /// Pointer to a subnet received by callout
+ static Subnet4Ptr callback_subnet4_;
+
+ /// A list of all available subnets (received by callout)
+ static const Subnet4Collection* callback_subnet4collection_;
+
+ /// A list of all received arguments
+ static vector<string> callback_argument_names_;
+
+ /// Flag indicating if copying retrieved options was enabled for
+ /// a query during callout execution.
+ static bool callback_qry_options_copy_;
+
+ /// Flag indicating if copying retrieved options was enabled for
+ /// a response during callout execution.
+ static bool callback_resp_options_copy_;
+};
+
+// The following fields are used in testing pkt4_receive_callout.
+// See fields description in the class for details
+IOServicePtr HooksDhcpv4SrvTest::io_service_;
+string HooksDhcpv4SrvTest::callback_name_;
+Pkt4Ptr HooksDhcpv4SrvTest::callback_qry_pkt4_;
+Pkt4Ptr HooksDhcpv4SrvTest::callback_resp_pkt4_;
+Subnet4Ptr HooksDhcpv4SrvTest::callback_subnet4_;
+const Subnet4Collection* HooksDhcpv4SrvTest::callback_subnet4collection_;
+HWAddrPtr HooksDhcpv4SrvTest::callback_hwaddr_;
+ClientIdPtr HooksDhcpv4SrvTest::callback_clientid_;
+Lease4Ptr HooksDhcpv4SrvTest::callback_lease4_;
+Lease4Ptr HooksDhcpv4SrvTest::callback_deleted_lease4_;
+vector<string> HooksDhcpv4SrvTest::callback_argument_names_;
+bool HooksDhcpv4SrvTest::callback_qry_options_copy_;
+bool HooksDhcpv4SrvTest::callback_resp_options_copy_;
+
+/// @brief Fixture class used to do basic library load/unload tests
+class LoadUnloadDhcpv4SrvTest : public ::testing::Test {
+public:
+ /// @brief Pointer to the tested server object
+ boost::shared_ptr<NakedDhcpv4Srv> server_;
+
+ LoadUnloadDhcpv4SrvTest() {
+ reset();
+ MultiThreadingMgr::instance().setMode(false);
+ }
+
+ /// @brief Destructor
+ ~LoadUnloadDhcpv4SrvTest() {
+ server_.reset();
+ reset();
+ MultiThreadingMgr::instance().setMode(false);
+ };
+
+ /// @brief Reset hooks data
+ ///
+ /// Resets the data for the hooks-related portion of the test by ensuring
+ /// that no libraries are loaded and that any marker files are deleted.
+ void reset() {
+ // Unload any previously-loaded libraries.
+ EXPECT_TRUE(HooksManager::unloadLibraries());
+
+ // Get rid of any marker files.
+ static_cast<void>(remove(LOAD_MARKER_FILE));
+ static_cast<void>(remove(UNLOAD_MARKER_FILE));
+ static_cast<void>(remove(SRV_CONFIG_MARKER_FILE));
+
+ CfgMgr::instance().clear();
+ }
+};
+
+// Checks if callouts installed on buffer4_receive are indeed called and the
+// all necessary parameters are passed.
+//
+// Note that the test name does not follow test naming convention,
+// but the proper hook name is "buffer4_receive".
+TEST_F(HooksDhcpv4SrvTest, buffer4ReceiveSimple) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Install buffer4_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "buffer4_receive", buffer4_receive_callout));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered buffer4_receive callback.
+ srv_->run();
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("buffer4_receive", callback_name_);
+
+ // Check that pkt4 argument passing was successful and returned proper value
+ EXPECT_TRUE(callback_qry_pkt4_.get() == discover.get());
+
+ // Check that all expected parameters are there
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back(string("query4"));
+
+ EXPECT_TRUE(expected_argument_names == callback_argument_names_);
+
+ // Pkt passed to a callout must be configured to copy retrieved options.
+ EXPECT_TRUE(callback_qry_options_copy_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(discover);
+}
+
+// Checks if callouts installed on buffer4_receive is able to change
+// the values and the parameters are indeed used by the server.
+TEST_F(HooksDhcpv4SrvTest, buffer4ReceiveValueChange) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Install buffer4_receive_change_hwaddr_callout that modifies MAC addr of incoming packet
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "buffer4_receive", buffer4_receive_change_hwaddr_callout));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered buffer4_receive callback.
+ srv_->run();
+
+ // Check that the server did send a response
+ ASSERT_EQ(1, srv_->fake_sent_.size());
+
+ // Make sure that we received a response
+ Pkt4Ptr offer = srv_->fake_sent_.front();
+ ASSERT_TRUE(offer);
+
+ // Get client-id...
+ HWAddrPtr hwaddr = offer->getHWAddr();
+
+ ASSERT_TRUE(hwaddr); // basic sanity check. HWAddr is always present
+
+ // ... and check if it is the modified value
+ ASSERT_FALSE(hwaddr->hwaddr_.empty()); // there must be a MAC address
+ EXPECT_EQ(0xff, hwaddr->hwaddr_[0]); // check that its first byte was modified
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(discover);
+}
+
+// Checks if callouts installed on buffer4_receive is able to set skip flag that
+// will cause the server to not process the packet (drop), even though it is valid.
+TEST_F(HooksDhcpv4SrvTest, buffer4ReceiveSkip) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Install buffer4_receive_skip_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "buffer4_receive", buffer4_receive_skip_callout));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered buffer4_receive callback.
+ srv_->run();
+
+ // Check that the server dropped the packet and did not produce any response
+ ASSERT_EQ(0, srv_->fake_sent_.size());
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(discover);
+}
+
+// Checks if callouts installed on buffer4_receive is able to set drop flag that
+// will cause the server to not process the packet (drop), even though it is valid.
+TEST_F(HooksDhcpv4SrvTest, buffer4ReceiveDrop) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Install buffer4_receive_drop_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "buffer4_receive", buffer4_receive_drop_callout));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered buffer4_receive callback.
+ srv_->run();
+
+ // Check that the server dropped the packet and did not produce any response
+ ASSERT_EQ(0, srv_->fake_sent_.size());
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(discover);
+}
+
+// Checks if callouts installed on pkt4_receive are indeed called and the
+// all necessary parameters are passed.
+//
+// Note that the test name does not follow test naming convention,
+// but the proper hook name is "pkt4_receive".
+TEST_F(HooksDhcpv4SrvTest, pkt4ReceiveSimple) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Install pkt4_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt4_receive", pkt4_receive_callout));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt4_receive callback.
+ srv_->run();
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("pkt4_receive", callback_name_);
+
+ // Check that pkt4 argument passing was successful and returned proper value
+ EXPECT_TRUE(callback_qry_pkt4_.get() == discover.get());
+
+ // Check that all expected parameters are there
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back(string("query4"));
+
+ EXPECT_TRUE(expected_argument_names == callback_argument_names_);
+
+ // Pkt passed to a callout must be configured to copy retrieved options.
+ EXPECT_TRUE(callback_qry_options_copy_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(discover);
+}
+
+// Checks if callouts installed on pkt4_received is able to change
+// the values and the parameters are indeed used by the server.
+TEST_F(HooksDhcpv4SrvTest, pkt4ReceiveValueChange) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Install pkt4_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt4_receive", pkt4_receive_change_clientid_callout));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt4_receive callback.
+ srv_->run();
+
+ // Check that the server did send a response
+ ASSERT_EQ(1, srv_->fake_sent_.size());
+
+ // Make sure that we received a response
+ Pkt4Ptr adv = srv_->fake_sent_.front();
+ ASSERT_TRUE(adv);
+
+ // Get client-id...
+ OptionPtr clientid = adv->getOption(DHO_DHCP_CLIENT_IDENTIFIER);
+
+ // ... and check if it is the modified value
+ OptionPtr expected = createOption(DHO_DHCP_CLIENT_IDENTIFIER);
+ EXPECT_TRUE(clientid->equals(expected));
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(discover);
+}
+
+// Checks if callouts installed on pkt4_received is able to delete
+// existing options and that change impacts server processing (mandatory
+// client-id option is deleted, so the packet is expected to be dropped)
+TEST_F(HooksDhcpv4SrvTest, pkt4ReceiveDeleteClientId) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Install pkt4_receive_delete_clientid_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt4_receive", pkt4_receive_delete_clientid_callout));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt4_receive callback.
+ srv_->run();
+
+ // Check that the server dropped the packet and did not send a response
+ ASSERT_EQ(0, srv_->fake_sent_.size());
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(discover);
+}
+
+// Checks if callouts installed on pkt4_received is able to set skip flag that
+// will cause the server to not process the packet (drop), even though it is valid.
+TEST_F(HooksDhcpv4SrvTest, pkt4ReceiveSkip) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Install pkt4_receive_skip_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt4_receive", pkt4_receive_skip_callout));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt4_receive callback.
+ srv_->run();
+
+ // Check that the server dropped the packet and did not produce any response
+ ASSERT_EQ(0, srv_->fake_sent_.size());
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(discover);
+}
+
+// Checks if callouts installed on pkt4_received is able to set drop flag that
+// will cause the server to not process the packet (drop), even though it is valid.
+TEST_F(HooksDhcpv4SrvTest, pkt4ReceiveDrop) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Install pkt4_receive_drop_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt4_receive", pkt4_receive_drop_callout));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt4_receive callback.
+ srv_->run();
+
+ // Check that the server dropped the packet and did not produce any response
+ ASSERT_EQ(0, srv_->fake_sent_.size());
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(discover);
+}
+
+// Checks if callouts installed on pkt4_send are indeed called and the
+// all necessary parameters are passed.
+TEST_F(HooksDhcpv4SrvTest, pkt4SendSimple) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Install pkt4_send_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt4_send", pkt4_send_callout));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt4_send callback.
+ srv_->run();
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("pkt4_send", callback_name_);
+
+ // Check that there is one packet sent
+ ASSERT_EQ(1, srv_->fake_sent_.size());
+ Pkt4Ptr adv = srv_->fake_sent_.front();
+
+ // Check that pkt4 argument passing was successful and returned proper
+ // values
+ ASSERT_TRUE(callback_qry_pkt4_);
+ EXPECT_TRUE(callback_qry_pkt4_.get() == discover.get());
+ ASSERT_TRUE(callback_resp_pkt4_);
+ EXPECT_TRUE(callback_resp_pkt4_.get() == adv.get());
+
+ // Check that all expected parameters are there
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back(string("query4"));
+ expected_argument_names.push_back(string("response4"));
+ sort(callback_argument_names_.begin(), callback_argument_names_.end());
+ sort(expected_argument_names.begin(), expected_argument_names.end());
+ EXPECT_TRUE(expected_argument_names == callback_argument_names_);
+
+ // Pkt passed to a callout must be configured to copy retrieved options.
+ EXPECT_TRUE(callback_qry_options_copy_);
+ EXPECT_TRUE(callback_resp_options_copy_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(discover);
+}
+
+// Checks if callouts installed on pkt4_send is able to change
+// the values and the packet sent contains those changes
+TEST_F(HooksDhcpv4SrvTest, pkt4SendValueChange) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Install pkt4_send_change_serverid_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt4_send", pkt4_send_change_serverid_callout));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt4_send callback.
+ srv_->run();
+
+ // Check that the server did send a response
+ ASSERT_EQ(1, srv_->fake_sent_.size());
+
+ // Make sure that we received a response
+ Pkt4Ptr adv = srv_->fake_sent_.front();
+ ASSERT_TRUE(adv);
+
+ // Get client-id...
+ OptionPtr clientid = adv->getOption(DHO_DHCP_SERVER_IDENTIFIER);
+
+ // ... and check if it is the modified value
+ OptionPtr expected = createOption(DHO_DHCP_SERVER_IDENTIFIER);
+ EXPECT_TRUE(clientid->equals(expected));
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(discover);
+}
+
+// Checks if callouts installed on pkt4_send is able to delete
+// existing options and that server applies those changes. In particular,
+// we are trying to send a packet without server-id. The packet should
+// be sent
+TEST_F(HooksDhcpv4SrvTest, pkt4SendDeleteServerId) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Install pkt4_send_delete_serverid_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt4_send", pkt4_send_delete_serverid_callout));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt4_send callback.
+ srv_->run();
+
+ // Check that the server indeed sent a malformed ADVERTISE
+ ASSERT_EQ(1, srv_->fake_sent_.size());
+
+ // Get that ADVERTISE
+ Pkt4Ptr adv = srv_->fake_sent_.front();
+ ASSERT_TRUE(adv);
+
+ // Make sure that it does not have server-id
+ EXPECT_FALSE(adv->getOption(DHO_DHCP_SERVER_IDENTIFIER));
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(discover);
+}
+
+// Checks if callouts installed on pkt4_skip is able to set skip flag that
+// will cause the server to not process the packet (drop), even though it is valid.
+TEST_F(HooksDhcpv4SrvTest, pkt4SendSkip) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Install pkt4_send_skip_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt4_send", pkt4_send_skip_callout));
+
+ // Let's create a simple REQUEST
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt4_send callback.
+ srv_->run();
+
+ // Check that the server sent the packet
+ ASSERT_EQ(1, srv_->fake_sent_.size());
+
+ // Get the first packet and check that it has zero length (i.e. the server
+ // did not do packing on its own)
+ Pkt4Ptr sent = srv_->fake_sent_.front();
+
+ // The actual size of sent packet should be 0
+ EXPECT_EQ(0, sent->getBuffer().getLength());
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(discover);
+}
+
+// Checks if callouts installed on pkt4_drop is able to set drop flag that
+// will cause the server to not process the packet (drop), even though it is valid.
+TEST_F(HooksDhcpv4SrvTest, pkt4SendDrop) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Install pkt4_send_drop_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt4_send", pkt4_send_drop_callout));
+
+ // Let's create a simple REQUEST
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt4_send callback.
+ srv_->run();
+
+ // Check that the server does not send the packet
+ EXPECT_EQ(0, srv_->fake_sent_.size());
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(discover);
+}
+
+// Checks if callouts installed on buffer4_send are indeed called and the
+// all necessary parameters are passed.
+TEST_F(HooksDhcpv4SrvTest, buffer4SendSimple) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Install buffer4_send_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "buffer4_send", buffer4_send_callout));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered buffer4_send callback.
+ srv_->run();
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("buffer4_send", callback_name_);
+
+ // Check that there is one packet sent
+ ASSERT_EQ(1, srv_->fake_sent_.size());
+ Pkt4Ptr adv = srv_->fake_sent_.front();
+
+ // Check that pkt4 argument passing was successful and returned proper value
+ EXPECT_TRUE(callback_resp_pkt4_.get() == adv.get());
+
+ // Check that all expected parameters are there
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back(string("response4"));
+ EXPECT_TRUE(expected_argument_names == callback_argument_names_);
+
+ // Pkt passed to a callout must be configured to copy retrieved options.
+ EXPECT_TRUE(callback_resp_options_copy_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(discover);
+}
+
+// Checks if callouts installed on buffer4_send are indeed called and that
+// the output buffer can be changed.
+TEST_F(HooksDhcpv4SrvTest, buffer4SendChange) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Install buffer4_send_change_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "buffer4_send", buffer4_send_change_callout));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered buffer4_send callback.
+ srv_->run();
+
+ // Check that there is one packet sent
+ ASSERT_EQ(1, srv_->fake_sent_.size());
+ Pkt4Ptr adv = srv_->fake_sent_.front();
+
+ // The callout is supposed to fill the output buffer with dummyFile content
+ ASSERT_EQ(sizeof(dummyFile), adv->getBuffer().getLength());
+ EXPECT_EQ(0, memcmp(adv->getBuffer().getData(), dummyFile, sizeof(dummyFile)));
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(discover);
+}
+
+// Checks if callouts installed on buffer4_send can set skip flag and that flag
+// causes the packet to not be sent
+TEST_F(HooksDhcpv4SrvTest, buffer4SendSkip) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Install buffer4_send_skip_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "buffer4_send", buffer4_send_skip_callout));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered buffer4_send callback.
+ srv_->run();
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("buffer4_send", callback_name_);
+
+ // Check that there is no packet sent.
+ ASSERT_EQ(0, srv_->fake_sent_.size());
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(discover);
+}
+
+// Checks if callouts installed on buffer4_send can set drop flag and that flag
+// causes the packet to not be sent
+TEST_F(HooksDhcpv4SrvTest, buffer4SendDrop) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Install buffer4_send_drop_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "buffer4_send", buffer4_send_drop_callout));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered buffer4_send callback.
+ srv_->run();
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("buffer4_send", callback_name_);
+
+ // Check that there is no packet sent
+ EXPECT_EQ(0, srv_->fake_sent_.size());
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(discover);
+}
+
+// This test checks if subnet4_select callout is triggered and reports
+// valid parameters
+TEST_F(HooksDhcpv4SrvTest, subnet4SelectSimple) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Configure 2 subnets, both directly reachable over local interface
+ // (let's not complicate the matter with relays)
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1, "
+ " \"pools\": [ { \"pool\": \"192.0.2.0/25\" } ],"
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"interface\": \"eth0\" "
+ " }, {"
+ " \"id\": 2, "
+ " \"pools\": [ { \"pool\": \"192.0.3.0/25\" } ],"
+ " \"subnet\": \"192.0.3.0/24\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ EXPECT_NO_THROW(json = parseDHCP4(config));
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ comment_ = parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ // Commit the config
+ CfgMgr::instance().commit();
+
+ // Install subnet4_select_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "subnet4_select", subnet4_select_callout));
+
+ // Prepare discover packet. Server should select first subnet for it
+ Pkt4Ptr discover = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ discover->setRemoteAddr(IOAddress("192.0.2.1"));
+ discover->setIface("eth1");
+ discover->setIndex(ETH1_INDEX);
+ OptionPtr clientid = generateClientId();
+ discover->addOption(clientid);
+
+ // Pass it to the server and get an advertise
+ Pkt4Ptr adv = srv_->processDiscover(discover);
+
+ // Check if we get response at all
+ ASSERT_TRUE(adv);
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("subnet4_select", callback_name_);
+
+ // Check that pkt4 argument passing was successful and returned proper value
+ EXPECT_TRUE(callback_qry_pkt4_.get() == discover.get());
+
+ const Subnet4Collection* exp_subnets =
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+
+ // The server is supposed to pick the first subnet, because of matching
+ // interface. Check that the value is reported properly.
+ ASSERT_TRUE(callback_subnet4_);
+ EXPECT_EQ(callback_subnet4_.get(), exp_subnets->begin()->get());
+
+ // Server is supposed to report two subnets
+ ASSERT_EQ(exp_subnets->size(), callback_subnet4collection_->size());
+ ASSERT_GE(exp_subnets->size(), 2);
+
+ // Compare that the available subnets are reported as expected
+ EXPECT_TRUE((*exp_subnets->begin())->get() == (*callback_subnet4collection_->begin())->get());
+ EXPECT_TRUE((*std::next(exp_subnets->begin()))->get() == (*std::next(callback_subnet4collection_->begin()))->get());
+
+ // Pkt passed to a callout must be configured to copy retrieved options.
+ EXPECT_TRUE(callback_qry_options_copy_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(discover);
+}
+
+// This test checks if callout installed on subnet4_select hook point can pick
+// a different subnet.
+TEST_F(HooksDhcpv4SrvTest, subnet4SelectChange) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Configure 2 subnets, both directly reachable over local interface
+ // (let's not complicate the matter with relays)
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"id\": 1, "
+ " \"pools\": [ { \"pool\": \"192.0.2.0/25\" } ],"
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"interface\": \"eth0\" "
+ " }, {"
+ " \"id\": 2, "
+ " \"pools\": [ { \"pool\": \"192.0.3.0/25\" } ],"
+ " \"subnet\": \"192.0.3.0/24\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ EXPECT_NO_THROW(json = parseDHCP4(config));
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ comment_ = parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ CfgMgr::instance().commit();
+
+ // Install subnet4_select_different_subnet_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "subnet4_select", subnet4_select_different_subnet_callout));
+
+ // Prepare discover packet. Server should select first subnet for it
+ Pkt4Ptr discover = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ discover->setRemoteAddr(IOAddress("192.0.2.1"));
+ discover->setIface("eth0");
+ discover->setIndex(ETH0_INDEX);
+ OptionPtr clientid = generateClientId();
+ discover->addOption(clientid);
+
+ // Pass it to the server and get an advertise
+ Pkt4Ptr adv = srv_->processDiscover(discover);
+
+ // Check if we get response at all
+ ASSERT_TRUE(adv);
+
+ // The response should have an address from second pool, so let's check it
+ IOAddress addr = adv->getYiaddr();
+ EXPECT_NE("0.0.0.0", addr.toText());
+
+ // Get all subnets and use second subnet for verification
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ ASSERT_EQ(2, subnets->size());
+
+ // Advertised address must belong to the second pool (in subnet's range,
+ // in dynamic pool)
+ auto subnet = subnets->begin();
+ ++subnet;
+ EXPECT_TRUE((*subnet)->inRange(addr));
+ EXPECT_TRUE((*subnet)->inPool(Lease::TYPE_V4, addr));
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(discover);
+}
+
+// Checks that subnet4_select is able to drop the packet.
+TEST_F(HooksDhcpv4SrvTest, subnet4SelectDrop) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Install subnet4_select_drop_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "subnet4_select", subnet4_select_drop_callout));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered subnet4_select callback.
+ srv_->run();
+
+ // Check that the server dropped the packet and did not produce any response
+ ASSERT_EQ(0, srv_->fake_sent_.size());
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(discover);
+}
+
+// This test verifies that the leases4_committed hook point is not triggered
+// for the DHCPDISCOVER.
+TEST_F(HooksDhcpv4SrvTest, leases4CommittedDiscover) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Install leases4_committed_callout
+ ASSERT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "leases4_committed", leases4_committed_callout));
+
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ client.setIfaceName("eth1");
+ client.setIfaceIndex(ETH1_INDEX);
+ ASSERT_NO_THROW(client.doDiscover());
+
+ // Make sure that we received a response
+ ASSERT_TRUE(client.getContext().response_);
+
+ // Make sure that the callout wasn't called.
+ EXPECT_TRUE(callback_name_.empty());
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(client.getContext().query_);
+}
+
+// This test verifies that the leases4_committed hook point is not triggered
+// for the DHCPINFORM.
+TEST_F(HooksDhcpv4SrvTest, leases4CommittedInform) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ ASSERT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "leases4_committed", leases4_committed_callout));
+
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ client.useRelay();
+ ASSERT_NO_THROW(client.doInform());
+
+ // Make sure that we received a response
+ ASSERT_TRUE(client.getContext().response_);
+
+ // Make sure that the callout wasn't called.
+ EXPECT_TRUE(callback_name_.empty());
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(client.getContext().query_);
+}
+
+// This test verifies that the callout installed on the leases4_committed hook
+// point is executed as a result of DHCPREQUEST message sent to allocate new
+// lease or renew an existing lease.
+TEST_F(HooksDhcpv4SrvTest, leases4CommittedRequest) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ ASSERT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "leases4_committed", leases4_committed_callout));
+
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ client.setIfaceName("eth1");
+ client.setIfaceIndex(ETH1_INDEX);
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<IOAddress>(new IOAddress("192.0.2.100"))));
+
+ // Make sure that we received a response
+ ASSERT_TRUE(client.getContext().response_);
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("leases4_committed", callback_name_);
+
+ // Check if all expected parameters were really received
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back("query4");
+ expected_argument_names.push_back("deleted_leases4");
+ expected_argument_names.push_back("leases4");
+
+ sort(expected_argument_names.begin(), expected_argument_names.end());
+ EXPECT_TRUE(callback_argument_names_ == expected_argument_names);
+
+ // Newly allocated lease should be returned.
+ ASSERT_TRUE(callback_lease4_);
+ EXPECT_EQ("192.0.2.100", callback_lease4_->addr_.toText());
+
+ // Deleted lease must not be present, because it is a new allocation.
+ EXPECT_FALSE(callback_deleted_lease4_);
+
+ // Pkt passed to a callout must be configured to copy retrieved options.
+ EXPECT_TRUE(callback_qry_options_copy_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(client.getContext().query_);
+
+ resetCalloutBuffers();
+
+ // Renew the lease and make sure that the callout has been executed.
+ client.setState(Dhcp4Client::RENEWING);
+ ASSERT_NO_THROW(client.doRequest());
+
+ // Make sure that we received a response
+ ASSERT_TRUE(client.getContext().response_);
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("leases4_committed", callback_name_);
+
+ // Renewed lease should be returned.
+ ASSERT_TRUE(callback_lease4_);
+ EXPECT_EQ("192.0.2.100", callback_lease4_->addr_.toText());
+
+ // Deleted lease must not be present, because it is a new allocation.
+ EXPECT_FALSE(callback_deleted_lease4_);
+
+ // Pkt passed to a callout must be configured to copy retrieved options.
+ EXPECT_TRUE(callback_qry_options_copy_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(client.getContext().query_);
+
+ resetCalloutBuffers();
+
+ // Let's try to renew again but force the client to request a different
+ // address.
+ client.ciaddr_ = IOAddress("192.0.2.101");
+
+ ASSERT_NO_THROW(client.doRequest());
+
+ // Make sure that we received a response
+ ASSERT_TRUE(client.getContext().response_);
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("leases4_committed", callback_name_);
+
+ // New lease should be returned.
+ ASSERT_TRUE(callback_lease4_);
+ EXPECT_EQ("192.0.2.101", callback_lease4_->addr_.toText());
+
+ // The old lease should have been deleted.
+ ASSERT_TRUE(callback_deleted_lease4_);
+ EXPECT_EQ("192.0.2.100", callback_deleted_lease4_->addr_.toText());
+
+ // Pkt passed to a callout must be configured to copy retrieved options.
+ EXPECT_TRUE(callback_qry_options_copy_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(client.getContext().query_);
+
+ resetCalloutBuffers();
+
+ // Now request an address that can't be allocated.
+ client.ciaddr_ = IOAddress("10.0.0.1");
+
+ ASSERT_NO_THROW(client.doRequest());
+
+ // Make sure that we did not receive a response. Since we're
+ // not authoritative, there should not be a DHCPNAK.
+ ASSERT_FALSE(client.getContext().response_);
+
+ // Check that no callback was not called.
+ EXPECT_EQ("", callback_name_);
+ EXPECT_FALSE(callback_lease4_);
+ EXPECT_FALSE(callback_deleted_lease4_);
+}
+
+// This test verifies that the leases4_committed callout is executed
+// with declined leases as argument when DHCPDECLINE is processed.
+TEST_F(HooksDhcpv4SrvTest, leases4CommittedDecline) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ ASSERT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "leases4_committed", leases4_committed_callout));
+
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ client.useRelay();
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<IOAddress>(new IOAddress("192.0.2.100"))));
+
+ // Make sure that we received a response
+ ASSERT_TRUE(client.getContext().response_);
+
+ resetCalloutBuffers();
+
+ ASSERT_NO_THROW(client.doDecline());
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("leases4_committed", callback_name_);
+
+ // Check if all expected parameters were really received
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back("query4");
+ expected_argument_names.push_back("deleted_leases4");
+ expected_argument_names.push_back("leases4");
+
+ sort(expected_argument_names.begin(), expected_argument_names.end());
+ EXPECT_TRUE(callback_argument_names_ == expected_argument_names);
+
+ // No new allocations.
+ ASSERT_TRUE(callback_lease4_);
+ EXPECT_EQ("192.0.2.100", callback_lease4_->addr_.toText());
+ EXPECT_EQ(Lease::STATE_DECLINED, callback_lease4_->state_);
+
+ // Released lease should be returned.
+ EXPECT_FALSE(callback_deleted_lease4_);
+
+ // Pkt passed to a callout must be configured to copy retrieved options.
+ EXPECT_TRUE(callback_qry_options_copy_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(client.getContext().query_);
+}
+
+// This test verifies that the leases4_committed callout is executed
+// with deleted leases as argument when DHCPRELEASE is processed.
+TEST_F(HooksDhcpv4SrvTest, leases4CommittedRelease) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ ASSERT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "leases4_committed", leases4_committed_callout));
+
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ client.setIfaceName("eth1");
+ client.setIfaceIndex(ETH1_INDEX);
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<IOAddress>(new IOAddress("192.0.2.100"))));
+
+ // Make sure that we received a response
+ ASSERT_TRUE(client.getContext().response_);
+
+ resetCalloutBuffers();
+
+ ASSERT_NO_THROW(client.doRelease());
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("leases4_committed", callback_name_);
+
+ // Check if all expected parameters were really received
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back("query4");
+ expected_argument_names.push_back("deleted_leases4");
+ expected_argument_names.push_back("leases4");
+
+ sort(expected_argument_names.begin(), expected_argument_names.end());
+ EXPECT_TRUE(callback_argument_names_ == expected_argument_names);
+
+ // No new allocations.
+ EXPECT_FALSE(callback_lease4_);
+
+ // Released lease should be returned.
+ ASSERT_TRUE(callback_deleted_lease4_);
+ EXPECT_EQ("192.0.2.100", callback_deleted_lease4_->addr_.toText());
+
+ // Pkt passed to a callout must be configured to copy retrieved options.
+ EXPECT_TRUE(callback_qry_options_copy_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(client.getContext().query_);
+}
+
+// This test verifies that the callout installed on the leases4_committed hook
+// point is executed as a result of DHCPREQUEST message sent to reuse an
+// existing lease.
+TEST_F(HooksDhcpv4SrvTest, leases4CommittedCache) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ ASSERT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "leases4_committed", leases4_committed_callout));
+
+ // Modify the subnet to reuse leases.
+ subnet_->setCacheThreshold(.25);
+
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ client.setIfaceName("eth1");
+ client.setIfaceIndex(ETH1_INDEX);
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<IOAddress>(new IOAddress("192.0.2.100"))));
+
+ // Make sure that we received a response
+ ASSERT_TRUE(client.getContext().response_);
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("leases4_committed", callback_name_);
+
+ // Check if all expected parameters were really received
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back("query4");
+ expected_argument_names.push_back("deleted_leases4");
+ expected_argument_names.push_back("leases4");
+
+ sort(expected_argument_names.begin(), expected_argument_names.end());
+ EXPECT_TRUE(callback_argument_names_ == expected_argument_names);
+
+ // Newly allocated lease should be returned.
+ ASSERT_TRUE(callback_lease4_);
+ EXPECT_EQ("192.0.2.100", callback_lease4_->addr_.toText());
+
+ // Deleted lease must not be present, because it is a new allocation.
+ EXPECT_FALSE(callback_deleted_lease4_);
+
+ // Pkt passed to a callout must be configured to copy retrieved options.
+ EXPECT_TRUE(callback_qry_options_copy_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(client.getContext().query_);
+
+ resetCalloutBuffers();
+
+ // Renew the lease and make sure that the callout has been executed.
+ client.setState(Dhcp4Client::RENEWING);
+ ASSERT_NO_THROW(client.doRequest());
+
+ // Make sure that we received a response
+ ASSERT_TRUE(client.getContext().response_);
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("leases4_committed", callback_name_);
+
+ // Renewed lease should not be present because it was reused.
+ EXPECT_FALSE(callback_lease4_);
+
+ // Deleted lease must not be present, because it renews the same address.
+ EXPECT_FALSE(callback_deleted_lease4_);
+
+ // Pkt passed to a callout must be configured to copy retrieved options.
+ EXPECT_TRUE(callback_qry_options_copy_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(client.getContext().query_);
+}
+
+// This test verifies that it is possible to park a packet as a result of
+// the leases4_committed callouts.
+TEST_F(HooksDhcpv4SrvTest, leases4CommittedParkRequests) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // This callout uses provided IO service object to post a function
+ // that unparks the packet. The packet is parked and can be unparked
+ // by simply calling IOService::poll.
+ ASSERT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "leases4_committed", leases4_committed_park_callout));
+
+ // Create first client and perform DORA.
+ Dhcp4Client client1(Dhcp4Client::SELECTING);
+ client1.setIfaceName("eth1");
+ client1.setIfaceIndex(ETH1_INDEX);
+ ASSERT_NO_THROW(client1.doDORA(boost::shared_ptr<IOAddress>(new IOAddress("192.0.2.100"))));
+
+ // We should be offered an address but the DHCPACK should not arrive
+ // at this point, because the packet is parked.
+ ASSERT_FALSE(client1.getContext().response_);
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("leases4_committed", callback_name_);
+
+ // Check if all expected parameters were really received
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back("query4");
+ expected_argument_names.push_back("deleted_leases4");
+ expected_argument_names.push_back("leases4");
+
+ sort(expected_argument_names.begin(), expected_argument_names.end());
+ EXPECT_TRUE(callback_argument_names_ == expected_argument_names);
+
+ // Newly allocated lease should be passed to the callout.
+ ASSERT_TRUE(callback_lease4_);
+ EXPECT_EQ("192.0.2.100", callback_lease4_->addr_.toText());
+
+ // Deleted lease must not be present, because it is a new allocation.
+ EXPECT_FALSE(callback_deleted_lease4_);
+
+ // Pkt passed to a callout must be configured to copy retrieved options.
+ EXPECT_TRUE(callback_qry_options_copy_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(client1.getContext().query_);
+
+ // Reset all indicators because we'll be now creating a second client.
+ resetCalloutBuffers();
+
+ // Create the second client to test that it may communicate with the
+ // server while the previous packet is parked.
+ Dhcp4Client client2(client1.getServer(), Dhcp4Client::SELECTING);
+ client2.setIfaceName("eth1");
+ client2.setIfaceIndex(ETH1_INDEX);
+ ASSERT_NO_THROW(client2.doDORA(boost::shared_ptr<IOAddress>(new IOAddress("192.0.2.101"))));
+
+ // The DHCPOFFER should have been returned but not DHCPACK, as this
+ // packet got parked too.
+ ASSERT_FALSE(client2.getContext().response_);
+
+ // Check that the callback called is indeed the one we installed.
+ EXPECT_EQ("leases4_committed", callback_name_);
+
+ // There should be now two actions scheduled on our IO service
+ // by the invoked callouts. They unpark both DHCPACK messages.
+ ASSERT_NO_THROW(io_service_->poll());
+
+ // Receive and check the first response.
+ ASSERT_NO_THROW(client1.receiveResponse());
+ Pkt4Ptr rsp = client1.getContext().response_;
+ ASSERT_TRUE(rsp);
+ EXPECT_EQ(DHCPACK, rsp->getType());
+ EXPECT_EQ("192.0.2.100", rsp->getYiaddr().toText());
+
+ // Receive and check the second response.
+ ASSERT_NO_THROW(client2.receiveResponse());
+ rsp = client2.getContext().response_;
+ ASSERT_TRUE(rsp);
+ EXPECT_EQ(DHCPACK, rsp->getType());
+ EXPECT_EQ("192.0.2.101", rsp->getYiaddr().toText());
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(client2.getContext().query_);
+}
+
+// This test verifies that incoming (positive) REQUEST/Renewing can be handled
+// properly and that callout installed on lease4_renew is triggered with
+// expected parameters.
+TEST_F(HooksDhcpv4SrvTest, lease4RenewSimple) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ const IOAddress addr("192.0.2.106");
+ const uint32_t temp_valid = 100;
+ const time_t temp_timestamp = time(NULL) - 10;
+
+ // Install lease4_renew_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease4_renew", lease4_renew_callout));
+
+ // Generate client-id also sets client_id_ member
+ OptionPtr clientid = generateClientId();
+
+ // Check that the address we are about to use is indeed in pool
+ ASSERT_TRUE(subnet_->inPool(Lease::TYPE_V4, addr));
+
+ // let's create a lease and put it in the LeaseMgr
+ uint8_t hwaddr2_data[] = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe };
+ HWAddrPtr hwaddr2(new HWAddr(hwaddr2_data, sizeof(hwaddr2_data), HTYPE_ETHER));
+ Lease4Ptr used(new Lease4(IOAddress("192.0.2.106"), hwaddr2,
+ &client_id_->getClientId()[0], client_id_->getClientId().size(),
+ temp_valid, temp_timestamp, subnet_->getID()));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(used));
+
+ // Check that the lease is really in the database
+ Lease4Ptr l = LeaseMgrFactory::instance().getLease4(addr);
+ ASSERT_TRUE(l);
+
+ // Let's create a RENEW
+ Pkt4Ptr req = Pkt4Ptr(new Pkt4(DHCPREQUEST, 1234));
+ req->setRemoteAddr(IOAddress(addr));
+ req->setYiaddr(addr);
+ req->setCiaddr(addr); // client's address
+ req->setIface("eth0");
+ req->setIndex(ETH0_INDEX);
+ req->setHWAddr(hwaddr2);
+
+ req->addOption(clientid);
+ req->addOption(srv_->getServerID());
+
+ // Pass it to the server and hope for a response
+ Pkt4Ptr ack = srv_->processRequest(req);
+
+ // Check if we get response at all
+ checkResponse(ack, DHCPACK, 1234);
+
+ // Check that the lease is really in the database
+ l = checkLease(ack, clientid, req->getHWAddr(), addr);
+ ASSERT_TRUE(l);
+
+ // Check that preferred, valid and cltt were really updated
+ EXPECT_EQ(l->valid_lft_, subnet_->getValid());
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("lease4_renew", callback_name_);
+
+ // Check that query4 argument passing was successful and
+ // returned proper value
+ EXPECT_TRUE(callback_qry_pkt4_.get() == req.get());
+
+ // Check that hwaddr parameter is passed properly
+ ASSERT_TRUE(callback_hwaddr_);
+ EXPECT_TRUE(*callback_hwaddr_ == *req->getHWAddr());
+
+ // Check that the subnet is passed properly
+ ASSERT_TRUE(callback_subnet4_);
+ EXPECT_EQ(callback_subnet4_->toText(), subnet_->toText());
+
+ ASSERT_TRUE(callback_clientid_);
+ ASSERT_TRUE(client_id_);
+ EXPECT_TRUE(*client_id_ == *callback_clientid_);
+
+ // Check if all expected parameters were really received
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back("query4");
+ expected_argument_names.push_back("subnet4");
+ expected_argument_names.push_back("clientid");
+ expected_argument_names.push_back("hwaddr");
+ expected_argument_names.push_back("lease4");
+ sort(callback_argument_names_.begin(), callback_argument_names_.end());
+ sort(expected_argument_names.begin(), expected_argument_names.end());
+
+ EXPECT_TRUE(callback_argument_names_ == expected_argument_names);
+
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(addr);
+ EXPECT_TRUE(LeaseMgrFactory::instance().deleteLease(lease));
+
+ // Pkt passed to a callout must be configured to copy retrieved options.
+ EXPECT_TRUE(callback_qry_options_copy_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(req);
+}
+
+// This test verifies that a callout installed on lease4_renew can trigger
+// the server to not renew a lease.
+TEST_F(HooksDhcpv4SrvTest, lease4RenewSkip) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ const IOAddress addr("192.0.2.106");
+ const uint32_t temp_valid = 100;
+ const time_t temp_timestamp = time(NULL) - 10;
+
+ // Install lease4_renew_skip_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease4_renew", lease4_renew_skip_callout));
+
+ // Generate client-id also sets client_id_ member
+ OptionPtr clientid = generateClientId();
+
+ // Check that the address we are about to use is indeed in pool
+ ASSERT_TRUE(subnet_->inPool(Lease::TYPE_V4, addr));
+
+ // let's create a lease and put it in the LeaseMgr
+ uint8_t hwaddr2_data[] = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe };
+ HWAddrPtr hwaddr2(new HWAddr(hwaddr2_data, sizeof(hwaddr2_data), HTYPE_ETHER));
+ Lease4Ptr used(new Lease4(IOAddress("192.0.2.106"), hwaddr2,
+ &client_id_->getClientId()[0], client_id_->getClientId().size(),
+ temp_valid, temp_timestamp, subnet_->getID()));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(used));
+
+ // Check that the lease is really in the database
+ Lease4Ptr l = LeaseMgrFactory::instance().getLease4(addr);
+ ASSERT_TRUE(l);
+
+ // Check that preferred, valid and cltt really set.
+ // Constructed lease looks as if it was assigned 10 seconds ago
+ EXPECT_EQ(l->valid_lft_, temp_valid);
+ EXPECT_EQ(l->cltt_, temp_timestamp);
+
+ // Let's create a RENEW
+ Pkt4Ptr req = Pkt4Ptr(new Pkt4(DHCPREQUEST, 1234));
+ req->setRemoteAddr(IOAddress(addr));
+ req->setYiaddr(addr);
+ req->setCiaddr(addr); // client's address
+ req->setIface("eth0");
+ req->setIndex(ETH0_INDEX);
+ req->setHWAddr(hwaddr2);
+
+ req->addOption(clientid);
+ req->addOption(srv_->getServerID());
+
+ // Pass it to the server and hope for a response
+ Pkt4Ptr ack = srv_->processRequest(req);
+ ASSERT_TRUE(ack);
+
+ // Check that the lease is really in the database
+ l = checkLease(ack, clientid, req->getHWAddr(), addr);
+ ASSERT_TRUE(l);
+
+ // Check that valid and cltt were NOT updated
+ EXPECT_EQ(temp_valid, l->valid_lft_);
+ EXPECT_EQ(temp_timestamp, l->cltt_);
+
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(addr);
+ EXPECT_TRUE(LeaseMgrFactory::instance().deleteLease(lease));
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(req);
+}
+
+// This test verifies that valid RELEASE triggers lease4_release callouts
+TEST_F(HooksDhcpv4SrvTest, lease4ReleaseSimple) {
+ IfaceMgrTestConfig test_config(true);
+ CfgMgr::instance().getCurrentCfg()->getCfgExpiration()->setFlushReclaimedTimerWaitTime(0);
+ CfgMgr::instance().getCurrentCfg()->getCfgExpiration()->setHoldReclaimedTime(0);
+ IfaceMgr::instance().openSockets4();
+
+ const IOAddress addr("192.0.2.106");
+ const uint32_t temp_valid = 100;
+ const time_t temp_timestamp = time(NULL) - 10;
+
+ // Install lease4_release_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease4_release", lease4_release_callout));
+
+ // Generate client-id also duid_
+ OptionPtr clientid = generateClientId();
+
+ // Check that the address we are about to use is indeed in pool
+ ASSERT_TRUE(subnet_->inPool(Lease::TYPE_V4, addr));
+
+ // Let's create a lease and put it in the LeaseMgr
+ uint8_t mac_addr[] = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe };
+ HWAddrPtr hw(new HWAddr(mac_addr, sizeof(mac_addr), HTYPE_ETHER));
+ Lease4Ptr used(new Lease4(addr, hw, &client_id_->getClientId()[0],
+ client_id_->getClientId().size(), temp_valid,
+ temp_timestamp, subnet_->getID()));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(used));
+
+ // Check that the lease is really in the database
+ Lease4Ptr l = LeaseMgrFactory::instance().getLease4(addr);
+ ASSERT_TRUE(l);
+
+ // Let's create a RELEASE
+ // Generate client-id also duid_
+ Pkt4Ptr rel = Pkt4Ptr(new Pkt4(DHCPRELEASE, 1234));
+ rel->setRemoteAddr(addr);
+ rel->setCiaddr(addr);
+ rel->addOption(clientid);
+ rel->addOption(srv_->getServerID());
+ rel->setHWAddr(hw);
+
+ // Note: there is no response to RELEASE in DHCPv4
+ EXPECT_NO_THROW(srv_->processRelease(rel));
+
+ // The lease should be gone from LeaseMgr
+ l = LeaseMgrFactory::instance().getLease4(addr);
+ EXPECT_FALSE(l);
+
+ // Try to get the lease by hardware address
+ Lease4Collection leases = LeaseMgrFactory::instance().getLease4(*hw);
+ EXPECT_EQ(leases.size(), 0);
+
+ // Try to get it by hw/subnet_id combination
+ l = LeaseMgrFactory::instance().getLease4(*hw, subnet_->getID());
+ EXPECT_FALSE(l);
+
+ // Try by client-id
+ leases = LeaseMgrFactory::instance().getLease4(*client_id_);
+ EXPECT_EQ(leases.size(), 0);
+
+ // Try by client-id/subnet-id
+ l = LeaseMgrFactory::instance().getLease4(*client_id_, subnet_->getID());
+ EXPECT_FALSE(l);
+
+ // Ok, the lease is *really* not there.
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("lease4_release", callback_name_);
+
+ // Check that pkt4 argument passing was successful and returned proper value
+ EXPECT_TRUE(callback_qry_pkt4_.get() == rel.get());
+
+ // Check if all expected parameters were really received
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back("query4");
+ expected_argument_names.push_back("lease4");
+ sort(callback_argument_names_.begin(), callback_argument_names_.end());
+ sort(expected_argument_names.begin(), expected_argument_names.end());
+ EXPECT_TRUE(callback_argument_names_ == expected_argument_names);
+
+ // Pkt passed to a callout must be configured to copy retrieved options.
+ EXPECT_TRUE(callback_qry_options_copy_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(rel);
+}
+
+// This test verifies that valid RELEASE triggers lease4_release callouts
+// This test is using infinite lease with lease affinity enabled.
+TEST_F(HooksDhcpv4SrvTest, lease4ReleaseSimpleInfiniteLease) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ const IOAddress addr("192.0.2.106");
+ const uint32_t temp_valid = Lease::INFINITY_LFT;
+ const time_t temp_timestamp = time(NULL) - 10;
+
+ // Install lease4_release_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease4_release", lease4_release_callout));
+
+ // Generate client-id also duid_
+ OptionPtr clientid = generateClientId();
+
+ // Check that the address we are about to use is indeed in pool
+ ASSERT_TRUE(subnet_->inPool(Lease::TYPE_V4, addr));
+
+ // Let's create a lease and put it in the LeaseMgr
+ uint8_t mac_addr[] = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe };
+ HWAddrPtr hw(new HWAddr(mac_addr, sizeof(mac_addr), HTYPE_ETHER));
+ Lease4Ptr used(new Lease4(addr, hw, &client_id_->getClientId()[0],
+ client_id_->getClientId().size(), temp_valid,
+ temp_timestamp, subnet_->getID()));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(used));
+
+ // Check that the lease is really in the database
+ Lease4Ptr l = LeaseMgrFactory::instance().getLease4(addr);
+ ASSERT_TRUE(l);
+
+ // Let's create a RELEASE
+ // Generate client-id also duid_
+ Pkt4Ptr rel = Pkt4Ptr(new Pkt4(DHCPRELEASE, 1234));
+ rel->setRemoteAddr(addr);
+ rel->setCiaddr(addr);
+ rel->addOption(clientid);
+ rel->addOption(srv_->getServerID());
+ rel->setHWAddr(hw);
+
+ // Note: there is no response to RELEASE in DHCPv4
+ EXPECT_NO_THROW(srv_->processRelease(rel));
+
+ // The lease should be gone from LeaseMgr
+ l = LeaseMgrFactory::instance().getLease4(addr);
+ EXPECT_FALSE(l);
+
+ // Try to get the lease by hardware address
+ Lease4Collection leases = LeaseMgrFactory::instance().getLease4(*hw);
+ EXPECT_EQ(leases.size(), 0);
+
+ // Try to get it by hw/subnet_id combination
+ l = LeaseMgrFactory::instance().getLease4(*hw, subnet_->getID());
+ EXPECT_FALSE(l);
+
+ // Try by client-id
+ leases = LeaseMgrFactory::instance().getLease4(*client_id_);
+ EXPECT_EQ(leases.size(), 0);
+
+ // Try by client-id/subnet-id
+ l = LeaseMgrFactory::instance().getLease4(*client_id_, subnet_->getID());
+ EXPECT_FALSE(l);
+
+ // Ok, the lease is *really* not there.
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("lease4_release", callback_name_);
+
+ // Check that pkt4 argument passing was successful and returned proper value
+ EXPECT_TRUE(callback_qry_pkt4_.get() == rel.get());
+
+ // Check if all expected parameters were really received
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back("query4");
+ expected_argument_names.push_back("lease4");
+ sort(callback_argument_names_.begin(), callback_argument_names_.end());
+ sort(expected_argument_names.begin(), expected_argument_names.end());
+ EXPECT_TRUE(callback_argument_names_ == expected_argument_names);
+
+ // Pkt passed to a callout must be configured to copy retrieved options.
+ EXPECT_TRUE(callback_qry_options_copy_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(rel);
+}
+
+// This test verifies that valid RELEASE triggers lease4_release callouts
+TEST_F(HooksDhcpv4SrvTest, lease4ReleaseSimpleNoDelete) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ const IOAddress addr("192.0.2.106");
+ const uint32_t temp_valid = 100;
+ const time_t temp_timestamp = time(NULL) - 10;
+
+ // Install lease4_release_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease4_release", lease4_release_callout));
+
+ // Generate client-id also duid_
+ OptionPtr clientid = generateClientId();
+
+ // Check that the address we are about to use is indeed in pool
+ ASSERT_TRUE(subnet_->inPool(Lease::TYPE_V4, addr));
+
+ // Let's create a lease and put it in the LeaseMgr
+ uint8_t mac_addr[] = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe };
+ HWAddrPtr hw(new HWAddr(mac_addr, sizeof(mac_addr), HTYPE_ETHER));
+ Lease4Ptr used(new Lease4(addr, hw, &client_id_->getClientId()[0],
+ client_id_->getClientId().size(), temp_valid,
+ temp_timestamp, subnet_->getID()));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(used));
+
+ // Check that the lease is really in the database
+ Lease4Ptr l = LeaseMgrFactory::instance().getLease4(addr);
+ ASSERT_TRUE(l);
+
+ // Let's create a RELEASE
+ // Generate client-id also duid_
+ Pkt4Ptr rel = Pkt4Ptr(new Pkt4(DHCPRELEASE, 1234));
+ rel->setRemoteAddr(addr);
+ rel->setCiaddr(addr);
+ rel->addOption(clientid);
+ rel->addOption(srv_->getServerID());
+ rel->setHWAddr(hw);
+
+ // Note: there is no response to RELEASE in DHCPv4
+ EXPECT_NO_THROW(srv_->processRelease(rel));
+
+ // The lease should not be gone from LeaseMgr
+ l = LeaseMgrFactory::instance().getLease4(addr);
+ EXPECT_TRUE(l);
+
+ // Try to get the lease by hardware address
+ Lease4Collection leases = LeaseMgrFactory::instance().getLease4(*hw);
+ EXPECT_EQ(leases.size(), 1);
+
+ // Try to get it by hw/subnet_id combination
+ l = LeaseMgrFactory::instance().getLease4(*hw, subnet_->getID());
+ EXPECT_TRUE(l);
+
+ // Try by client-id
+ leases = LeaseMgrFactory::instance().getLease4(*client_id_);
+ EXPECT_EQ(leases.size(), 1);
+
+ // Try by client-id/subnet-id
+ l = LeaseMgrFactory::instance().getLease4(*client_id_, subnet_->getID());
+ EXPECT_TRUE(l);
+
+ // Ok, the lease is *really* there.
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("lease4_release", callback_name_);
+
+ // Check that pkt4 argument passing was successful and returned proper value
+ EXPECT_TRUE(callback_qry_pkt4_.get() == rel.get());
+
+ // Check if all expected parameters were really received
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back("query4");
+ expected_argument_names.push_back("lease4");
+ sort(callback_argument_names_.begin(), callback_argument_names_.end());
+ sort(expected_argument_names.begin(), expected_argument_names.end());
+ EXPECT_TRUE(callback_argument_names_ == expected_argument_names);
+
+ // Pkt passed to a callout must be configured to copy retrieved options.
+ EXPECT_TRUE(callback_qry_options_copy_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(rel);
+}
+
+// This test verifies that skip flag returned by a callout installed on the
+// lease4_release hook point will keep the lease.
+TEST_F(HooksDhcpv4SrvTest, lease4ReleaseSkip) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ const IOAddress addr("192.0.2.106");
+ const uint32_t temp_valid = 100;
+ const time_t temp_timestamp = time(NULL) - 10;
+
+ // Install lease4_release_skip_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease4_release", lease4_release_skip_callout));
+
+ // Generate client-id also duid_
+ OptionPtr clientid = generateClientId();
+
+ // Check that the address we are about to use is indeed in pool
+ ASSERT_TRUE(subnet_->inPool(Lease::TYPE_V4, addr));
+
+ // Let's create a lease and put it in the LeaseMgr
+ uint8_t mac_addr[] = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe };
+ HWAddrPtr hw(new HWAddr(mac_addr, sizeof(mac_addr), HTYPE_ETHER));
+ Lease4Ptr used(new Lease4(addr, hw, &client_id_->getClientId()[0],
+ client_id_->getClientId().size(), temp_valid,
+ temp_timestamp, subnet_->getID()));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(used));
+
+ // Check that the lease is really in the database
+ Lease4Ptr l = LeaseMgrFactory::instance().getLease4(addr);
+ ASSERT_TRUE(l);
+
+ // Let's create a RELEASE
+ // Generate client-id also duid_
+ Pkt4Ptr rel = Pkt4Ptr(new Pkt4(DHCPRELEASE, 1234));
+ rel->setRemoteAddr(addr);
+ rel->setYiaddr(addr);
+ rel->addOption(clientid);
+ rel->addOption(srv_->getServerID());
+ rel->setHWAddr(hw);
+
+ // Note: there is no response to RELEASE in DHCPv4
+ EXPECT_NO_THROW(srv_->processRelease(rel));
+
+ // The lease should be still there
+ l = LeaseMgrFactory::instance().getLease4(addr);
+ EXPECT_TRUE(l);
+
+ // Try by client-id/subnet-id
+ l = LeaseMgrFactory::instance().getLease4(*client_id_, subnet_->getID());
+ EXPECT_TRUE(l);
+
+ // Try to get the lease by hardware address, should succeed
+ Lease4Collection leases = LeaseMgrFactory::instance().getLease4(*hw);
+ EXPECT_EQ(leases.size(), 1);
+
+ // Try by client-id, should be successful as well.
+ leases = LeaseMgrFactory::instance().getLease4(*client_id_);
+ EXPECT_EQ(leases.size(), 1);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(rel);
+}
+
+// This test verifies that drop flag returned by a callout installed on the
+// lease4_release hook point will keep the lease.
+TEST_F(HooksDhcpv4SrvTest, lease4ReleaseDrop) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ const IOAddress addr("192.0.2.106");
+ const uint32_t temp_valid = 100;
+ const time_t temp_timestamp = time(NULL) - 10;
+
+ // Install lease4_release_drop_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease4_release", lease4_release_drop_callout));
+
+ // Generate client-id also duid_
+ OptionPtr clientid = generateClientId();
+
+ // Check that the address we are about to use is indeed in pool
+ ASSERT_TRUE(subnet_->inPool(Lease::TYPE_V4, addr));
+
+ // Let's create a lease and put it in the LeaseMgr
+ uint8_t mac_addr[] = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe };
+ HWAddrPtr hw(new HWAddr(mac_addr, sizeof(mac_addr), HTYPE_ETHER));
+ Lease4Ptr used(new Lease4(addr, hw, &client_id_->getClientId()[0],
+ client_id_->getClientId().size(), temp_valid,
+ temp_timestamp, subnet_->getID()));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(used));
+
+ // Check that the lease is really in the database
+ Lease4Ptr l = LeaseMgrFactory::instance().getLease4(addr);
+ ASSERT_TRUE(l);
+
+ // Let's create a RELEASE
+ // Generate client-id also duid_
+ Pkt4Ptr rel = Pkt4Ptr(new Pkt4(DHCPRELEASE, 1234));
+ rel->setRemoteAddr(addr);
+ rel->setYiaddr(addr);
+ rel->addOption(clientid);
+ rel->addOption(srv_->getServerID());
+ rel->setHWAddr(hw);
+
+ // Note: there is no response to RELEASE in DHCPv4
+ EXPECT_NO_THROW(srv_->processRelease(rel));
+
+ // The lease should be still there
+ l = LeaseMgrFactory::instance().getLease4(addr);
+ EXPECT_TRUE(l);
+
+ // Try by client-id/subnet-id
+ l = LeaseMgrFactory::instance().getLease4(*client_id_, subnet_->getID());
+ EXPECT_TRUE(l);
+
+ // Try to get the lease by hardware address, should succeed
+ Lease4Collection leases = LeaseMgrFactory::instance().getLease4(*hw);
+ EXPECT_EQ(leases.size(), 1);
+
+ // Try by client-id, should be successful as well.
+ leases = LeaseMgrFactory::instance().getLease4(*client_id_);
+ EXPECT_EQ(leases.size(), 1);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(rel);
+}
+
+// This test checks that the basic decline hook (lease4_decline) is
+// triggered properly.
+TEST_F(HooksDhcpv4SrvTest, lease4DeclineSimple) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Install lease4_decline_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease4_decline", lease4_decline_callout));
+
+ HooksManager::setTestMode(true);
+
+ // Conduct the actual DORA + Decline.
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ acquireAndDecline(client, "01:02:03:04:05:06", "12:14",
+ "01:02:03:04:05:06", "12:14",
+ SHOULD_PASS);
+
+ EXPECT_EQ("lease4_decline", callback_name_);
+
+ // Verifying DHCPDECLINE is a bit tricky, as it is created somewhere in
+ // acquireAndDecline. We'll just verify that it's really a DECLINE
+ // and that its address is equal to what we have in LeaseMgr.
+ ASSERT_TRUE(callback_qry_pkt4_);
+ ASSERT_TRUE(callback_lease4_);
+
+ // Check that it's the proper packet that was reported.
+ EXPECT_EQ(DHCPDECLINE, callback_qry_pkt4_->getType());
+
+ // Extract the address being declined.
+ OptionCustomPtr opt_declined_addr = boost::dynamic_pointer_cast<
+ OptionCustom>(callback_qry_pkt4_->getOption(DHO_DHCP_REQUESTED_ADDRESS));
+ ASSERT_TRUE(opt_declined_addr);
+ IOAddress addr(opt_declined_addr->readAddress());
+
+ // And try to get a matching lease from the lease manager.
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(addr);
+ ASSERT_TRUE(from_mgr);
+ EXPECT_EQ(Lease::STATE_DECLINED, from_mgr->state_);
+
+ // Let's now check that those 3 things (packet, lease returned and lease from
+ // the lease manager) all match.
+ EXPECT_EQ(addr, from_mgr->addr_);
+ EXPECT_EQ(addr, callback_lease4_->addr_);
+
+ // Pkt passed to a callout must be configured to copy retrieved options.
+ EXPECT_TRUE(callback_qry_options_copy_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(client.getContext().query_);
+}
+
+// Test that the lease4_decline hook point can handle SKIP status.
+TEST_F(HooksDhcpv4SrvTest, lease4DeclineSkip) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Install lease4_decline_skip_callout. It will set the status to skip
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease4_decline", lease4_decline_skip_callout));
+
+ HooksManager::setTestMode(true);
+
+ // Conduct the actual DORA + Decline. The DECLINE should fail, as the
+ // hook will set the status to SKIP.
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ acquireAndDecline(client, "01:02:03:04:05:06", "12:14",
+ "01:02:03:04:05:06", "12:14",
+ SHOULD_FAIL);
+
+ EXPECT_EQ("lease4_decline", callback_name_);
+
+ // Verifying DHCPDECLINE is a bit tricky, as it is created somewhere in
+ // acquireAndDecline. We'll just verify that it's really a DECLINE
+ // and that its address is equal to what we have in LeaseMgr.
+ ASSERT_TRUE(callback_qry_pkt4_);
+ ASSERT_TRUE(callback_lease4_);
+
+ // Check that it's the proper packet that was reported.
+ EXPECT_EQ(DHCPDECLINE, callback_qry_pkt4_->getType());
+
+ // Extract the address being declined.
+ OptionCustomPtr opt_declined_addr = boost::dynamic_pointer_cast<
+ OptionCustom>(callback_qry_pkt4_->getOption(DHO_DHCP_REQUESTED_ADDRESS));
+ ASSERT_TRUE(opt_declined_addr);
+ IOAddress addr(opt_declined_addr->readAddress());
+
+ // And try to get a matching lease from the lease manager. The lease should
+ // still be there in default state, not in declined state.
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(addr);
+ ASSERT_TRUE(from_mgr);
+ EXPECT_EQ(Lease::STATE_DEFAULT, from_mgr->state_);
+
+ // As a final sanity check, let's now check that those 3 things (packet,
+ // lease returned and lease from the lease manager) all match.
+ EXPECT_EQ(addr, from_mgr->addr_);
+ EXPECT_EQ(addr, callback_lease4_->addr_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(client.getContext().query_);
+}
+
+// Test that the lease4_decline hook point can handle DROP status.
+TEST_F(HooksDhcpv4SrvTest, lease4DeclineDrop) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Install lease4_decline_drop_callout. It will set the status to drop
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease4_decline", lease4_decline_drop_callout));
+
+ HooksManager::setTestMode(true);
+
+ // Conduct the actual DORA + Decline. The DECLINE should fail, as the
+ // hook will set the status to DROP.
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ acquireAndDecline(client, "01:02:03:04:05:06", "12:14",
+ "01:02:03:04:05:06", "12:14",
+ SHOULD_FAIL);
+
+ EXPECT_EQ("lease4_decline", callback_name_);
+
+ // Verifying DHCPDECLINE is a bit tricky, as it is created somewhere in
+ // acquireAndDecline. We'll just verify that it's really a DECLINE
+ // and that its address is equal to what we have in LeaseMgr.
+ ASSERT_TRUE(callback_qry_pkt4_);
+ ASSERT_TRUE(callback_lease4_);
+
+ // Check that it's the proper packet that was reported.
+ EXPECT_EQ(DHCPDECLINE, callback_qry_pkt4_->getType());
+
+ // Extract the address being declined.
+ OptionCustomPtr opt_declined_addr = boost::dynamic_pointer_cast<
+ OptionCustom>(callback_qry_pkt4_->getOption(DHO_DHCP_REQUESTED_ADDRESS));
+ ASSERT_TRUE(opt_declined_addr);
+ IOAddress addr(opt_declined_addr->readAddress());
+
+ // And try to get a matching lease from the lease manager. The lease should
+ // still be there in default state, not in declined state.
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(addr);
+ ASSERT_TRUE(from_mgr);
+ EXPECT_EQ(Lease::STATE_DEFAULT, from_mgr->state_);
+
+ // As a final sanity check, let's now check that those 3 things (packet,
+ // lease returned and lease from the lease manager) all match.
+ EXPECT_EQ(addr, from_mgr->addr_);
+ EXPECT_EQ(addr, callback_lease4_->addr_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(client.getContext().query_);
+}
+
+// Checks if callout installed on host4_identifier can generate an
+// identifier and whether that identifier is actually used.
+TEST_F(HooksDhcpv4SrvTest, host4Identifier) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Configure a subnet with host reservation. The reservation is based on
+ // flexible identifier value of 'foo'. That's exactly what the
+ // host4_identifier_foo_callout sets.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"host-reservation-identifiers\": [ \"flex-id\" ], "
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.0/25\" } ],"
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"id\": 1, "
+ " \"interface\": \"eth0\", "
+ " \"reservations\": ["
+ " {"
+ " \"flex-id\": \"'foo'\","
+ " \"ip-address\": \"192.0.2.201\""
+ " }"
+ " ]"
+ "} ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ EXPECT_NO_THROW(json = parseDHCP4(config));
+ ASSERT_TRUE(json);
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ comment_ = parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ CfgMgr::instance().commit();
+
+ // Install host4_identifier_foo_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "host4_identifier", host4_identifier_foo_callout));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt4_receive callback.
+ srv_->run();
+
+ // check that the server did send a response
+ ASSERT_EQ(1, srv_->fake_sent_.size());
+
+ // Make sure that we received a response
+ Pkt4Ptr adv = srv_->fake_sent_.front();
+ ASSERT_TRUE(adv);
+
+ // Make sure the address offered is the one that was reserved.
+ EXPECT_EQ("192.0.2.201", adv->getYiaddr().toText());
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(discover);
+}
+
+// Checks if callout installed on host4_identifier can generate an identifier of
+// other type. This particular callout always returns hwaddr.
+TEST_F(HooksDhcpv4SrvTest, host4IdentifierHWAddr) {
+ IfaceMgrTestConfig test_config(true);
+ IfaceMgr::instance().openSockets4();
+
+ // Configure a subnet with host reservation. The reservation is based on
+ // flexible identifier value of 'foo'. That's exactly what the
+ // host4_identifier_foo_callout sets.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"host-reservation-identifiers\": [ \"flex-id\" ], "
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.0/25\" } ],"
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"id\": 1, "
+ " \"interface\": \"eth0\", "
+ " \"reservations\": ["
+ " {"
+ " \"hw-address\": \"00:01:02:03:04:05\","
+ " \"ip-address\": \"192.0.2.201\""
+ " }"
+ " ]"
+ "} ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ EXPECT_NO_THROW(json = parseDHCP4(config));
+ ASSERT_TRUE(json);
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ comment_ = parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ CfgMgr::instance().commit();
+
+ // Install host4_identifier_hwaddr_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "host4_identifier", host4_identifier_hwaddr_callout));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt4_receive callback.
+ srv_->run();
+
+ // check that the server did send a response
+ ASSERT_EQ(1, srv_->fake_sent_.size());
+
+ // Make sure that we received a response
+ Pkt4Ptr adv = srv_->fake_sent_.front();
+ ASSERT_TRUE(adv);
+
+ // Make sure the address offered is the one that was reserved.
+ EXPECT_EQ("192.0.2.201", adv->getYiaddr().toText());
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(discover);
+}
+
+// Verifies that libraries are unloaded by server destruction
+// The callout libraries write their library index number to a marker
+// file upon load and unload, making it simple to test whether or not
+// the load and unload callouts have been invoked.
+TEST_F(LoadUnloadDhcpv4SrvTest, unloadLibraries) {
+
+ ASSERT_NO_THROW(server_.reset(new NakedDhcpv4Srv()));
+
+ // Ensure no marker files to start with.
+ ASSERT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
+ ASSERT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+
+ // Load the test libraries
+ HookLibsCollection libraries;
+ libraries.push_back(make_pair(std::string(CALLOUT_LIBRARY_1),
+ ConstElementPtr()));
+ libraries.push_back(make_pair(std::string(CALLOUT_LIBRARY_2),
+ ConstElementPtr()));
+
+ ASSERT_TRUE(HooksManager::loadLibraries(libraries));
+
+ // Verify that they load functions created the LOAD_MARKER_FILE
+ // and that its contents are correct: "12" - the first library
+ // appends "1" to the file, the second appends "2"). Also
+ // check that the unload marker file does not yet exist.
+ EXPECT_TRUE(checkMarkerFile(LOAD_MARKER_FILE, "12"));
+ EXPECT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+
+ // Destroy the server, instance which should unload the libraries.
+ server_.reset();
+
+ // Check that the libraries were unloaded. The libraries are
+ // unloaded in the reverse order to which they are loaded, and
+ // this should be reflected in the unload file.
+ EXPECT_TRUE(checkMarkerFile(UNLOAD_MARKER_FILE, "21"));
+ EXPECT_TRUE(checkMarkerFile(LOAD_MARKER_FILE, "12"));
+}
+
+// Verifies that libraries incompatible with multi threading are not loaded by
+// the server.
+// The callout libraries write their library index number to a marker
+// file upon load and unload, making it simple to test whether or not
+// the load and unload callouts have been invoked.
+TEST_F(LoadUnloadDhcpv4SrvTest, failLoadIncompatibleLibraries) {
+
+ ASSERT_NO_THROW(server_.reset(new NakedDhcpv4Srv()));
+
+ // Ensure no marker files to start with.
+ ASSERT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
+ ASSERT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+
+ // Load the test libraries
+ HookLibsCollection libraries;
+ libraries.push_back(make_pair(std::string(CALLOUT_LIBRARY_2),
+ ConstElementPtr()));
+
+ ASSERT_FALSE(HooksManager::loadLibraries(libraries, true));
+
+ // The library is missing multi_threading_compatible function so loading
+ // should fail
+ EXPECT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
+ EXPECT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+
+ libraries.clear();
+ libraries.push_back(make_pair(std::string(CALLOUT_LIBRARY_3),
+ ConstElementPtr()));
+
+ ASSERT_FALSE(HooksManager::loadLibraries(libraries, true));
+
+ // The library is not multi threading compatible so loading should fail
+ EXPECT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
+ EXPECT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+
+ // Destroy the server, instance which should unload the libraries.
+ server_.reset();
+
+ // Check that the libraries were unloaded. The libraries are
+ // unloaded in the reverse order to which they are loaded, and
+ // this should be reflected in the unload file.
+ EXPECT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
+ EXPECT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+}
+
+// Checks if callouts installed on the dhcp4_srv_configured ared indeed called
+// and all the necessary parameters are passed.
+TEST_F(LoadUnloadDhcpv4SrvTest, Dhcpv4SrvConfigured) {
+ for (string parameters : {
+ "",
+ R"(, "parameters": { "mode": "fail-without-error" } )",
+ R"(, "parameters": { "mode": "fail-with-error" } )"}) {
+
+ reset();
+
+ boost::shared_ptr<ControlledDhcpv4Srv> srv(new ControlledDhcpv4Srv(0));
+
+ // Ensure no marker files to start with.
+ ASSERT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
+ ASSERT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+ ASSERT_FALSE(checkMarkerFileExists(SRV_CONFIG_MARKER_FILE));
+
+ // Minimal valid configuration for the server. It includes the
+ // section which loads the callout library #3, which implements
+ // dhcp4_srv_configured callout. MT needs to be disabled
+ // since the library is single-threaded.
+ string config_str =
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ ]"
+ " },"
+ " \"rebind-timer\": 2000,"
+ " \"renew-timer\": 1000,"
+ " \"subnet4\": [ ],"
+ " \"valid-lifetime\": 4000,"
+ " \"lease-database\": {"
+ " \"type\": \"memfile\","
+ " \"persist\": false"
+ " },"
+ " \"hooks-libraries\": ["
+ " {"
+ " \"library\": \"" + std::string(CALLOUT_LIBRARY_3) + "\""
+ + parameters +
+ " }"
+ R"( ],
+ "multi-threading": {
+ "enable-multi-threading": false
+ }
+ })";
+
+ ConstElementPtr config = Element::fromJSON(config_str);
+
+ // Configure the server.
+ ConstElementPtr answer;
+ ASSERT_NO_THROW(answer = srv->processConfig(config));
+
+ // Make sure there were no errors.
+ int status_code;
+ parseAnswer(status_code, answer);
+ if (parameters.empty()) {
+ EXPECT_EQ(0, status_code);
+ string expected = "{ \"arguments\": { \"hash\": \"";
+ ConstElementPtr config =
+ CfgMgr::instance().getStagingCfg()->toElement();
+ expected += BaseCommandMgr::getHash(config);
+ expected += "\" }, \"result\": 0, \"text\": ";
+ expected += "\"Configuration successful.\" }";
+ EXPECT_EQ(answer->str(), expected);
+ } else {
+ EXPECT_EQ(1, status_code);
+ if (parameters.find("fail-without-error") != string::npos) {
+ EXPECT_EQ(answer->str(), R"({ "result": 1, "text": "unknown error" })");
+ } else if (parameters.find("fail-with-error") != string::npos) {
+ EXPECT_EQ(answer->str(),
+ R"({ "result": 1, "text": "user explicitly configured me to fail" })");
+ } else {
+ GTEST_FAIL() << "unchecked test case";
+ }
+ }
+
+ // The hook library should have been loaded.
+ EXPECT_TRUE(checkMarkerFile(LOAD_MARKER_FILE, "3"));
+ EXPECT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+ // The dhcp4_srv_configured should have been invoked and the provided
+ // parameters should be recorded.
+ EXPECT_TRUE(checkMarkerFile(SRV_CONFIG_MARKER_FILE,
+ "3io_contextjson_confignetwork_stateserver_config"));
+
+ // Destroy the server, instance which should unload the libraries.
+ srv.reset();
+
+ // The server was destroyed, so the unload() function should now
+ // include the library number in its marker file.
+ EXPECT_TRUE(checkMarkerFile(LOAD_MARKER_FILE, "3"));
+ EXPECT_TRUE(checkMarkerFile(UNLOAD_MARKER_FILE, "3"));
+ EXPECT_TRUE(checkMarkerFile(SRV_CONFIG_MARKER_FILE,
+ "3io_contextjson_confignetwork_stateserver_config"));
+ }
+}
+
+// This test verifies that parked-packet-limit is properly enforced.
+TEST_F(HooksDhcpv4SrvTest, leases4ParkedPacketLimit) {
+ IfaceMgrTestConfig test_config(true);
+
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"parked-packet-limit\": 1,"
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.0/24\" } ],"
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"id\": 1, "
+ " \"interface\": \"eth1\" "
+ " } ],"
+ "\"valid-lifetime\": 4000"
+ "}";
+
+ ConstElementPtr json;
+ EXPECT_NO_THROW(json = parseDHCP4(config));
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(*srv_, json));
+ ASSERT_TRUE(status);
+ comment_ = parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ // Commit the config
+ CfgMgr::instance().commit();
+ IfaceMgr::instance().openSockets4();
+
+ // This callout uses the provided IO service object to post a function
+ // that unparks the packet. Once the packet is parked, it can be unparked
+ // by simply calling IOService::poll.
+ ASSERT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "leases4_committed", leases4_committed_park_callout));
+
+ // Statistic should not show any drops.
+ EXPECT_EQ(0, getStatistic("pkt4-receive-drop"));
+
+ // Create a client and initiate a DORA cycle for it.
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ client.setIfaceName("eth1");
+ client.setIfaceIndex(ETH1_INDEX);
+ ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<IOAddress>(new IOAddress("192.0.2.100"))));
+
+ // Check that the callback called is indeed the one we installed
+ ASSERT_EQ("leases4_committed", callback_name_);
+
+ // Make sure that we have not received a response.
+ ASSERT_FALSE(client.getContext().response_);
+
+ // Verify we have a packet parked.
+ const auto& parking_lot = ServerHooks::getServerHooks().getParkingLotPtr("leases4_committed");
+ ASSERT_TRUE(parking_lot);
+ ASSERT_EQ(1, parking_lot->size());
+
+ // Clear callout buffers.
+ resetCalloutBuffers();
+
+ // Create a second client and initiate a DORA for it.
+ // Since the parking lot limit has been reached, the packet
+ // should be dropped with no response.
+ Dhcp4Client client2(Dhcp4Client::SELECTING);
+ client2.setIfaceName("eth1");
+ client2.setIfaceIndex(ETH1_INDEX);
+ ASSERT_NO_THROW(client2.doDORA(boost::shared_ptr<IOAddress>(new IOAddress("192.0.2.101"))));
+
+ // Check that no callback was called.
+ ASSERT_EQ("", callback_name_);
+
+ // Make sure that we have not received a response.
+ ASSERT_FALSE(client2.getContext().response_);
+
+ // Verify we have not parked another packet.
+ ASSERT_EQ(1, parking_lot->size());
+
+ // Statistic should show one drop.
+ EXPECT_EQ(1, getStatistic("pkt4-receive-drop"));
+
+ // Invoking poll should run the scheduled action only for
+ // the first client.
+ ASSERT_NO_THROW(io_service_->poll());
+
+ // Receive and check the first response.
+ ASSERT_NO_THROW(client.receiveResponse());
+ Pkt4Ptr rsp = client.getContext().response_;
+ ASSERT_TRUE(rsp);
+ EXPECT_EQ(DHCPACK, rsp->getType());
+ EXPECT_EQ("192.0.2.100", rsp->getYiaddr().toText());
+
+ // Verify we have no parked packets.
+ ASSERT_EQ(0, parking_lot->size());
+
+ resetCalloutBuffers();
+
+ // Try client2 again.
+ ASSERT_NO_THROW(client2.doDORA(boost::shared_ptr<IOAddress>(new IOAddress("192.0.2.101"))));
+
+ // Check that the callback called is indeed the one we installed
+ ASSERT_EQ("leases4_committed", callback_name_);
+
+ // Make sure that we have not received a response.
+ ASSERT_FALSE(client2.getContext().response_);
+
+ // Verify we parked the packet.
+ ASSERT_EQ(1, parking_lot->size());
+
+ // Invoking poll should run the scheduled action.
+ ASSERT_NO_THROW(io_service_->poll());
+
+ // Receive and check the first response.
+ ASSERT_NO_THROW(client2.receiveResponse());
+ rsp = client2.getContext().response_;
+ ASSERT_TRUE(rsp);
+ EXPECT_EQ(DHCPACK, rsp->getType());
+ EXPECT_EQ("192.0.2.101", rsp->getYiaddr().toText());
+
+ // Verify we have no parked packets.
+ ASSERT_EQ(0, parking_lot->size());
+
+ // Statistic should still show one drop.
+ EXPECT_EQ(1, getStatistic("pkt4-receive-drop"));
+}
+
+} // namespace
diff --git a/src/bin/dhcp4/tests/host_options_unittest.cc b/src/bin/dhcp4/tests/host_options_unittest.cc
new file mode 100644
index 0000000..db6ccb1
--- /dev/null
+++ b/src/bin/dhcp4/tests/host_options_unittest.cc
@@ -0,0 +1,554 @@
+// Copyright (C) 2016-2020 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <asiolink/io_address.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/docsis3_option_defs.h>
+#include <dhcp/option_int.h>
+#include <dhcp/option_vendor.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <dhcp4/tests/dhcp4_client.h>
+#include <stats/stats_mgr.h>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+
+namespace {
+
+/// @brief Boolean value used to signal stateless configuration test.
+const bool STATELESS = true;
+
+/// @brief Boolean value used to signal stateful configuration test.
+const bool STATEFUL = false;
+
+/// @brief Set of JSON configurations used throughout the tests.
+///
+/// - Configuration 0:
+/// - Used to test that host specific options override subnet specific
+/// options when these options are requested with PRL option.
+/// - Single subnet 10.0.0.0/24 with a pool of 10.0.0.10-10.0.0.100
+/// - 4 options configured within subnet scope
+/// - routers: 10.0.0.200,10.0.0.201,
+/// - domain-name-servers: 10.0.0.202,10.0.0.203,
+/// - log-servers: 10.0.0.200,10.0.0.201,
+/// - cookie-servers: 10.0.0.202,10.0.0.203
+/// - Single reservation within the subnet:
+/// - HW address: aa:bb:cc:dd:ee:ff
+/// - ip-address: 10.0.0.7
+/// - Two options overriding subnet specific options:
+/// - cookie-servers: 10.1.1.202, 10.1.1.203
+/// - log-servers: 10.1.1.200, 10.1.1.201
+///
+/// - Configuration 1:
+/// - Used to test that host specific options override subnet specific
+/// default options. Default options are those that are sent even when
+/// not requested by a client.
+/// - Single subnet 10.0.0.0/24 with a pool of 10.0.0.10-10.0.0.100
+/// - 4 options configured within subnet scope
+/// - routers: 10.0.0.200,10.0.0.201,
+/// - domain-name-servers: 10.0.0.202,10.0.0.203,
+/// - log-servers: 10.0.0.200,10.0.0.201,
+/// - cookie-servers: 10.0.0.202,10.0.0.203
+/// - Single reservation within the subnet:
+/// - HW address: aa:bb:cc:dd:ee:ff
+/// - ip-address: 10.0.0.7
+/// - Two options overriding subnet specific default options:
+/// - routers: 10.1.1.200, 10.1.1.201
+/// - domain-name-servers: 10.1.1.202, 10.1.1.203
+///
+/// - Configuration 2:
+/// - Used to test that client receives options solely specified in a
+/// host scope.
+/// - Single reservation within the subnet:
+/// - HW address: aa:bb:cc:dd:ee:ff
+/// - ip-address: 10.0.0.7
+/// - Two options:
+/// - routers: 10.1.1.200, 10.1.1.201
+/// - cookie-servers: 10.1.1.202, 10.1.1.203
+///
+/// - Configuration 3:
+/// - Used to test that host specific vendor options override globally
+/// specified vendor options.
+/// - Globally specified option 125 with Cable Labs vendor id.
+/// - TFTP servers sub option: 10.0.0.202,10.0.0.203
+/// - Single subnet 10.0.0.0/24 with a pool of 10.0.0.10-10.0.0.100
+/// - Single reservation within the subnet:
+/// - HW address: aa:bb:cc:dd:ee:ff
+/// - ip-address 10.0.0.7
+/// - Vendor option for Cable Labs vendor id specified for the reservation:
+/// - TFTP servers suboption overriding globally specified suboption:
+/// 10.1.1.202,10.1.1.203
+///
+const char* HOST_CONFIGS[] = {
+// Configuration 0
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"relay\": { \"ip-address\": \"10.0.0.233\" },"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"option-data\": [ {"
+ " \"name\": \"routers\","
+ " \"data\": \"10.0.0.200,10.0.0.201\""
+ " },"
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"10.0.0.202,10.0.0.203\""
+ " },"
+ " {"
+ " \"name\": \"log-servers\","
+ " \"data\": \"10.0.0.204,10.0.0.205\""
+ " },"
+ " {"
+ " \"name\": \"cookie-servers\","
+ " \"data\": \"10.0.0.206,10.0.0.207\""
+ " } ],"
+ " \"reservations\": [ "
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"ip-address\": \"10.0.0.7\","
+ " \"option-data\": [ {"
+ " \"name\": \"cookie-servers\","
+ " \"data\": \"10.1.1.202,10.1.1.203\""
+ " },"
+ " {"
+ " \"name\": \"log-servers\","
+ " \"data\": \"10.1.1.200,10.1.1.201\""
+ " } ]"
+ " } ]"
+ " } ]"
+ "}",
+
+// Configuration 1
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"relay\": { \"ip-address\": \"10.0.0.233\" },"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"option-data\": [ {"
+ " \"name\": \"routers\","
+ " \"data\": \"10.0.0.200,10.0.0.201\""
+ " },"
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"10.0.0.202,10.0.0.203\""
+ " },"
+ " {"
+ " \"name\": \"log-servers\","
+ " \"data\": \"10.0.0.204,10.0.0.205\""
+ " },"
+ " {"
+ " \"name\": \"cookie-servers\","
+ " \"data\": \"10.0.0.206,10.0.0.207\""
+ " } ],"
+ " \"reservations\": [ "
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"ip-address\": \"10.0.0.7\","
+ " \"option-data\": [ {"
+ " \"name\": \"routers\","
+ " \"data\": \"10.1.1.200,10.1.1.201\""
+ " },"
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"10.1.1.202,10.1.1.203\""
+ " } ]"
+ " } ]"
+ " } ]"
+ "}",
+
+// Configuration 2
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"relay\": { \"ip-address\": \"10.0.0.233\" },"
+ " \"reservations\": [ "
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"ip-address\": \"10.0.0.7\","
+ " \"option-data\": [ {"
+ " \"name\": \"routers\","
+ " \"data\": \"10.1.1.200,10.1.1.201\""
+ " },"
+ " {"
+ " \"name\": \"cookie-servers\","
+ " \"data\": \"10.1.1.206,10.1.1.207\""
+ " } ]"
+ " } ]"
+ " } ]"
+ "}",
+
+// Configuration 3
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"option-data\": [ {"
+ " \"name\": \"vivso-suboptions\","
+ " \"data\": \"4491\""
+ "},"
+ "{"
+ " \"name\": \"tftp-servers\","
+ " \"space\": \"vendor-4491\","
+ " \"data\": \"10.0.0.202,10.0.0.203\""
+ "} ],"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"relay\": { \"ip-address\": \"10.0.0.233\" },"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"reservations\": [ "
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"ip-address\": \"10.0.0.7\","
+ " \"option-data\": [ {"
+ " \"name\": \"vivso-suboptions\","
+ " \"data\": \"4491\""
+ " },"
+ " {"
+ " \"name\": \"tftp-servers\","
+ " \"space\": \"vendor-4491\","
+ " \"data\": \"10.1.1.202,10.1.1.203\""
+ " } ]"
+ " } ]"
+ " } ]"
+ "}"
+};
+
+/// @brief Test fixture class for testing static reservations of options.
+class HostOptionsTest : public Dhcpv4SrvTest {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Sets up fake interfaces.
+ HostOptionsTest()
+ : Dhcpv4SrvTest(),
+ iface_mgr_test_config_(true) {
+ }
+
+ /// @brief Verifies that host specific options override subnet specific
+ /// options.
+ ///
+ /// Overridden options are requested with Parameter Request List
+ /// option.
+ ///
+ /// @param stateless Boolean value indicating if stateless or stateful
+ /// configuration should be performed.
+ void testOverrideRequestedOptions(const bool stateless);
+
+ /// @brief Verifies that host specific options override subnet specific
+ /// options.
+ ///
+ /// Overridden options are the options which server sends regardless
+ /// if they are requested with Parameter Request List option or not.
+ ///
+ /// @param stateless Boolean value indicating if stateless or stateful
+ /// configuration should be performed.
+ void testOverrideDefaultOptions(const bool stateless);
+
+ /// @brief Verifies that client receives options when they are solely
+ /// defined in the host scope (and not in the global or subnet scope).
+ ///
+ /// @param stateless Boolean value indicating if stateless or stateful
+ /// configuration should be performed.
+ void testHostOnlyOptions(const bool stateless);
+
+ /// @brief Verifies that host specific vendor options override vendor
+ /// options defined in the global scope.
+ ///
+ /// @param stateless Boolean value indicating if stateless or stateful
+ /// configuration should be performed.
+ void testOverrideVendorOptions(const bool stateless);
+
+ /// @brief Interface Manager's fake configuration control.
+ IfaceMgrTestConfig iface_mgr_test_config_;
+
+};
+
+void
+HostOptionsTest::testOverrideRequestedOptions(const bool stateless) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+ client.requestOptions(DHO_DOMAIN_NAME_SERVERS, DHO_LOG_SERVERS,
+ DHO_COOKIE_SERVERS);
+
+ // Configure DHCP server.
+ configure(HOST_CONFIGS[0], *client.getServer());
+
+ if (stateless) {
+ // Need to relay the message from a specific address which can
+ // be matched with a configured subnet.
+ client.useRelay(true, IOAddress("10.0.0.233"));
+ ASSERT_NO_THROW(client.doInform());
+
+ } else {
+ // Perform 4-way exchange with the server but to not request any
+ // specific address in the DHCPDISCOVER message.
+ ASSERT_NO_THROW(client.doDORA());
+ }
+
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ if (!stateless) {
+ // Make sure that the client has got the lease for the reserved
+ // address.
+ ASSERT_EQ("10.0.0.7", client.config_.lease_.addr_.toText());
+ }
+
+ ASSERT_EQ(2, client.config_.routers_.size());
+ EXPECT_EQ("10.0.0.200", client.config_.routers_[0].toText());
+ EXPECT_EQ("10.0.0.201", client.config_.routers_[1].toText());
+ // Make sure that the DNS Servers option has been received.
+ ASSERT_EQ(2, client.config_.dns_servers_.size());
+ EXPECT_EQ("10.0.0.202", client.config_.dns_servers_[0].toText());
+ EXPECT_EQ("10.0.0.203", client.config_.dns_servers_[1].toText());
+ // Make sure that the Quotes Servers option has been received.
+ ASSERT_EQ(2, client.config_.quotes_servers_.size());
+ EXPECT_EQ("10.1.1.202", client.config_.quotes_servers_[0].toText());
+ EXPECT_EQ("10.1.1.203", client.config_.quotes_servers_[1].toText());
+ // Make sure that the Log Servers option has been received.
+ ASSERT_EQ(2, client.config_.log_servers_.size());
+ EXPECT_EQ("10.1.1.200", client.config_.log_servers_[0].toText());
+ EXPECT_EQ("10.1.1.201", client.config_.log_servers_[1].toText());
+}
+
+void
+HostOptionsTest::testOverrideDefaultOptions(const bool stateless) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+
+ client.requestOptions(DHO_LOG_SERVERS, DHO_COOKIE_SERVERS);
+
+ // Configure DHCP server.
+ configure(HOST_CONFIGS[1], *client.getServer());
+
+ if (stateless) {
+ // Need to relay the message from a specific address which can
+ // be matched with a configured subnet.
+ client.useRelay(true, IOAddress("10.0.0.233"));
+ ASSERT_NO_THROW(client.doInform());
+
+ } else {
+ // Perform 4-way exchange with the server but to not request any
+ // specific address in the DHCPDISCOVER message.
+ ASSERT_NO_THROW(client.doDORA());
+ }
+
+ // Perform 4-way exchange with the server but to not request any
+ // specific address in the DHCPDISCOVER message.
+ ASSERT_NO_THROW(client.doDORA());
+
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ if (!stateless) {
+ // Make sure that the client has got the lease for the reserved
+ // address.
+ ASSERT_EQ("10.0.0.7", client.config_.lease_.addr_.toText());
+ }
+
+ ASSERT_EQ(2, client.config_.routers_.size());
+ EXPECT_EQ("10.1.1.200", client.config_.routers_[0].toText());
+ EXPECT_EQ("10.1.1.201", client.config_.routers_[1].toText());
+ // Make sure that the DNS Servers option has been received.
+ ASSERT_EQ(2, client.config_.dns_servers_.size());
+ EXPECT_EQ("10.1.1.202", client.config_.dns_servers_[0].toText());
+ EXPECT_EQ("10.1.1.203", client.config_.dns_servers_[1].toText());
+ // Make sure that the Quotes Servers option has been received.
+ ASSERT_EQ(2, client.config_.quotes_servers_.size());
+ EXPECT_EQ("10.0.0.206", client.config_.quotes_servers_[0].toText());
+ EXPECT_EQ("10.0.0.207", client.config_.quotes_servers_[1].toText());
+ // Make sure that the Log Servers option has been received.
+ ASSERT_EQ(2, client.config_.log_servers_.size());
+ EXPECT_EQ("10.0.0.204", client.config_.log_servers_[0].toText());
+ EXPECT_EQ("10.0.0.205", client.config_.log_servers_[1].toText());
+}
+
+void
+HostOptionsTest::testHostOnlyOptions(const bool stateless) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+ client.requestOptions(DHO_COOKIE_SERVERS);
+
+ // Configure DHCP server.
+ configure(HOST_CONFIGS[2], *client.getServer());
+
+ if (stateless) {
+ // Need to relay the message from a specific address which can
+ // be matched with a configured subnet.
+ client.useRelay(true, IOAddress("10.0.0.233"));
+ ASSERT_NO_THROW(client.doInform());
+
+ } else {
+ // Perform 4-way exchange with the server but to not request any
+ // specific address in the DHCPDISCOVER message.
+ ASSERT_NO_THROW(client.doDORA());
+ }
+
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ if (!stateless) {
+ // Make sure that the client has got the lease for the reserved
+ // address.
+ ASSERT_EQ("10.0.0.7", client.config_.lease_.addr_.toText());
+ }
+
+ // Make sure that the Routers options has been received.
+ ASSERT_EQ(2, client.config_.routers_.size());
+ EXPECT_EQ("10.1.1.200", client.config_.routers_[0].toText());
+ EXPECT_EQ("10.1.1.201", client.config_.routers_[1].toText());
+ // Make sure that the Quotes Servers option has been received.
+ ASSERT_EQ(2, client.config_.quotes_servers_.size());
+ EXPECT_EQ("10.1.1.206", client.config_.quotes_servers_[0].toText());
+ EXPECT_EQ("10.1.1.207", client.config_.quotes_servers_[1].toText());
+
+ // Other options are not configured and should not be delivered.
+ EXPECT_EQ(0, client.config_.dns_servers_.size());
+ EXPECT_EQ(0, client.config_.log_servers_.size());
+}
+
+void
+HostOptionsTest::testOverrideVendorOptions(const bool stateless) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+
+ // Client needs to include V-I Vendor Specific Information option
+ // to include ORO suboption, which the server will use to determine
+ // which suboptions should be returned to the client.
+ OptionVendorPtr opt_vendor(new OptionVendor(Option::V4,
+ VENDOR_ID_CABLE_LABS));
+ // Include ORO with TFTP servers suboption code being requested.
+ opt_vendor->addOption(OptionPtr(new OptionUint8(Option::V4, DOCSIS3_V4_ORO,
+ DOCSIS3_V4_TFTP_SERVERS)));
+ client.addExtraOption(opt_vendor);
+
+ // Configure DHCP server.
+ configure(HOST_CONFIGS[3], *client.getServer());
+
+ if (stateless) {
+ // Need to relay the message from a specific address which can
+ // be matched with a configured subnet.
+ client.useRelay(true, IOAddress("10.0.0.233"));
+ ASSERT_NO_THROW(client.doInform());
+
+ } else {
+ // Perform 4-way exchange with the server.
+ ASSERT_NO_THROW(client.doDORA());
+ }
+
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ if (!stateless) {
+ // Make sure that the client has got the lease for the reserved
+ // address.
+ ASSERT_EQ("10.0.0.7", client.config_.lease_.addr_.toText());
+ }
+
+ // Make sure the server has responded with a V-I Vendor Specific
+ // Information option with exactly one suboption.
+ ASSERT_EQ(1, client.config_.vendor_suboptions_.size());
+ // Assume this suboption is a TFTP servers suboption.
+ std::multimap<unsigned int, OptionPtr>::const_iterator opt =
+ client.config_.vendor_suboptions_.find(DOCSIS3_V4_TFTP_SERVERS);
+ ASSERT_TRUE(opt->second);
+ Option4AddrLstPtr opt_tftp = boost::dynamic_pointer_cast<
+ Option4AddrLst>(opt->second);
+ ASSERT_TRUE(opt_tftp);
+ // TFTP servers suboption should contain addresses specified on host level.
+ const Option4AddrLst::AddressContainer& tftps = opt_tftp->getAddresses();
+ ASSERT_EQ(2, tftps.size());
+ EXPECT_EQ("10.1.1.202", tftps[0].toText());
+ EXPECT_EQ("10.1.1.203", tftps[1].toText());
+}
+
+// This test checks that host specific options override subnet specific
+// options. Overridden options are requested with Parameter Request List
+// option (stateless case).
+TEST_F(HostOptionsTest, overrideRequestedOptionsStateless) {
+ testOverrideRequestedOptions(STATELESS);
+}
+
+// This test checks that host specific options override subnet specific
+// options. Overridden options are requested with Parameter Request List
+// option (stateful case).
+TEST_F(HostOptionsTest, overrideRequestedOptionsStateful) {
+ testOverrideRequestedOptions(STATEFUL);
+}
+
+// This test checks that host specific options override subnet specific
+// options. Overridden options are the options which server sends
+// regardless if they are requested with Parameter Request List option
+// or not (stateless case).
+TEST_F(HostOptionsTest, overrideDefaultOptionsStateless) {
+ testOverrideDefaultOptions(STATELESS);
+}
+
+// This test checks that host specific options override subnet specific
+// options. Overridden options are the options which server sends
+// regardless if they are requested with Parameter Request List option
+// or not (stateful case).
+TEST_F(HostOptionsTest, overrideDefaultOptionsStateful) {
+ testOverrideDefaultOptions(STATEFUL);
+}
+
+// This test checks that client receives options when they are
+// solely defined in the host scope and not in the global or subnet
+// scope (stateless case).
+TEST_F(HostOptionsTest, hostOnlyOptionsStateless) {
+ testHostOnlyOptions(STATELESS);
+}
+
+// This test checks that client receives options when they are
+// solely defined in the host scope and not in the global or subnet
+// scope (stateful case).
+TEST_F(HostOptionsTest, hostOnlyOptionsStateful) {
+ testHostOnlyOptions(STATEFUL);
+}
+
+// This test checks that host specific vendor options override vendor
+// options defined in the global scope (stateless case).
+TEST_F(HostOptionsTest, overrideVendorOptionsStateless) {
+ testOverrideVendorOptions(STATELESS);
+}
+
+// This test checks that host specific vendor options override vendor
+// options defined in the global scope (stateful case).
+TEST_F(HostOptionsTest, overrideVendorOptionsStateful) {
+ testOverrideVendorOptions(STATEFUL);
+}
+
+} // end of anonymous namespace
diff --git a/src/bin/dhcp4/tests/host_unittest.cc b/src/bin/dhcp4/tests/host_unittest.cc
new file mode 100644
index 0000000..5a39c85
--- /dev/null
+++ b/src/bin/dhcp4/tests/host_unittest.cc
@@ -0,0 +1,843 @@
+// Copyright (C) 2018-2023 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <asiolink/io_address.h>
+#include <cc/data.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/host.h>
+#include <dhcpsrv/host_mgr.h>
+#include <dhcpsrv/subnet_id.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <dhcp4/tests/dhcp4_client.h>
+#include <boost/shared_ptr.hpp>
+#include <stats/stats_mgr.h>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::stats;
+
+
+namespace {
+
+/// @brief Set of JSON configuration(s) used throughout the Host tests.
+///
+/// - Configuration 0:
+/// - Used for testing global host reservations
+/// - 5 global reservations
+/// - 1 subnet: 10.0.0.0/24
+const char* CONFIGS[] = {
+ // Configuration 0
+ // 1 subnet, global only,
+ // global reservations for different identifier types
+ "{ \"interfaces-config\": {\n"
+ " \"interfaces\": [ \"*\" ]\n"
+ "},\n"
+ "\"host-reservation-identifiers\": [ \"circuit-id\", \"hw-address\",\n"
+ " \"duid\", \"client-id\" ],\n"
+ "\"reservations\": [ \n"
+ "{\n"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n"
+ " \"hostname\": \"hw-host-dynamic\"\n"
+ "},\n"
+ "{\n"
+ " \"hw-address\": \"01:02:03:04:05:06\",\n"
+ " \"hostname\": \"hw-host-fixed-out-of-range\",\n"
+ " \"ip-address\": \"192.0.1.77\"\n"
+ "},\n"
+ "{\n"
+ " \"hw-address\": \"02:02:03:04:05:06\",\n"
+ " \"hostname\": \"hw-host-fixed-in-range\",\n"
+ " \"ip-address\": \"10.0.0.77\"\n"
+ "},\n"
+ "{\n"
+ " \"duid\": \"01:02:03:04:05\",\n"
+ " \"hostname\": \"duid-host\"\n"
+ "},\n"
+ "{\n"
+ " \"circuit-id\": \"'charter950'\",\n"
+ " \"hostname\": \"circuit-id-host\"\n"
+ "},\n"
+ "{\n"
+ " \"client-id\": \"01:11:22:33:44:55:66\",\n"
+ " \"hostname\": \"client-id-host\"\n"
+ "}\n"
+ "],\n"
+ "\"valid-lifetime\": 600,\n"
+ "\"subnet4\": [ { \n"
+ " \"subnet\": \"10.0.0.0/24\",\n"
+ " \"id\": 10,\n"
+ " \"reservations-global\": true,\n"
+ " \"reservations-in-subnet\": false,\n"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ]\n"
+ "} ]\n"
+ "}\n"
+ ,
+ // Configuration 1 global vs in-subnet
+ // 2 subnets, one default reservations flags (aka in-subnet),
+ // one reservations flags global only
+ // Host reservations for the same client, one global, one in each subnet
+ "{ \"interfaces-config\": {\n"
+ " \"interfaces\": [ \"*\" ]\n"
+ "},\n"
+ "\"valid-lifetime\": 600,\n"
+ "\"reservations\": [ \n"
+ "{\n"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n"
+ " \"hostname\": \"global-host\"\n"
+ "}\n"
+ "],\n"
+ "\"subnet4\": [\n"
+ " {\n"
+ " \"subnet\": \"10.0.0.0/24\", \n"
+ " \"id\": 10, \n"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],\n"
+ " \"interface\": \"eth0\",\n"
+ " \"reservations\": [ \n"
+ " {\n"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n"
+ " \"hostname\": \"subnet-10-host\"\n"
+ " }]\n"
+ " },\n"
+ " {\n"
+ " \"subnet\": \"192.0.2.0/26\", \n"
+ " \"id\": 20,"
+ " \"pools\": [ { \"pool\": \"192.0.2.10-192.0.2.63\" } ],\n"
+ " \"interface\": \"eth1\",\n"
+ " \"reservations-global\": true,\n"
+ " \"reservations-in-subnet\": false,\n"
+ " \"reservations\": [ \n"
+ " {\n"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n"
+ " \"hostname\": \"subnet-20-host\"\n"
+ " }]\n"
+ " }\n"
+ "]\n"
+ "}\n"
+ ,
+ // Configuration 2 global and in-subnet with out-of-pool
+ "{ \"interfaces-config\": {\n"
+ " \"interfaces\": [ \"*\" ]\n"
+ "},\n"
+ "\"valid-lifetime\": 600,\n"
+ "\"reservations\": [ \n"
+ "{\n"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n"
+ " \"hostname\": \"global-host\"\n"
+ "}\n"
+ "],\n"
+ "\"subnet4\": [\n"
+ " {\n"
+ " \"subnet\": \"10.0.0.0/24\", \n"
+ " \"id\": 10, \n"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],\n"
+ " \"interface\": \"eth0\",\n"
+ " \"reservations-global\": false,\n"
+ " \"reservations-in-subnet\": true,\n"
+ " \"reservations-out-of-pool\": true,\n"
+ " \"reservations\": [ \n"
+ " {\n"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n"
+ " \"hostname\": \"subnet-10-host\",\n"
+ " \"ip-address\": \"10.0.0.105\"\n"
+ " }]\n"
+ " }\n"
+ "]\n"
+ "}\n"
+ ,
+ // Configuration 3 global and in-subnet
+ "{ \"interfaces-config\": {\n"
+ " \"interfaces\": [ \"*\" ]\n"
+ "},\n"
+ "\"valid-lifetime\": 600,\n"
+ "\"reservations\": [ \n"
+ "{\n"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n"
+ " \"hostname\": \"global-host\"\n"
+ "}\n"
+ "],\n"
+ "\"subnet4\": [\n"
+ " {\n"
+ " \"subnet\": \"10.0.0.0/24\", \n"
+ " \"id\": 10, \n"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],\n"
+ " \"interface\": \"eth0\",\n"
+ " \"reservations-global\": false,\n"
+ " \"reservations-in-subnet\": true,\n"
+ " \"reservations-out-of-pool\": false,\n"
+ " \"reservations\": [ \n"
+ " {\n"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n"
+ " \"hostname\": \"subnet-10-host\",\n"
+ " \"ip-address\": \"10.0.0.105\"\n"
+ " }]\n"
+ " }\n"
+ "]\n"
+ "}\n"
+ ,
+
+ // Configuration 4 client-class reservation in global, shared network
+ // and client-class guarded pools.
+ "{ \"interfaces-config\": {\n"
+ " \"interfaces\": [ \"*\" ]\n"
+ "},\n"
+ "\"client-classes\": ["
+ "{"
+ " \"name\": \"reserved_class\""
+ "},"
+ "{"
+ " \"name\": \"unreserved_class\","
+ " \"test\": \"not member('reserved_class')\""
+ "}"
+ "],\n"
+ "\"reservations-global\": true,\n"
+ "\"reservations-in-subnet\": false,\n"
+ "\"valid-lifetime\": 600,\n"
+ "\"reservations\": [ \n"
+ "{\n"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:fe\",\n"
+ " \"client-classes\": [ \"reserved_class\" ]\n"
+ "}\n"
+ "],\n"
+ "\"shared-networks\": [{"
+ " \"name\": \"frog\",\n"
+ " \"subnet4\": [\n"
+ " {\n"
+ " \"subnet\": \"10.0.0.0/24\", \n"
+ " \"id\": 10, \n"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"10.0.0.10-10.0.0.11\","
+ " \"client-class\": \"reserved_class\""
+ " }"
+ " ],\n"
+ " \"interface\": \"eth0\"\n"
+ " },\n"
+ " {\n"
+ " \"subnet\": \"192.0.3.0/24\", \n"
+ " \"id\": 11,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.3.10-192.0.3.11\","
+ " \"client-class\": \"unreserved_class\""
+ " }"
+ " ],\n"
+ " \"interface\": \"eth0\"\n"
+ " }\n"
+ " ]\n"
+ "}]\n"
+ "}",
+
+ // Configuration 5 client-class reservation in global, shared network
+ // and client-class guarded subnets.
+ "{ \"interfaces-config\": {\n"
+ " \"interfaces\": [ \"*\" ]\n"
+ "},\n"
+ "\"client-classes\": ["
+ "{"
+ " \"name\": \"reserved_class\""
+ "},"
+ "{"
+ " \"name\": \"unreserved_class\","
+ " \"test\": \"not member('reserved_class')\""
+ "}"
+ "],\n"
+ "\"reservations-global\": true,\n"
+ "\"reservations-in-subnet\": false,\n"
+ "\"valid-lifetime\": 600,\n"
+ "\"reservations\": [ \n"
+ "{\n"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:fe\",\n"
+ " \"client-classes\": [ \"reserved_class\" ]\n"
+ "}\n"
+ "],\n"
+ "\"shared-networks\": [{"
+ " \"name\": \"frog\",\n"
+ " \"subnet4\": [\n"
+ " {\n"
+ " \"subnet\": \"10.0.0.0/24\", \n"
+ " \"id\": 10, \n"
+ " \"client-class\": \"reserved_class\","
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"10.0.0.10-10.0.0.10\""
+ " }"
+ " ],\n"
+ " \"interface\": \"eth0\"\n"
+ " },\n"
+ " {\n"
+ " \"subnet\": \"192.0.3.0/24\", \n"
+ " \"id\": 11,"
+ " \"client-class\": \"unreserved_class\","
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.3.10-192.0.3.10\""
+ " }"
+ " ],\n"
+ " \"interface\": \"eth0\"\n"
+ " }\n"
+ " ]\n"
+ "}]\n"
+ "}",
+
+ // Configuration 6 client-class reservation and client-class guarded pools.
+ "{ \"interfaces-config\": {\n"
+ " \"interfaces\": [ \"*\" ]\n"
+ "},\n"
+ "\"client-classes\": ["
+ "{"
+ " \"name\": \"reserved_class\""
+ "},"
+ "{"
+ " \"name\": \"unreserved_class\","
+ " \"test\": \"not member('reserved_class')\""
+ "}"
+ "],\n"
+ "\"valid-lifetime\": 600,\n"
+ "\"subnet4\": [\n"
+ " {\n"
+ " \"subnet\": \"10.0.0.0/24\", \n"
+ " \"id\": 10, \n"
+ " \"reservations\": [{ \n"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:fe\",\n"
+ " \"client-classes\": [ \"reserved_class\" ]\n"
+ " }],\n"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"10.0.0.10-10.0.0.11\","
+ " \"client-class\": \"reserved_class\""
+ " },"
+ " {"
+ " \"pool\": \"10.0.0.20-10.0.0.21\","
+ " \"client-class\": \"unreserved_class\""
+ " }"
+ " ],\n"
+ " \"interface\": \"eth0\"\n"
+ " }\n"
+ "]\n"
+ "}",
+
+ // Configuration 7 multiple reservations for the same IP address.
+ "{ \"interfaces-config\": {\n"
+ " \"interfaces\": [ \"*\" ]\n"
+ "},\n"
+ "\"valid-lifetime\": 600,\n"
+ "\"ip-reservations-unique\": false,\n"
+ "\"host-reservation-identifiers\": [ \"hw-address\" ],\n"
+ "\"subnet4\": [\n"
+ " {\n"
+ " \"subnet\": \"10.0.0.0/24\",\n"
+ " \"id\": 10,\n"
+ " \"reservations\": [\n"
+ " { \n"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:fe\",\n"
+ " \"ip-address\": \"10.0.0.123\"\n"
+ " },\n"
+ " { \n"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n"
+ " \"ip-address\": \"10.0.0.123\"\n"
+ " }\n"
+ " ],\n"
+ " \"pools\": [\n"
+ " {\n"
+ " \"pool\": \"10.0.0.10-10.0.0.255\"\n"
+ " }\n"
+ " ],\n"
+ " \"interface\": \"eth0\"\n"
+ " }\n"
+ "]\n"
+ "}",
+
+ // Configuration 8 both global and in-subnet
+ // 2 subnets, one default reservations flags (aka in-subnet),
+ // one reservations flags global and in-subnet.
+ // Host reservations for the same client, one global, one in each subnet
+ "{ \"interfaces-config\": {\n"
+ " \"interfaces\": [ \"*\" ]\n"
+ "},\n"
+ "\"valid-lifetime\": 600,\n"
+ "\"reservations\": [ \n"
+ "{\n"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n"
+ " \"hostname\": \"global-host\"\n"
+ "}\n"
+ "],\n"
+ "\"subnet4\": [\n"
+ " {\n"
+ " \"subnet\": \"10.0.0.0/24\", \n"
+ " \"id\": 10, \n"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],\n"
+ " \"interface\": \"eth0\",\n"
+ " \"reservations\": [ \n"
+ " {\n"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n"
+ " \"hostname\": \"subnet-10-host\"\n"
+ " }]\n"
+ " },\n"
+ " {\n"
+ " \"subnet\": \"192.0.2.0/26\", \n"
+ " \"id\": 20,"
+ " \"pools\": [ { \"pool\": \"192.0.2.10-192.0.2.63\" } ],\n"
+ " \"interface\": \"eth1\",\n"
+ " \"reservations-global\": true,\n"
+ " \"reservations-in-subnet\": true,\n"
+ " \"reservations-out-of-pool\": false,\n"
+ " \"reservations\": [ \n"
+ " {\n"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\",\n"
+ " \"hostname\": \"subnet-20-host\"\n"
+ " }]\n"
+ " }\n"
+ "]\n"
+ "}\n"
+};
+
+/// @brief Test fixture class for testing global v4 reservations.
+class HostTest : public Dhcpv4SrvTest {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Sets up fake interfaces.
+ HostTest()
+ : Dhcpv4SrvTest(),
+ iface_mgr_test_config_(true) {
+ // Let's wipe all existing statistics.
+ isc::stats::StatsMgr::instance().removeAll();
+ }
+
+ /// @brief Destructor.
+ ///
+ /// Cleans up statistics after the test.
+ ~HostTest() {
+ // Let's wipe all existing statistics.
+ isc::stats::StatsMgr::instance().removeAll();
+ }
+
+ /// @brief Interface Manager's fake configuration control.
+ IfaceMgrTestConfig iface_mgr_test_config_;
+
+ /// @brief Conducts DORA exchange and checks assigned address and hostname
+ ///
+ /// If expected_host is empty, the test expects the hostname option to not
+ /// be assigned.
+ ///
+ /// @param config configuration to be used
+ /// @param client reference to a client instance
+ /// @param expected_host expected hostname to be assigned (may be empty)
+ /// @param expected_addr expected address to be assigned
+ void runDoraTest(const std::string& config, Dhcp4Client& client,
+ const std::string& expected_host,
+ const std::string& expected_addr,
+ const std::string& requested_addr = "") {
+
+ // Configure DHCP server.
+ ASSERT_NO_FATAL_FAILURE(configure(config, *client.getServer()));
+ client.requestOptions(DHO_HOST_NAME);
+
+ // Perform 4-way exchange with the server but to not request any
+ // specific address in the DHCPDISCOVER message.
+ boost::shared_ptr<IOAddress> hint;
+ if (!requested_addr.empty()) {
+ hint = boost::make_shared<IOAddress>(requested_addr);
+ }
+ ASSERT_NO_THROW(client.doDORA(hint));
+
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Fetch the hostname option
+ OptionStringPtr hostname = boost::dynamic_pointer_cast<
+ OptionString>(resp->getOption(DHO_HOST_NAME));
+
+ if (expected_host.empty()) {
+ ASSERT_FALSE(hostname);
+ } else {
+ ASSERT_TRUE(hostname);
+ EXPECT_EQ(expected_host, hostname->getValue());
+ }
+
+ EXPECT_EQ(client.config_.lease_.addr_.toText(), expected_addr);
+ }
+
+ /// @brief Test pool or subnet selection using global class reservation.
+ ///
+ /// Verifies that client class specified in the global reservation
+ /// may be used to influence pool or subnet selection.
+ ///
+ /// @param config_idx Index of the server configuration from the
+ /// @c CONFIGS array.
+ /// @param first_address Address to be allocated from the pool having
+ /// a reservation.
+ /// @param second_address Address to be allocated from the pool not
+ /// having a reservation.
+ void testGlobalClassSubnetPoolSelection(const int config_idx,
+ const std::string& first_address = "10.0.0.10",
+ const std::string& second_address = "192.0.3.10") {
+ Dhcp4Client client_resrv(Dhcp4Client::SELECTING);
+
+ // Use HW address for which we have host reservation including
+ // client class.
+ client_resrv.setHWAddress("aa:bb:cc:dd:ee:fe");
+ client_resrv.setIfaceName("eth0");
+ client_resrv.setIfaceIndex(ETH0_INDEX);
+
+ ASSERT_NO_FATAL_FAILURE(configure(CONFIGS[config_idx], *client_resrv.getServer()));
+
+ // This client should be given an address from the 10.0.0.0/24 pool.
+ // Let's use the 192.0.3.10 as a hint to make sure that the server
+ // refuses allocating it and uses the sole pool available for this
+ // client.
+ ASSERT_NO_THROW(client_resrv.doDORA(boost::make_shared<IOAddress>(second_address)));
+ ASSERT_TRUE(client_resrv.getContext().response_);
+ auto resp = client_resrv.getContext().response_;
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ EXPECT_EQ(first_address, resp->getYiaddr().toText());
+
+ // This client has no reservation and therefore should be
+ // assigned to the unreserved_class and be given an address
+ // from the other pool.
+ Dhcp4Client client_no_resrv(client_resrv.getServer(), Dhcp4Client::SELECTING);
+ client_no_resrv.setHWAddress("aa:bb:cc:dd:ee:ff");
+ client_no_resrv.setIfaceName("eth0");
+ client_no_resrv.setIfaceIndex(ETH0_INDEX);
+
+ // Let's use the address of 10.0.0.10 as a hint to make sure that the
+ // server refuses it in favor of the 192.0.3.10.
+ ASSERT_NO_THROW(client_no_resrv.doDORA(boost::make_shared<IOAddress>(first_address)));
+ ASSERT_TRUE(client_no_resrv.getContext().response_);
+ resp = client_no_resrv.getContext().response_;
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ EXPECT_EQ(second_address, resp->getYiaddr().toText());
+ }
+
+ /// @brief Test that two clients having reservations for the same IP
+ /// address are offered the reserved lease.
+ ///
+ /// This test verifies the case when two clients have reservations for
+ /// the same IP address. The first client sends DHCPDICOVER and is
+ /// offered the reserved address. At the same time, the second client
+ /// having the reservation for the same IP address performs 4-way
+ /// exchange using the reserved address as a hint in DHCPDISCOVER.
+ /// The client gets the lease for this address. This test verifies
+ /// that the allocation engine correctly identifies that the second
+ /// client has a reservation for this address. In order to verify
+ /// that the allocation engine must fetch all reservations for the
+ /// reserved address and verifies that one of them belongs to the
+ /// second client.
+ ///
+ /// @param hw_address1 Hardware address of the first client having
+ /// the reservation.
+ /// @param hw_address2 Hardware address of the second client having
+ /// the reservation.
+ void testMultipleClientsRace(const std::string& hw_address1,
+ const std::string& hw_address2) {
+ // Create first client having the reservation.
+ Dhcp4Client client1(Dhcp4Client::SELECTING);
+ client1.setHWAddress(hw_address1);
+
+ // Configure the server.
+ ASSERT_NO_FATAL_FAILURE(configure(CONFIGS[7], *client1.getServer()));
+
+ // Sends DHCPDISCOVER and make sure the client is offered the
+ // reserved IP address.
+ client1.doDiscover(boost::make_shared<IOAddress>("10.0.0.123"));
+ ASSERT_TRUE(client1.getContext().response_);
+ Pkt4Ptr resp = client1.getContext().response_;
+ ASSERT_EQ(DHCPOFFER, static_cast<int>(resp->getType()));
+ EXPECT_EQ("10.0.0.123", resp->getYiaddr().toText());
+
+ // Create the second client matching the second reservation for
+ // the given IP address.
+ Dhcp4Client client2(client1.getServer(), Dhcp4Client::SELECTING);
+ client2.setHWAddress(hw_address2);
+
+ // Make sure that the second client gets the reserved lease.
+ client2.doDORA(boost::make_shared<IOAddress>("10.0.0.123"));
+ ASSERT_TRUE(client2.getContext().response_);
+ resp = client2.getContext().response_;
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ EXPECT_EQ("10.0.0.123", resp->getYiaddr().toText());
+ }
+};
+
+// Verifies that a client, which fails to match to a global
+// reservation, still gets a dynamic address when subnet reservations
+// flags are global only.
+TEST_F(HostTest, globalHardwareNoMatch) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ client.setHWAddress("99:99:99:99:99:99");
+ runDoraTest(CONFIGS[0], client, "", "10.0.0.10");
+}
+
+// Verifies that a client, that matches to a global hostname
+// reservation, gets both the hostname and a dynamic address,
+// when the subnet reservations flags are global only.
+TEST_F(HostTest, globalHardwareDynamicAddress) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+ runDoraTest(CONFIGS[0], client, "hw-host-dynamic", "10.0.0.10");
+}
+
+// Verifies that a client matched to a global in-subnet address reservation
+// gets both the hostname and the reserved address when the subnet reservations
+// flags are global only.
+TEST_F(HostTest, globalHardwareFixedAddressInRange) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ client.setHWAddress("02:02:03:04:05:06");
+ runDoraTest(CONFIGS[0], client, "hw-host-fixed-in-range", "10.0.0.77");
+}
+
+// Verifies that a client matched to a global out-of-range address reservation
+// gets the hostname and a dynamic address when the subnet reservations
+// flags are global only.
+TEST_F(HostTest, globalHardwareFixedAddressOutOfRange) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ client.setHWAddress("01:02:03:04:05:06");
+ runDoraTest(CONFIGS[0], client, "hw-host-fixed-out-of-range", "10.0.0.10");
+}
+
+// Verifies that a client can be matched to a global reservation by DUID
+TEST_F(HostTest, globalDuid) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ // Set hw address to a none-matching value
+ client.setHWAddress("99:99:99:99:99:99");
+
+ // - FF is a client identifier type for DUID,
+ // - 45454545 - represents 4 bytes for IAID
+ // - 01:02:03:04:05 - is an actual DUID for which there is a
+ client.includeClientId("FF:45:45:45:45:01:02:03:04:05");
+
+ runDoraTest(CONFIGS[0], client, "duid-host", "10.0.0.10");
+}
+
+// Verifies that a client can be matched to a global reservation by circuit-id
+TEST_F(HostTest, globalCircuitId) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ // Set hw address to a none-matching value
+ client.setHWAddress("99:99:99:99:99:99");
+
+ // Use relay agent so as the circuit-id can be inserted.
+ client.useRelay(true, IOAddress("10.0.0.1"), IOAddress("10.0.0.2"));
+
+ // Set the circuit id
+ client.setCircuitId("charter950");
+
+ runDoraTest(CONFIGS[0], client, "circuit-id-host", "10.0.0.10");
+}
+
+// Verifies that a client can be matched to a global reservation by client-id
+TEST_F(HostTest, globalClientID) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ // Set hw address to a none-matching value
+ client.setHWAddress("99:99:99:99:99:99");
+
+ // - 01 is a client identifier type for CLIENT_ID,
+ // - 11:22:33:44:55:66 - is an actual DUID for which there is a
+ client.includeClientId("01:11:22:33:44:55:66");
+
+ runDoraTest(CONFIGS[0], client, "client-id-host", "10.0.0.10");
+}
+
+// Verifies that even when a matching global reservation exists,
+// client will get a subnet scoped reservation, when subnet
+// reservations flags are default
+TEST_F(HostTest, defaultOverGlobal) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ // Hardware address matches all reservations
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+
+ // Subnet 10 uses default reservations flags (i.e. in-subnet), so its
+ // reservation should be used, rather than global.
+ runDoraTest(CONFIGS[1], client, "subnet-10-host", "10.0.0.10");
+}
+
+// Verifies that when there are matching reservations at
+// both the global and subnet levels, client will be matched
+// to the global reservation, when subnet reservations flags
+// are global only.
+TEST_F(HostTest, globalOverSubnet) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ // Hardware address matches all reservations
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+
+ // Change to subnet 20
+ client.setIfaceName("eth1");
+ client.setIfaceIndex(ETH1_INDEX);
+
+ // Subnet 20 uses global only reservations flags, so the global
+ // reservation should be used, rather than the subnet one.
+ runDoraTest(CONFIGS[1], client, "global-host", "192.0.2.10");
+}
+
+// Verifies that when there are matching reservations at
+// both the global and subnet levels, client will be matched
+// to the subnet reservation, when subnet reservations flags
+// are in-subnet and out-of-pool.
+TEST_F(HostTest, outOfPoolOverGlobal) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ // Hardware address matches all reservations
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+
+ // Subnet 10 uses in-subnet and out-of-pool reservations flags,
+ // so its reservation should be used, rather than global.
+ runDoraTest(CONFIGS[2], client, "subnet-10-host", "10.0.0.105");
+}
+
+// Verifies that when there are matching reservations at
+// both the global and subnet levels, client will be matched
+// to the subnet reservation, when subnet reservations flags
+// are in-subnet only.
+TEST_F(HostTest, allOverGlobal) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ // Hardware address matches all reservations
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+
+ // Subnet 10 uses default reservations flags (i.e. in-subnet), so its
+ // reservation should be used, rather than global.
+ runDoraTest(CONFIGS[3], client, "subnet-10-host", "10.0.0.105");
+}
+
+// Verifies that when there are matching reservations at
+// both the global and subnet levels, client will be matched
+// to the subnet reservation, when subnet reservations flags
+// are global and in-subnet, i.e. the subnet has the preference.
+TEST_F(HostTest, subnetOverGlobal) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ // Hardware address matches all reservations
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+
+ // Change to subnet 20
+ client.setIfaceName("eth1");
+ client.setIfaceIndex(ETH1_INDEX);
+
+ // Subnet 20 uses both global and in-subnet reservations flags,
+ // so the subnet reservation has the preference.
+ runDoraTest(CONFIGS[8], client, "subnet-20-host", "192.0.2.10");
+}
+
+// Verifies that client class specified in the global reservation
+// may be used to influence pool selection.
+TEST_F(HostTest, clientClassGlobalPoolSelection) {
+ ASSERT_NO_FATAL_FAILURE(testGlobalClassSubnetPoolSelection(4));
+}
+
+// Verifies that client class specified in the global reservation
+// may be used to influence subnet selection within shared network.
+TEST_F(HostTest, clientClassGlobalSubnetSelection) {
+ ASSERT_NO_FATAL_FAILURE(testGlobalClassSubnetPoolSelection(5));
+}
+
+// Verifies that client class specified in the reservation may be
+// used to influence pool selection within a subnet.
+TEST_F(HostTest, clientClassPoolSelection) {
+ ASSERT_NO_FATAL_FAILURE(testGlobalClassSubnetPoolSelection(6, "10.0.0.10", "10.0.0.20"));
+}
+
+// Verifies that if the server is configured to allow for specifying
+// multiple reservations for the same IP address the first client
+// matching the reservation will be given this address. The second
+// client will be given a different lease.
+TEST_F(HostTest, firstClientGetsReservedAddress) {
+ // Create a client which has MAC address matching the reservation.
+ Dhcp4Client client1(Dhcp4Client::SELECTING);
+ client1.setHWAddress("aa:bb:cc:dd:ee:fe");
+ // Do 4-way exchange for this client to get the reserved address.
+ runDoraTest(CONFIGS[7], client1, "", "10.0.0.123");
+
+ // Create another client that has a reservation for the same
+ // IP address.
+ Dhcp4Client client2(client1.getServer(), Dhcp4Client::SELECTING);
+ client2.setHWAddress("aa:bb:cc:dd:ee:ff");
+ // Do 4-way exchange with client2.
+ ASSERT_NO_THROW(client2.doDORA());
+
+ // Make sure that the server responded with DHCPACK.
+ ASSERT_TRUE(client2.getContext().response_);
+ Pkt4Ptr resp = client2.getContext().response_;
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Even though the client has reservation for this address the
+ // server should not assign this address because another client
+ // has taken it already.
+ EXPECT_NE("10.0.0.123", resp->getYiaddr().toText());
+ // Ensure stats are being recorded for HR conflicts
+ ObservationPtr subnet_conflicts = StatsMgr::instance().getObservation(
+ "subnet[10].v4-reservation-conflicts");
+ ASSERT_TRUE(subnet_conflicts);
+ ASSERT_EQ(1, subnet_conflicts->getInteger().first);
+ subnet_conflicts = StatsMgr::instance().getObservation("v4-reservation-conflicts");
+ ASSERT_TRUE(subnet_conflicts);
+ ASSERT_EQ(1, subnet_conflicts->getInteger().first);
+
+ // If the client1 releases the reserved lease, the client2 should acquire it.
+ ASSERT_NO_THROW(client1.doRelease());
+
+ // Client2 attempts to renew the currently used lease, but should get the
+ // DHCPNAK.
+ client2.setState(Dhcp4Client::RENEWING);
+ ASSERT_NO_THROW(client2.doRequest());
+ resp = client2.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPNAK, static_cast<int>(resp->getType()));
+
+ // The client falls back to 4-way exchange and gets the reserved address.
+ client2.setState(Dhcp4Client::SELECTING);
+ ASSERT_NO_THROW(client2.doDORA());
+ resp = client2.getContext().response_;
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ EXPECT_EQ("10.0.0.123", resp->getYiaddr().toText());
+}
+
+// This test verifies the case when two clients have reservations for
+// the same IP address. The first client sends DHCPDICOVER and is
+// offered the reserved address. At the same time, the second client
+// having the reservation for the same IP address performs 4-way
+// exchange using the reserved address as a hint in DHCPDISCOVER.
+// The client gets the lease for this address. This test verifies
+// that the allocation engine correctly identifies that the second
+// client has a reservation for this address. In order to verify
+// that the allocation engine must fetch all reservations for the
+// reserved address and verifies that one of them belongs to the
+// second client.
+TEST_F(HostTest, multipleClientsRace1) {
+ ASSERT_NO_FATAL_FAILURE(testMultipleClientsRace("aa:bb:cc:dd:ee:fe",
+ "aa:bb:cc:dd:ee:ff"));
+}
+
+// This is a second variant of the multipleClientsRace1. The test is almost
+// the same but the client matching the second reservation sends DHCPDISCOVER
+// first and then the client having the first reservation performs 4-way
+// exchange. This is to ensure that the order in which reservations are
+// defined does not matter.
+TEST_F(HostTest, multipleClientsRace2) {
+ ASSERT_NO_FATAL_FAILURE(testMultipleClientsRace("aa:bb:cc:dd:ee:ff",
+ "aa:bb:cc:dd:ee:fe"));
+}
+
+} // end of anonymous namespace
diff --git a/src/bin/dhcp4/tests/inform_unittest.cc b/src/bin/dhcp4/tests/inform_unittest.cc
new file mode 100644
index 0000000..ed3114f
--- /dev/null
+++ b/src/bin/dhcp4/tests/inform_unittest.cc
@@ -0,0 +1,657 @@
+// Copyright (C) 2014-2023 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <asiolink/io_address.h>
+#include <cc/data.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/libdhcp++.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <dhcp4/tests/dhcp4_client.h>
+#include <stats/stats_mgr.h>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+
+namespace {
+
+/// @brief Set of JSON configurations used throughout the Inform tests.
+///
+/// - Configuration 0:
+/// - Used for testing direct traffic
+/// - 1 subnet: 10.0.0.0/24
+/// - 1 pool: 10.0.0.10-10.0.0.100
+/// - Router option present: 10.0.0.200 and 10.0.0.201
+/// - Domain Name Server option present: 10.0.0.202, 10.0.0.203.
+/// - Log Servers option present: 192.0.2.200 and 192.0.2.201
+/// - Quotes Servers option present: 192.0.2.202, 192.0.2.203.
+///
+/// - Configuration 1:
+/// - Use for testing relayed messages
+/// - 1 subnet: 192.0.2.0/24
+/// - Router option present: 192.0.2.200 and 192.0.2.201
+/// - Domain Name Server option present: 192.0.2.202, 192.0.2.203.
+/// - Log Servers option present: 192.0.2.200 and 192.0.2.201
+/// - Quotes Servers option present: 192.0.2.202, 192.0.2.203.
+///
+/// - Configuration 2:
+/// - This configuration provides reservations for next-server,
+/// server-hostname and boot-file-name value.
+/// - 1 subnet: 192.0.2.0/24
+/// - 1 reservation for this subnet:
+/// - Client's HW address: aa:bb:cc:dd:ee:ff
+/// - next-server = 10.0.0.7
+/// - server name = "some-name.example.org"
+/// - boot-file-name = "bootfile.efi"
+///
+/// - Configuration 3:
+/// - This configuration provides reservations for big options
+/// server-hostname and boot-file-name value.
+/// - 1 subnet: 192.0.2.0/24
+/// - 1 reservation for this subnet:
+/// - Client's HW address: aa:bb:cc:dd:ee:ff
+/// - option 240 = data
+/// -00010203040506070809-00010203040506070809-00010203040506070809-00010203040506070809
+/// -00010203040506070809-00010203040506070809-00010203040506070809-00010203040506070809
+/// -00010203040506070809-00010203040506070809-00010203040506070809-00010203040506070809
+/// -data
+const char* INFORM_CONFIGS[] = {
+// Configuration 0
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1, "
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"option-data\": [ {"
+ " \"name\": \"routers\","
+ " \"data\": \"10.0.0.200,10.0.0.201\""
+ " },"
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"10.0.0.202,10.0.0.203\""
+ " },"
+ " {"
+ " \"name\": \"log-servers\","
+ " \"data\": \"10.0.0.202,10.0.0.203\""
+ " },"
+ " {"
+ " \"name\": \"cookie-servers\","
+ " \"data\": \"10.0.0.200,10.0.0.201\""
+ " } ]"
+ " } ]"
+ "}",
+
+// Configuration 1
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"id\": 1, "
+ " \"option-data\": [ {"
+ " \"name\": \"routers\","
+ " \"data\": \"192.0.2.200,192.0.2.201\""
+ " },"
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.202,192.0.2.203\""
+ " },"
+ " {"
+ " \"name\": \"log-servers\","
+ " \"data\": \"10.0.0.200,10.0.0.201\""
+ " },"
+ " {"
+ " \"name\": \"cookie-servers\","
+ " \"data\": \"10.0.0.202,10.0.0.203\""
+ " } ]"
+ " } ]"
+ "}",
+
+// Configuration 2
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"next-server\": \"10.0.0.1\","
+ "\"server-hostname\": \"nohost\","
+ "\"boot-file-name\": \"nofile\","
+ "\"subnet4\": [ { "
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"id\": 1, "
+ " \"reservations\": [ "
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"next-server\": \"10.0.0.7\","
+ " \"server-hostname\": \"some-name.example.org\","
+ " \"boot-file-name\": \"bootfile.efi\""
+ " }"
+ " ]"
+ "} ]"
+ "}",
+
+// Configuration 3
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"next-server\": \"10.0.0.1\","
+ "\"server-hostname\": \"nohost\","
+ "\"boot-file-name\": \"nofile\","
+ "\"option-def\": ["
+ " {"
+ " \"array\": false,"
+ " \"code\": 240,"
+ " \"encapsulate\": \"\","
+ " \"name\": \"my-option\","
+ " \"space\": \"dhcp4\","
+ " \"type\": \"string\""
+ " }"
+ "],"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"id\": 1, "
+ " \"reservations\": [ "
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"option-data\": ["
+ " {"
+ " \"always-send\": false,"
+ " \"code\": 240,"
+ " \"name\": \"my-option\","
+ " \"csv-format\": true,"
+ " \"data\": \"data"
+ "-00010203040506070809-00010203040506070809-00010203040506070809-00010203040506070809"
+ "-00010203040506070809-00010203040506070809-00010203040506070809-00010203040506070809"
+ "-00010203040506070809-00010203040506070809-00010203040506070809-00010203040506070809"
+ "-data\","
+ " \"space\": \"dhcp4\""
+ " }"
+ "],"
+ " }"
+ " ]"
+ "} ]"
+ "}",
+};
+
+/// @brief Test fixture class for testing DHCPINFORM.
+class InformTest : public Dhcpv4SrvTest {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Sets up fake interfaces.
+ InformTest()
+ : Dhcpv4SrvTest(),
+ iface_mgr_test_config_(true) {
+ // Let's wipe all existing statistics.
+ isc::stats::StatsMgr::instance().removeAll();
+ }
+
+ /// @brief Destructor.
+ ///
+ /// Cleans up statistics after the test.
+ ~InformTest() {
+ // Let's wipe all existing statistics.
+ isc::stats::StatsMgr::instance().removeAll();
+ }
+
+ /// @brief Interface Manager's fake configuration control.
+ IfaceMgrTestConfig iface_mgr_test_config_;
+
+};
+
+// Test that directly connected client's DHCPINFORM message is processed and
+// DHCPACK message is sent back.
+TEST_F(InformTest, directClientBroadcast) {
+ Dhcp4Client client;
+ // Configure DHCP server.
+ configure(INFORM_CONFIGS[0], *client.getServer());
+ // Request some configuration when DHCPINFORM is sent.
+ client.requestOptions(DHO_LOG_SERVERS, DHO_COOKIE_SERVERS);
+ // Preconfigure the client with the IP address.
+ client.createLease(IOAddress("10.0.0.56"), 600);
+
+ // Send DHCPINFORM message to the server.
+ ASSERT_NO_THROW(client.doInform());
+
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Response should have been unicast to the ciaddr.
+ EXPECT_EQ(IOAddress("10.0.0.56"), resp->getLocalAddr());
+ // The ciaddr should have been copied.
+ EXPECT_EQ(IOAddress("10.0.0.56"), resp->getCiaddr());
+ // Response must not be relayed.
+ EXPECT_FALSE(resp->isRelayed());
+ // Make sure that the server id is present.
+ EXPECT_EQ("10.0.0.1", client.config_.serverid_.toText());
+
+ // Make sure that the Routers option has been received.
+ ASSERT_EQ(2, client.config_.routers_.size());
+ EXPECT_EQ("10.0.0.200", client.config_.routers_[0].toText());
+ EXPECT_EQ("10.0.0.201", client.config_.routers_[1].toText());
+ // Make sure that the DNS Servers option has been received.
+ ASSERT_EQ(2, client.config_.dns_servers_.size());
+ EXPECT_EQ("10.0.0.202", client.config_.dns_servers_[0].toText());
+ EXPECT_EQ("10.0.0.203", client.config_.dns_servers_[1].toText());
+ // Make sure that the Log Servers option has been received.
+ ASSERT_EQ(2, client.config_.quotes_servers_.size());
+ EXPECT_EQ("10.0.0.200", client.config_.quotes_servers_[0].toText());
+ EXPECT_EQ("10.0.0.201", client.config_.quotes_servers_[1].toText());
+ // Make sure that the Quotes Servers option has been received.
+ ASSERT_EQ(2, client.config_.log_servers_.size());
+ EXPECT_EQ("10.0.0.202", client.config_.log_servers_[0].toText());
+ EXPECT_EQ("10.0.0.203", client.config_.log_servers_[1].toText());
+
+ // Check that we can send another DHCPINFORM message using
+ // different ciaddr and we will get the configuration.
+ client.createLease(IOAddress("10.0.0.12"), 600);
+ // This time do not request Quotes Servers option and it should not
+ // be returned.
+ client.requestOptions(DHO_LOG_SERVERS);
+
+ // Send DHCPINFORM.
+ ASSERT_NO_THROW(client.doInform());
+
+ // Make sure that the server responded.
+ resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Response should have been unicast to the ciaddr.
+ EXPECT_EQ(IOAddress("10.0.0.12"), resp->getLocalAddr());
+ // The ciaddr should have been copied.
+ EXPECT_EQ(IOAddress("10.0.0.12"), resp->getCiaddr());
+ // Response must not be relayed.
+ EXPECT_FALSE(resp->isRelayed());
+
+ // Make sure that the server id is present.
+ EXPECT_EQ("10.0.0.1", client.config_.serverid_.toText());
+ // Make sure that the Routers option has been received.
+ ASSERT_EQ(2, client.config_.routers_.size());
+ EXPECT_EQ("10.0.0.200", client.config_.routers_[0].toText());
+ EXPECT_EQ("10.0.0.201", client.config_.routers_[1].toText());
+ // Make sure that the DNS Servers option has been received.
+ ASSERT_EQ(2, client.config_.dns_servers_.size());
+ EXPECT_EQ("10.0.0.202", client.config_.dns_servers_[0].toText());
+ EXPECT_EQ("10.0.0.203", client.config_.dns_servers_[1].toText());
+ // Make sure that the Quotes Servers option hasn't been received.
+ ASSERT_TRUE(client.config_.quotes_servers_.empty());
+}
+
+// This test checks that the server drops DHCPINFORM message when the
+// source address and ciaddr is 0.
+TEST_F(InformTest, directClientBroadcastNoAddress) {
+ Dhcp4Client client;
+ // Configure DHCP server.
+ configure(INFORM_CONFIGS[0], *client.getServer());
+ // Request some configuration when DHCPINFORM is sent.
+ client.requestOptions(DHO_LOG_SERVERS, DHO_COOKIE_SERVERS);
+ // Send DHCPINFORM message to the server.
+ ASSERT_NO_THROW(client.doInform());
+ // Make sure that the server dropped the message.
+ ASSERT_FALSE(client.getContext().response_);
+}
+
+// Test that client's DHCPINFORM message sent to a unicast address
+// is received and processed by the server and that the DHCPACK is
+// is sent.
+TEST_F(InformTest, directClientUnicast) {
+ Dhcp4Client client;
+ // Configure DHCP server.
+ configure(INFORM_CONFIGS[0], *client.getServer());
+ // Preconfigure the client with the IP address.
+ client.createLease(IOAddress("10.0.0.56"), 600);
+ // Set remote address to unicast.
+ client.setDestAddress(IOAddress("10.0.0.1"));
+ // Send DHCPINFORM message to the server.
+ ASSERT_NO_THROW(client.doInform());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Response should have been unicast to the ciaddr.
+ EXPECT_EQ(IOAddress("10.0.0.56"), resp->getLocalAddr());
+ // The ciaddr should have been copied.
+ EXPECT_EQ(IOAddress("10.0.0.56"), resp->getCiaddr());
+ // Response must not be relayed.
+ EXPECT_FALSE(resp->isRelayed());
+ // Make sure that the server id is present.
+ EXPECT_EQ("10.0.0.1", client.config_.serverid_.toText());
+ // Make sure that the Routers option has been received.
+ ASSERT_EQ(2, client.config_.routers_.size());
+ EXPECT_EQ("10.0.0.200", client.config_.routers_[0].toText());
+ EXPECT_EQ("10.0.0.201", client.config_.routers_[1].toText());
+ // Make sure that the DNS Servers option has been received.
+ ASSERT_EQ(2, client.config_.dns_servers_.size());
+ EXPECT_EQ("10.0.0.202", client.config_.dns_servers_[0].toText());
+ EXPECT_EQ("10.0.0.203", client.config_.dns_servers_[1].toText());
+}
+
+// This test checks that the server responds to the source address of the
+// packet received from the directly connected client if the client didn't
+// set the ciaddr.
+TEST_F(InformTest, directClientNoCiaddr) {
+ Dhcp4Client client;
+ // Configure DHCP server.
+ configure(INFORM_CONFIGS[0], *client.getServer());
+ // Preconfigure the client with the IP address.
+ client.createLease(IOAddress("10.0.0.56"), 600);
+ // Send DHCPINFORM message (with ciaddr not set) to the server.
+ ASSERT_NO_THROW(client.doInform(false));
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Response should have been unicast to the client address.
+ EXPECT_EQ(IOAddress("10.0.0.56"), resp->getLocalAddr());
+ // The ciaddr should be 0.
+ EXPECT_EQ(IOAddress("0.0.0.0"), resp->getCiaddr());
+ // Response must not be relayed.
+ EXPECT_FALSE(resp->isRelayed());
+ EXPECT_EQ(DHCP4_CLIENT_PORT, resp->getLocalPort());
+ EXPECT_EQ(DHCP4_SERVER_PORT, resp->getRemotePort());
+ // Make sure that the server id is present.
+ EXPECT_EQ("10.0.0.1", client.config_.serverid_.toText());
+ // Make sure that the Routers option has been received.
+ ASSERT_EQ(2, client.config_.routers_.size());
+ EXPECT_EQ("10.0.0.200", client.config_.routers_[0].toText());
+ EXPECT_EQ("10.0.0.201", client.config_.routers_[1].toText());
+ // Make sure that the DNS Servers option has been received.
+ ASSERT_EQ(2, client.config_.dns_servers_.size());
+ EXPECT_EQ("10.0.0.202", client.config_.dns_servers_[0].toText());
+ EXPECT_EQ("10.0.0.203", client.config_.dns_servers_[1].toText());
+}
+
+// This test checks that the server receiving DHCPINFORM via relay, unicasts the
+// DHCPACK to the client (ciaddr).
+TEST_F(InformTest, relayedClient) {
+ Dhcp4Client client;
+ // Configure DHCP server.
+ configure(INFORM_CONFIGS[1], *client.getServer());
+ // Message is relayed.
+ client.useRelay();
+ // Request some configuration when DHCPINFORM is sent.
+ client.requestOptions(DHO_LOG_SERVERS, DHO_COOKIE_SERVERS);
+ // Preconfigure the client with the IP address.
+ client.createLease(IOAddress("192.0.2.56"), 600);
+ // Send DHCPINFORM message to the server.
+ ASSERT_NO_THROW(client.doInform());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Response should have been unicast to the ciaddr.
+ EXPECT_EQ(IOAddress("192.0.2.56"), resp->getLocalAddr());
+ // The ciaddr should have been copied.
+ EXPECT_EQ(IOAddress("192.0.2.56"), resp->getCiaddr());
+ // Response is unicast to the client, so it must not be relayed.
+ EXPECT_FALSE(resp->isRelayed());
+ EXPECT_EQ(DHCP4_CLIENT_PORT, resp->getLocalPort());
+ EXPECT_EQ(DHCP4_SERVER_PORT, resp->getRemotePort());
+ // Make sure that the server id is present.
+ EXPECT_EQ("10.0.0.1", client.config_.serverid_.toText());
+ // Make sure that the Routers option has been received.
+ ASSERT_EQ(2, client.config_.routers_.size());
+ EXPECT_EQ("192.0.2.200", client.config_.routers_[0].toText());
+ EXPECT_EQ("192.0.2.201", client.config_.routers_[1].toText());
+ // Make sure that the DNS Servers option has been received.
+ ASSERT_EQ(2, client.config_.dns_servers_.size());
+ EXPECT_EQ("192.0.2.202", client.config_.dns_servers_[0].toText());
+ EXPECT_EQ("192.0.2.203", client.config_.dns_servers_[1].toText());
+ // Make sure that the Quotes Servers option has been received.
+ ASSERT_EQ(2, client.config_.quotes_servers_.size());
+ EXPECT_EQ("10.0.0.202", client.config_.quotes_servers_[0].toText());
+ EXPECT_EQ("10.0.0.203", client.config_.quotes_servers_[1].toText());
+ // Make sure that the Log Servers option has been received.
+ ASSERT_EQ(2, client.config_.log_servers_.size());
+ EXPECT_EQ("10.0.0.200", client.config_.log_servers_[0].toText());
+ EXPECT_EQ("10.0.0.201", client.config_.log_servers_[1].toText());
+}
+
+// This test checks that the server can respond to the DHCPINFORM message
+// received via relay when the ciaddr is not set.
+TEST_F(InformTest, relayedClientNoCiaddr) {
+ Dhcp4Client client;
+ // Configure DHCP server.
+ configure(INFORM_CONFIGS[1], *client.getServer());
+ // Message is relayed.
+ client.useRelay();
+ // Send DHCPINFORM message to the server.
+ ASSERT_NO_THROW(client.doInform());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Response should go through a relay as there is no ciaddr.
+ EXPECT_EQ(IOAddress("192.0.2.2"), resp->getLocalAddr());
+ EXPECT_EQ(IOAddress("192.0.2.2"), resp->getGiaddr());
+ EXPECT_EQ(1, resp->getHops());
+ EXPECT_EQ(DHCP4_SERVER_PORT, resp->getLocalPort());
+ EXPECT_EQ(DHCP4_SERVER_PORT, resp->getRemotePort());
+ // In the case when the client didn't set the ciaddr and the message
+ // was received via relay the server sets the Broadcast flag to help
+ // the relay forwarding the message (without yiaddr) to the client.
+ EXPECT_EQ(BOOTP_BROADCAST, resp->getFlags() & BOOTP_BROADCAST);
+ // Make sure that the server id is present.
+ EXPECT_EQ("10.0.0.1", client.config_.serverid_.toText());
+ // Make sure that the Routers option has been received.
+ ASSERT_EQ(2, client.config_.routers_.size());
+ EXPECT_EQ("192.0.2.200", client.config_.routers_[0].toText());
+ EXPECT_EQ("192.0.2.201", client.config_.routers_[1].toText());
+ // Make sure that the DNS Servers option has been received.
+ ASSERT_EQ(2, client.config_.dns_servers_.size());
+ EXPECT_EQ("192.0.2.202", client.config_.dns_servers_[0].toText());
+ EXPECT_EQ("192.0.2.203", client.config_.dns_servers_[1].toText());
+}
+
+// This test verifies that the server assigns reserved values for the
+// siaddr, sname and file fields carried within DHCPv4 message.
+TEST_F(InformTest, messageFieldsReservations) {
+ // Client has a reservation.
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Message is relayed.
+ client.useRelay();
+ // Set explicit HW address so as it matches the reservation in the
+ // configuration used below.
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+ // Configure DHCP server.
+ configure(INFORM_CONFIGS[2], *client.getServer());
+ // Client sends DHCPINFORM and should receive reserved fields.
+ ASSERT_NO_THROW(client.doInform());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Check that the reserved values have been assigned.
+ EXPECT_EQ("10.0.0.7", client.config_.siaddr_.toText());
+ EXPECT_EQ("some-name.example.org", client.config_.sname_);
+ EXPECT_EQ("bootfile.efi", client.config_.boot_file_name_);
+}
+
+// This test verifies that the server assigns and splits long options within
+// DHCPv4 message.
+TEST_F(InformTest, messageFieldsLongOptions) {
+ // Client has a reservation.
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Message is relayed.
+ client.useRelay();
+ // Set explicit HW address so as it matches the reservation in the
+ // configuration used below.
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+ // Configure DHCP server.
+ configure(INFORM_CONFIGS[3], *client.getServer());
+ // Client requests big option.
+ client.requestOption(240);
+ // Client also sends multiple options with the same code.
+ OptionDefinitionPtr rai_def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE,
+ DHO_DHCP_AGENT_OPTIONS);
+ ASSERT_TRUE(rai_def);
+ // Create RAI options which should be fused by the server.
+ OptionCustomPtr rai(new OptionCustom(*rai_def, Option::V4));
+ for (uint8_t i = 0; i < 4; ++i) {
+ // Create a buffer holding some binary data. This data will be
+ // used as reference when we read back the data from a created
+ // option.
+ OptionBuffer buf_in(16);
+ for (uint8_t j = 0; j < 16; ++j) {
+ buf_in[j] = i * 16 + j;
+ }
+
+ OptionPtr circuit_id_opt(new Option(Option::V4,
+ RAI_OPTION_AGENT_CIRCUIT_ID, buf_in));
+ ASSERT_TRUE(circuit_id_opt);
+ rai->addOption(circuit_id_opt);
+ }
+ client.addExtraOption(rai);
+
+ // Client sends large options which should be split by the client.
+ OptionDefinition opt_def_bar("option-foo", 231, "my-space", "binary",
+ "option-foo-space");
+ // Create a buffer holding some binary data. This data will be
+ // used as reference when we read back the data from a created
+ // option.
+ OptionBuffer buf_in(2560);
+ for (uint32_t i = 0; i < 2560; ++i) {
+ buf_in[i] = i;
+ }
+
+ boost::shared_ptr<OptionCustom> option;
+ ASSERT_NO_THROW(option.reset(new OptionCustom(opt_def_bar, Option::V4, buf_in)));
+ ASSERT_TRUE(option);
+ client.addExtraOption(option);
+ // Client sends DHCPINFORM and should receive reserved fields.
+ ASSERT_NO_THROW(client.doInform());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Long option should have been split by the client on pack, serialized and
+ // then restored.
+ uint32_t count = 0;
+ uint8_t index = 0;
+ for (auto const& option : client.getContext().query_->options_) {
+ if (option.first == 231) {
+ for (auto const& value : option.second->getData()) {
+ ASSERT_EQ(value, index);
+ index++;
+ }
+ count++;
+ }
+ }
+ ASSERT_EQ(1, count);
+
+ count = 0;
+ for (auto const& option : resp->options_) {
+ if (option.first == DHO_DHCP_AGENT_OPTIONS) {
+ for (auto const& suboption: option.second->getOptions()) {
+ if (suboption.first == RAI_OPTION_AGENT_CIRCUIT_ID) {
+ uint8_t index = 0;
+ for (auto const& value : suboption.second->getData()) {
+ ASSERT_EQ(value, index);
+ index++;
+ }
+ count++;
+ }
+ }
+ }
+ }
+ // Multiple options should have been fused by the server on unpack.
+ ASSERT_EQ(count, 1);
+
+ // Check that the reserved and requested values have been assigned.
+ string expected =
+ "-00010203040506070809-00010203040506070809-00010203040506070809-00010203040506070809"
+ "-00010203040506070809-00010203040506070809-00010203040506070809-00010203040506070809"
+ "-00010203040506070809-00010203040506070809-00010203040506070809-00010203040506070809";
+
+ count = 0;
+ string value = "";
+ for (auto const& option : resp->options_) {
+ if (option.second->getType() == 240) {
+ value += string(reinterpret_cast<const char*>(&option.second->getData()[0]),
+ option.second->getData().size());
+ count++;
+ }
+ }
+ // Multiple options should have been fused by the server on unpack.
+ ASSERT_EQ(count, 1);
+ ASSERT_EQ(value, string("data") + expected + string("-data"));
+}
+
+/// This test verifies that after a client completes its INFORM exchange,
+/// appropriate statistics are updated.
+TEST_F(InformTest, statisticsInform) {
+ Dhcp4Client client;
+ // Configure DHCP server.
+ configure(INFORM_CONFIGS[0], *client.getServer());
+ // Request some configuration when DHCPINFORM is sent.
+ client.requestOptions(DHO_LOG_SERVERS, DHO_COOKIE_SERVERS);
+ // Preconfigure the client with the IP address.
+ client.createLease(IOAddress("10.0.0.56"), 600);
+
+ // Send DHCPINFORM message to the server.
+ ASSERT_NO_THROW(client.doInform());
+
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Ok, let's check the statistics.
+ using namespace isc::stats;
+ StatsMgr& mgr = StatsMgr::instance();
+ ObservationPtr pkt4_received = mgr.getObservation("pkt4-received");
+ ObservationPtr pkt4_inform_received = mgr.getObservation("pkt4-inform-received");
+ ObservationPtr pkt4_ack_sent = mgr.getObservation("pkt4-ack-sent");
+ ObservationPtr pkt4_sent = mgr.getObservation("pkt4-sent");
+
+ // All expected statistics must be present.
+ ASSERT_TRUE(pkt4_received);
+ ASSERT_TRUE(pkt4_inform_received);
+ ASSERT_TRUE(pkt4_ack_sent);
+ ASSERT_TRUE(pkt4_sent);
+
+ // And they must have expected values.
+ EXPECT_EQ(1, pkt4_received->getInteger().first);
+ EXPECT_EQ(1, pkt4_inform_received->getInteger().first);
+ EXPECT_EQ(1, pkt4_ack_sent->getInteger().first);
+ EXPECT_EQ(1, pkt4_sent->getInteger().first);
+
+ // Let the client send iform 4 times, which should make the server
+ // to send 4 acks.
+ ASSERT_NO_THROW(client.doInform());
+ ASSERT_NO_THROW(client.doInform());
+ ASSERT_NO_THROW(client.doInform());
+ ASSERT_NO_THROW(client.doInform());
+
+ // Let's see if the stats are properly updated.
+ EXPECT_EQ(5, pkt4_received->getInteger().first);
+ EXPECT_EQ(5, pkt4_inform_received->getInteger().first);
+ EXPECT_EQ(5, pkt4_ack_sent->getInteger().first);
+ EXPECT_EQ(5, pkt4_sent->getInteger().first);
+}
+
+} // end of anonymous namespace
diff --git a/src/bin/dhcp4/tests/kea_controller_unittest.cc b/src/bin/dhcp4/tests/kea_controller_unittest.cc
new file mode 100644
index 0000000..ea5149f
--- /dev/null
+++ b/src/bin/dhcp4/tests/kea_controller_unittest.cc
@@ -0,0 +1,1102 @@
+// Copyright (C) 2014-2023 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <asiolink/interval_timer.h>
+#include <asiolink/io_address.h>
+#include <asiolink/io_service.h>
+#include <cc/command_interpreter.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/hwaddr.h>
+#include <dhcp/iface_mgr.h>
+#include <dhcp4/ctrl_dhcp4_srv.h>
+#include <dhcp4/parser_context.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <dhcpsrv/cb_ctl_dhcp4.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/lease.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <process/config_base.h>
+
+#ifdef HAVE_MYSQL
+#include <mysql/testutils/mysql_schema.h>
+#endif
+
+#include <log/logger_support.h>
+#include <util/stopwatch.h>
+
+#include <boost/pointer_cast.hpp>
+#include <boost/scoped_ptr.hpp>
+#include <gtest/gtest.h>
+
+#include <functional>
+#include <fstream>
+#include <iostream>
+#include <signal.h>
+#include <sstream>
+
+#include <arpa/inet.h>
+#include <unistd.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::config;
+using namespace isc::data;
+
+#ifdef HAVE_MYSQL
+using namespace isc::db::test;
+#endif
+
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::hooks;
+
+namespace {
+
+/// @brief Test implementation of the @c CBControlDHCPv4.
+///
+/// This implementation is installed on the test server instance. It
+/// overrides the implementation of the @c databaseConfigFetch function
+/// to verify arguments passed to this function and throw an exception
+/// when desired in the negative test scenarios. It doesn't do the
+/// actual configuration fetch as this is tested elswhere and would
+/// require setting up a database configuration backend.
+class TestCBControlDHCPv4 : public CBControlDHCPv4 {
+public:
+
+ /// @brief Constructor.
+ TestCBControlDHCPv4()
+ : CBControlDHCPv4(), db_total_config_fetch_calls_(0),
+ db_current_config_fetch_calls_(0), db_staging_config_fetch_calls_(0),
+ enable_check_fetch_mode_(false), enable_throw_(false) {
+ }
+
+ /// @brief Stub implementation of the "fetch" function.
+ ///
+ /// If this is not the first invocation of this function, it
+ /// verifies that the @c fetch_mode has been correctly set to
+ /// @c FetchMode::FETCH_UPDATE.
+ ///
+ /// It also throws an exception when desired by a test, to
+ /// verify that the server gracefully handles such exception.
+ ///
+ /// @param config either the staging or the current configuration.
+ /// @param fetch_mode value indicating if the method is called upon the
+ /// server start up or it is called to fetch configuration updates.
+ ///
+ /// @throw Unexpected when configured to do so.
+ virtual void databaseConfigFetch(const process::ConfigPtr& config,
+ const FetchMode& fetch_mode) {
+ ++db_total_config_fetch_calls_;
+
+ if (config == CfgMgr::instance().getCurrentCfg()) {
+ ++db_current_config_fetch_calls_;
+ } else if (config == CfgMgr::instance().getStagingCfg()) {
+ ++db_staging_config_fetch_calls_;
+ }
+
+ if (enable_check_fetch_mode_) {
+ if ((db_total_config_fetch_calls_ <= 1) &&
+ (fetch_mode == FetchMode::FETCH_UPDATE)) {
+ ADD_FAILURE() << "databaseConfigFetch was called with the value "
+ "of fetch_mode=FetchMode::FETCH_UPDATE upon the server configuration";
+
+ } else if ((db_total_config_fetch_calls_ > 1) &&
+ (fetch_mode == FetchMode::FETCH_ALL)) {
+ ADD_FAILURE() << "databaseConfigFetch was called with the value "
+ "of fetch_mode=FetchMode::FETCH_ALL during fetching the updates";
+ }
+ }
+
+ if (enable_throw_) {
+ isc_throw(Unexpected, "testing if exceptions are correctly handled");
+ }
+ }
+
+ /// @brief Returns number of invocations of the @c databaseConfigFetch
+ /// (total).
+ size_t getDatabaseTotalConfigFetchCalls() const {
+ return (db_total_config_fetch_calls_);
+ }
+
+ /// @brief Returns number of invocations of the @c databaseConfigFetch
+ /// (current configuration).
+ size_t getDatabaseCurrentConfigFetchCalls() const {
+ return (db_current_config_fetch_calls_);
+ }
+
+ /// @brief Returns number of invocations of the @c databaseConfigFetch
+ /// (staging configuration).
+ size_t getDatabaseStagingConfigFetchCalls() const {
+ return (db_staging_config_fetch_calls_);
+ }
+
+ /// @brief Enables checking of the @c fetch_mode value.
+ void enableCheckFetchMode() {
+ enable_check_fetch_mode_ = true;
+ }
+
+ /// @brief Enables the object to throw from @c databaseConfigFetch.
+ void enableThrow() {
+ enable_throw_ = true;
+ }
+
+private:
+
+ /// @brief Counter holding number of invocations of the
+ /// @c databaseConfigFetch (total).
+ size_t db_total_config_fetch_calls_;
+
+ /// @brief Counter holding number of invocations of the
+ /// @c databaseConfigFetch (current configuration).
+ size_t db_current_config_fetch_calls_;
+
+ /// @brief Counter holding number of invocations of the
+ /// @c databaseConfigFetch (staging configuration).
+ size_t db_staging_config_fetch_calls_;
+
+ /// @brief Boolean flag indicated if the value of the @c fetch_mode
+ /// should be verified.
+ bool enable_check_fetch_mode_;
+
+ /// @brief Boolean flag indicating if the @c databaseConfigFetch should
+ /// throw.
+ bool enable_throw_;
+};
+
+/// @brief Shared pointer to the @c TestCBControlDHCPv4.
+typedef boost::shared_ptr<TestCBControlDHCPv4> TestCBControlDHCPv4Ptr;
+
+/// @brief "Naked" DHCPv4 server.
+///
+/// Exposes internal fields and installs stub implementation of the
+/// @c CBControlDHCPv4 object.
+class NakedControlledDhcpv4Srv: public ControlledDhcpv4Srv {
+public:
+
+ /// @brief Constructor.
+ NakedControlledDhcpv4Srv()
+ : ControlledDhcpv4Srv(0) {
+ // We're replacing the @c CBControlDHCPv4 instance with our
+ // stub implementation used in tests.
+ cb_control_.reset(new TestCBControlDHCPv4());
+ }
+};
+
+/// @brief test class for Kea configuration backend
+///
+/// This class is used for testing Kea configuration backend.
+/// It is very simple and currently focuses on reading
+/// config file from disk. It is expected to be expanded in the
+/// near future.
+class JSONFileBackendTest : public isc::dhcp::test::BaseServerTest {
+public:
+ JSONFileBackendTest() {
+ }
+
+ ~JSONFileBackendTest() {
+ LeaseMgrFactory::destroy();
+ static_cast<void>(remove(TEST_FILE));
+ static_cast<void>(remove(TEST_INCLUDE));
+ };
+
+ /// @brief writes specified content to a well known file
+ ///
+ /// Writes specified content to TEST_FILE. Tests will
+ /// attempt to read that file.
+ ///
+ /// @param file_name name of file to be written
+ /// @param content content to be written to file
+ void writeFile(const std::string& file_name, const std::string& content) {
+ static_cast<void>(remove(file_name.c_str()));
+
+ ofstream out(file_name.c_str(), ios::trunc);
+ EXPECT_TRUE(out.is_open());
+ out << content;
+ out.close();
+ }
+
+ /// @brief Runs timers for specified time.
+ ///
+ /// @param io_service Pointer to the IO service to be ran.
+ /// @param timeout_ms Amount of time after which the method returns.
+ /// @param cond Pointer to the function which if returns true it
+ /// stops the IO service and causes the function to return.
+ void runTimersWithTimeout(const IOServicePtr& io_service, const long timeout_ms,
+ std::function<bool()> cond = std::function<bool()>()) {
+ IntervalTimer timer(*io_service);
+ std::atomic<bool> stopped(false);
+ timer.setup([&io_service, &stopped]() {
+ stopped = true;
+ io_service->stop();
+ }, timeout_ms, IntervalTimer::ONE_SHOT);
+
+ // Run as long as the timeout hasn't occurred and the interrupting
+ // condition is not specified or not met.
+ while (!stopped && (!cond || !cond())) {
+ io_service->run_one();
+ }
+ io_service->get_io_service().reset();
+ }
+
+ /// @brief This test verifies that the timer used to fetch the configuration
+ /// updates from the database works as expected.
+ void testConfigBackendTimer(const int config_wait_fetch_time,
+ const bool throw_during_fetch = false,
+ const bool call_command = false) {
+ std::ostringstream config;
+ config <<
+ "{ \"Dhcp4\": {"
+ "\"interfaces-config\": {"
+ " \"interfaces\": [ ]"
+ "},"
+ "\"lease-database\": {"
+ " \"type\": \"memfile\","
+ " \"persist\": false"
+ "},"
+ "\"config-control\": {"
+ " \"config-fetch-wait-time\": " << config_wait_fetch_time <<
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, \n"
+ "\"subnet4\": [ ],"
+ "\"valid-lifetime\": 4000 }"
+ "}";
+ writeFile(TEST_FILE, config.str());
+
+ // Create an instance of the server and initialize it.
+ boost::scoped_ptr<NakedControlledDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new NakedControlledDhcpv4Srv()));
+ ASSERT_NO_THROW(srv->init(TEST_FILE));
+
+ // Get the CBControlDHCPv4 object belonging to this server.
+ auto cb_control = boost::dynamic_pointer_cast<TestCBControlDHCPv4>(srv->getCBControl());
+
+ // Verify that the parameter passed to the databaseConfigFetch has an
+ // expected value.
+ cb_control->enableCheckFetchMode();
+
+ // Instruct our stub implementation of the CBControlDHCPv4 to throw as a
+ // result of fetch if desired.
+ if (throw_during_fetch) {
+ cb_control->enableThrow();
+ }
+
+ // So far there should be exactly one attempt to fetch the configuration
+ // from the backend. That's the attempt made upon startup on
+ // the staging configuration.
+ // All other fetches will be on the current configuration:
+ // - the timer makes a closure with the staging one but it is
+ // committed so becomes the current one.
+ // - the command is called outside configuration so it must
+ // be the current configuration. The test explicitly checks this.
+ EXPECT_EQ(1, cb_control->getDatabaseTotalConfigFetchCalls());
+ EXPECT_EQ(0, cb_control->getDatabaseCurrentConfigFetchCalls());
+ EXPECT_EQ(1, cb_control->getDatabaseStagingConfigFetchCalls());
+
+
+ if (call_command) {
+ // The case where there is no backend is tested in the
+ // controlled server tests so we have only to verify
+ // that the command calls the database config fetch.
+
+ // Count the startup.
+ EXPECT_EQ(cb_control->getDatabaseTotalConfigFetchCalls(), 1);
+ EXPECT_EQ(cb_control->getDatabaseCurrentConfigFetchCalls(), 0);
+ EXPECT_EQ(cb_control->getDatabaseStagingConfigFetchCalls(), 1);
+
+ ConstElementPtr result =
+ ControlledDhcpv4Srv::processCommand("config-backend-pull",
+ ConstElementPtr());
+ EXPECT_EQ(cb_control->getDatabaseTotalConfigFetchCalls(), 2);
+ std::string expected;
+
+ if (throw_during_fetch) {
+ expected = "{ \"result\": 1, \"text\": ";
+ expected += "\"On demand configuration update failed: ";
+ expected += "testing if exceptions are correctly handled\" }";
+ } else {
+ expected = "{ \"result\": 0, \"text\": ";
+ expected += "\"On demand configuration update successful.\" }";
+ }
+ EXPECT_EQ(expected, result->str());
+
+ // No good way to check the rescheduling...
+ ASSERT_NO_THROW(runTimersWithTimeout(srv->getIOService(), 20));
+
+ if (config_wait_fetch_time > 0) {
+ EXPECT_GE(cb_control->getDatabaseTotalConfigFetchCalls(), 5);
+ EXPECT_GE(cb_control->getDatabaseCurrentConfigFetchCalls(), 4);
+ EXPECT_EQ(cb_control->getDatabaseStagingConfigFetchCalls(), 1);
+ } else {
+ EXPECT_EQ(cb_control->getDatabaseTotalConfigFetchCalls(), 2);
+ EXPECT_EQ(cb_control->getDatabaseCurrentConfigFetchCalls(), 1);
+ EXPECT_EQ(cb_control->getDatabaseStagingConfigFetchCalls(), 1);
+ }
+
+ } else if ((config_wait_fetch_time > 0) && (!throw_during_fetch)) {
+ // If we're configured to run the timer, we expect that it was
+ // invoked at least 3 times. This is sufficient to verify that
+ // the timer was scheduled and that the timer continued to run
+ // even when an exception occurred during fetch (that's why it
+ // is 3 not 2).
+ ASSERT_NO_THROW(runTimersWithTimeout(srv->getIOService(), 500,
+ [cb_control]() {
+ // Interrupt the timers poll if we have recorded at
+ // least 3 attempts to fetch the updates.
+ return (cb_control->getDatabaseTotalConfigFetchCalls() >= 3);
+ }));
+ EXPECT_GE(cb_control->getDatabaseTotalConfigFetchCalls(), 3);
+ EXPECT_GE(cb_control->getDatabaseCurrentConfigFetchCalls(), 2);
+ EXPECT_EQ(cb_control->getDatabaseStagingConfigFetchCalls(), 1);
+
+ } else {
+ ASSERT_NO_THROW(runTimersWithTimeout(srv->getIOService(), 500));
+
+ if (throw_during_fetch) {
+ // If we're simulating the failure condition the number
+ // of consecutive failures should not exceed 10. Therefore
+ // the number of recorded fetches should be 12. One at
+ // startup, 10 failures and one that causes the timer
+ // to stop.
+ EXPECT_EQ(12, cb_control->getDatabaseTotalConfigFetchCalls());
+ EXPECT_EQ(11, cb_control->getDatabaseCurrentConfigFetchCalls());
+ EXPECT_EQ(1, cb_control->getDatabaseStagingConfigFetchCalls());
+
+ } else {
+ // If the server is not configured to schedule the timer,
+ // we should still have one fetch attempt recorded.
+ EXPECT_EQ(1, cb_control->getDatabaseTotalConfigFetchCalls());
+ EXPECT_EQ(0, cb_control->getDatabaseCurrentConfigFetchCalls());
+ EXPECT_EQ(1, cb_control->getDatabaseStagingConfigFetchCalls());
+ }
+ }
+ }
+
+ /// Name of a config file used during tests
+ static const char* TEST_FILE;
+ static const char* TEST_INCLUDE;
+};
+
+const char* JSONFileBackendTest::TEST_FILE = "test-config.json";
+const char* JSONFileBackendTest::TEST_INCLUDE = "test-include.json";
+
+// This test checks if configuration can be read from a JSON file.
+TEST_F(JSONFileBackendTest, jsonFile) {
+
+ // Prepare configuration file.
+ string config = "{ \"Dhcp4\": {"
+ "\"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/24\" "
+ " },"
+ " {"
+ " \"pools\": [ { \"pool\": \"192.0.3.101 - 192.0.3.150\" } ],"
+ " \"subnet\": \"192.0.3.0/24\", "
+ " \"id\": 2 "
+ " },"
+ " {"
+ " \"pools\": [ { \"pool\": \"192.0.4.101 - 192.0.4.150\" } ],"
+ " \"id\": 3, "
+ " \"subnet\": \"192.0.4.0/24\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }"
+ "}";
+
+ writeFile(TEST_FILE, config);
+
+ // Now initialize the server
+ boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+ ASSERT_NO_THROW(
+ srv.reset(new ControlledDhcpv4Srv(0))
+ );
+
+ // And configure it using the config file.
+ EXPECT_NO_THROW(srv->init(TEST_FILE));
+
+ // Now check if the configuration has been applied correctly.
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(3, subnets->size()); // We expect 3 subnets.
+
+ // Check subnet 1.
+ auto subnet = subnets->begin();
+ ASSERT_TRUE(subnet != subnets->end());
+ EXPECT_EQ("192.0.2.0", (*subnet)->get().first.toText());
+ EXPECT_EQ(24, (*subnet)->get().second);
+
+ // Check pools in the first subnet.
+ const PoolCollection& pools1 = (*subnet)->getPools(Lease::TYPE_V4);
+ ASSERT_EQ(1, pools1.size());
+ EXPECT_EQ("192.0.2.1", pools1.at(0)->getFirstAddress().toText());
+ EXPECT_EQ("192.0.2.100", pools1.at(0)->getLastAddress().toText());
+ EXPECT_EQ(Lease::TYPE_V4, pools1.at(0)->getType());
+
+ // Check subnet 2.
+ ++subnet;
+ ASSERT_TRUE(subnet != subnets->end());
+ EXPECT_EQ("192.0.3.0", (*subnet)->get().first.toText());
+ EXPECT_EQ(24, (*subnet)->get().second);
+
+ // Check pools in the second subnet.
+ const PoolCollection& pools2 = (*subnet)->getPools(Lease::TYPE_V4);
+ ASSERT_EQ(1, pools2.size());
+ EXPECT_EQ("192.0.3.101", pools2.at(0)->getFirstAddress().toText());
+ EXPECT_EQ("192.0.3.150", pools2.at(0)->getLastAddress().toText());
+ EXPECT_EQ(Lease::TYPE_V4, pools2.at(0)->getType());
+
+ // And finally check subnet 3.
+ ++subnet;
+ ASSERT_TRUE(subnet != subnets->end());
+ EXPECT_EQ("192.0.4.0", (*subnet)->get().first.toText());
+ EXPECT_EQ(24, (*subnet)->get().second);
+
+ // ... and its only pool.
+ const PoolCollection& pools3 = (*subnet)->getPools(Lease::TYPE_V4);
+ EXPECT_EQ("192.0.4.101", pools3.at(0)->getFirstAddress().toText());
+ EXPECT_EQ("192.0.4.150", pools3.at(0)->getLastAddress().toText());
+ EXPECT_EQ(Lease::TYPE_V4, pools3.at(0)->getType());
+}
+
+// This test checks if configuration can be read from a JSON file
+// using hash (#) line comments
+TEST_F(JSONFileBackendTest, hashComments) {
+
+ string config_hash_comments = "# This is a comment. It should be \n"
+ "#ignored. Real config starts in line below\n"
+ "{ \"Dhcp4\": {"
+ "\"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, \n"
+ "# comments in the middle should be ignored, too\n"
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.0/24\" } ],"
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/22\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }"
+ "}";
+
+ writeFile(TEST_FILE, config_hash_comments);
+
+ // Now initialize the server
+ boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+ ASSERT_NO_THROW(
+ srv.reset(new ControlledDhcpv4Srv(0))
+ );
+
+ // And configure it using config with comments.
+ EXPECT_NO_THROW(srv->init(TEST_FILE));
+
+ // Now check if the configuration has been applied correctly.
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(1, subnets->size());
+
+ // Check subnet 1.
+ auto subnet = subnets->begin();
+ ASSERT_TRUE(subnet != subnets->end());
+ EXPECT_EQ("192.0.2.0", (*subnet)->get().first.toText());
+ EXPECT_EQ(22, (*subnet)->get().second);
+
+ // Check pools in the first subnet.
+ const PoolCollection& pools1 = (*subnet)->getPools(Lease::TYPE_V4);
+ ASSERT_EQ(1, pools1.size());
+ EXPECT_EQ("192.0.2.0", pools1.at(0)->getFirstAddress().toText());
+ EXPECT_EQ("192.0.2.255", pools1.at(0)->getLastAddress().toText());
+ EXPECT_EQ(Lease::TYPE_V4, pools1.at(0)->getType());
+}
+
+// This test checks if configuration can be read from a JSON file
+// using C++ line (//) comments.
+TEST_F(JSONFileBackendTest, cppLineComments) {
+
+ string config_cpp_line_comments = "// This is a comment. It should be \n"
+ "//ignored. Real config starts in line below\n"
+ "{ \"Dhcp4\": {"
+ "\"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, \n"
+ "// comments in the middle should be ignored, too\n"
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.0/24\" } ],"
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/22\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }"
+ "}";
+
+ writeFile(TEST_FILE, config_cpp_line_comments);
+
+ // Now initialize the server
+ boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+ ASSERT_NO_THROW(
+ srv.reset(new ControlledDhcpv4Srv(0))
+ );
+
+ // And configure it using config with comments.
+ EXPECT_NO_THROW(srv->init(TEST_FILE));
+
+ // Now check if the configuration has been applied correctly.
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(1, subnets->size());
+
+ // Check subnet 1.
+ auto subnet = subnets->begin();
+ ASSERT_TRUE(subnet != subnets->end());
+ EXPECT_EQ("192.0.2.0", (*subnet)->get().first.toText());
+ EXPECT_EQ(22, (*subnet)->get().second);
+
+ // Check pools in the first subnet.
+ const PoolCollection& pools1 = (*subnet)->getPools(Lease::TYPE_V4);
+ ASSERT_EQ(1, pools1.size());
+ EXPECT_EQ("192.0.2.0", pools1.at(0)->getFirstAddress().toText());
+ EXPECT_EQ("192.0.2.255", pools1.at(0)->getLastAddress().toText());
+ EXPECT_EQ(Lease::TYPE_V4, pools1.at(0)->getType());
+}
+
+// This test checks if configuration can be read from a JSON file
+// using C block (/* */) comments
+TEST_F(JSONFileBackendTest, cBlockComments) {
+
+ string config_c_block_comments = "/* This is a comment. It should be \n"
+ "ignored. Real config starts in line below*/\n"
+ "{ \"Dhcp4\": {"
+ "\"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, \n"
+ "/* comments in the middle should be ignored, too*/\n"
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.0/24\" } ],"
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/22\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }"
+ "}";
+
+ writeFile(TEST_FILE, config_c_block_comments);
+
+ // Now initialize the server
+ boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+ ASSERT_NO_THROW(
+ srv.reset(new ControlledDhcpv4Srv(0))
+ );
+
+ // And configure it using config with comments.
+ EXPECT_NO_THROW(srv->init(TEST_FILE));
+
+ // Now check if the configuration has been applied correctly.
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(1, subnets->size());
+
+ // Check subnet 1.
+ auto subnet = subnets->begin();
+ ASSERT_TRUE(subnet != subnets->end());
+ EXPECT_EQ("192.0.2.0", (*subnet)->get().first.toText());
+ EXPECT_EQ(22, (*subnet)->get().second);
+
+ // Check pools in the first subnet.
+ const PoolCollection& pools1 = (*subnet)->getPools(Lease::TYPE_V4);
+ ASSERT_EQ(1, pools1.size());
+ EXPECT_EQ("192.0.2.0", pools1.at(0)->getFirstAddress().toText());
+ EXPECT_EQ("192.0.2.255", pools1.at(0)->getLastAddress().toText());
+ EXPECT_EQ(Lease::TYPE_V4, pools1.at(0)->getType());
+}
+
+// This test checks if configuration can be read from a JSON file
+// using an include file.
+TEST_F(JSONFileBackendTest, include) {
+
+ string config_hash_comments = "{ \"Dhcp4\": {"
+ "\"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, \n"
+ "<?include \"" + string(TEST_INCLUDE) + "\"?>,"
+ "\"valid-lifetime\": 4000 }"
+ "}";
+ string include = "\n"
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.0/24\" } ],"
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/22\" "
+ " } ]\n";
+
+ writeFile(TEST_FILE, config_hash_comments);
+ writeFile(TEST_INCLUDE, include);
+
+ // Now initialize the server
+ boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+ ASSERT_NO_THROW(
+ srv.reset(new ControlledDhcpv4Srv(0))
+ );
+
+ // And configure it using config with comments.
+ EXPECT_NO_THROW(srv->init(TEST_FILE));
+
+ // Now check if the configuration has been applied correctly.
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(1, subnets->size());
+
+ // Check subnet 1.
+ auto subnet = subnets->begin();
+ ASSERT_TRUE(subnet != subnets->end());
+ EXPECT_EQ("192.0.2.0", (*subnet)->get().first.toText());
+ EXPECT_EQ(22, (*subnet)->get().second);
+
+ // Check pools in the first subnet.
+ const PoolCollection& pools1 = (*subnet)->getPools(Lease::TYPE_V4);
+ ASSERT_EQ(1, pools1.size());
+ EXPECT_EQ("192.0.2.0", pools1.at(0)->getFirstAddress().toText());
+ EXPECT_EQ("192.0.2.255", pools1.at(0)->getLastAddress().toText());
+ EXPECT_EQ(Lease::TYPE_V4, pools1.at(0)->getType());
+}
+
+// This test checks if recursive include of a file is detected
+TEST_F(JSONFileBackendTest, recursiveInclude) {
+
+ string config_recursive_include = "{ \"Dhcp4\": {"
+ "\"interfaces-config\": {"
+ " \"interfaces\": [ <?include \"" + string(TEST_INCLUDE) + "\"?> ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, \n"
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.0/24\" } ],"
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/22\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }"
+ "}";
+ string include = "\"eth\", <?include \"" + string(TEST_INCLUDE) + "\"?>";
+ string msg = "configuration error using file '" + string(TEST_FILE) +
+ "': Too many nested include.";
+
+ writeFile(TEST_FILE, config_recursive_include);
+ writeFile(TEST_INCLUDE, include);
+
+ // Now initialize the server
+ boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+ ASSERT_NO_THROW(
+ srv.reset(new ControlledDhcpv4Srv(0))
+ );
+
+ // And configure it using config with comments.
+ try {
+ srv->init(TEST_FILE);
+ FAIL() << "Expected Dhcp4ParseError but nothing was raised";
+ }
+ catch (const Exception& ex) {
+ EXPECT_EQ(msg, ex.what());
+ }
+}
+
+// This test checks if configuration detects failure when trying:
+// - empty file
+// - empty filename
+// - no Dhcp4 element
+// - Config file that contains Dhcp4 but has a content error
+TEST_F(JSONFileBackendTest, configBroken) {
+
+ // Empty config is not allowed, because Dhcp4 element is missing
+ string config_empty = "";
+
+ // This config does not have mandatory Dhcp4 element
+ string config_v6 = "{ \"Dhcp6\": { \"interfaces\": [ \"*\" ],"
+ "\"preferred-lifetime\": 3000,"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"2001:db8::/80\" ],"
+ " \"id\": 1, "
+ " \"subnet\": \"2001:db8::/64\" "
+ " } ]}";
+
+ // This has Dhcp4 element, but it's utter nonsense
+ string config_nonsense = "{ \"Dhcp4\": { \"reviews\": \"are so much fun\" } }";
+
+ // Now initialize the server
+ boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+ ASSERT_NO_THROW(
+ srv.reset(new ControlledDhcpv4Srv(0))
+ );
+
+ // Try to configure without filename. Should fail.
+ EXPECT_THROW(srv->init(""), BadValue);
+
+ // Try to configure it using empty file. Should fail.
+ writeFile(TEST_FILE, config_empty);
+ EXPECT_THROW(srv->init(TEST_FILE), BadValue);
+
+ // Now try to load a config that does not have Dhcp4 component.
+ writeFile(TEST_FILE, config_v6);
+ EXPECT_THROW(srv->init(TEST_FILE), BadValue);
+
+ // Now try to load a config with Dhcp4 full of nonsense.
+ writeFile(TEST_FILE, config_nonsense);
+ EXPECT_THROW(srv->init(TEST_FILE), BadValue);
+}
+
+/// This unit-test reads all files enumerated in configs-test.txt file, loads
+/// each of them and verify that they can be loaded.
+///
+/// @todo: Unfortunately, we have this test disabled, because all loaded
+/// configs use memfile, which attempts to create lease file in
+/// /usr/local/var/lib/kea/kea-leases4.csv. We have couple options here:
+/// a) disable persistence in example configs - a very bad thing to do
+/// as users will forget to reenable it and then will be surprised when their
+/// leases disappear
+/// b) change configs to store lease file in /tmp. It's almost as bad as the
+/// previous one. Users will then be displeased when all their leases are
+/// wiped. (most systems wipe /tmp during boot)
+/// c) read each config and rewrite it on the fly, so persistence is disabled.
+/// This is probably the way to go, but this is a work for a dedicated ticket.
+///
+/// Hence I'm leaving the test in, but it is disabled.
+TEST_F(JSONFileBackendTest, DISABLED_loadAllConfigs) {
+
+ // Create server first
+ boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+ ASSERT_NO_THROW(
+ srv.reset(new ControlledDhcpv4Srv(0))
+ );
+
+ const char* configs_list = "configs-list.txt";
+ fstream configs(configs_list, ios::in);
+ ASSERT_TRUE(configs.is_open());
+ std::string config_name;
+ while (std::getline(configs, config_name)) {
+
+ // Ignore empty and commented lines
+ if (config_name.empty() || config_name[0] == '#') {
+ continue;
+ }
+
+ // Unit-tests usually do not print out anything, but in this case I
+ // think printing out tests configs is warranted.
+ std::cout << "Loading config file " << config_name << std::endl;
+
+ try {
+ srv->init(config_name);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "Exception thrown" << ex.what() << endl;
+ }
+ }
+}
+
+// This test verifies that the DHCP server installs the timers for reclaiming
+// and flushing expired leases.
+TEST_F(JSONFileBackendTest, timers) {
+ // This is a basic configuration which enables timers for reclaiming
+ // expired leases and flushing them after 500 seconds since they expire.
+ // Both timers run at 1 second intervals.
+ string config =
+ "{ \"Dhcp4\": {"
+ "\"interfaces-config\": {"
+ " \"interfaces\": [ ]"
+ "},"
+ "\"lease-database\": {"
+ " \"type\": \"memfile\","
+ " \"persist\": false"
+ "},"
+ "\"expired-leases-processing\": {"
+ " \"reclaim-timer-wait-time\": 1,"
+ " \"hold-reclaimed-time\": 500,"
+ " \"flush-reclaimed-timer-wait-time\": 1"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, \n"
+ "\"subnet4\": [ ],"
+ "\"valid-lifetime\": 4000 }"
+ "}";
+ writeFile(TEST_FILE, config);
+
+ // Create an instance of the server and initialize it.
+ boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new ControlledDhcpv4Srv(0)));
+ ASSERT_NO_THROW(srv->init(TEST_FILE));
+
+ // Create an expired lease. The lease is expired by 40 seconds ago
+ // (valid lifetime = 60, cltt = now - 100). The lease will be reclaimed
+ // but shouldn't be flushed in the database because the reclaimed are
+ // held in the database 500 seconds after reclamation, according to the
+ // current configuration.
+ HWAddrPtr hwaddr_expired(new HWAddr(HWAddr::fromText("00:01:02:03:04:05")));
+ Lease4Ptr lease_expired(new Lease4(IOAddress("10.0.0.1"), hwaddr_expired,
+ ClientIdPtr(), 60,
+ time(NULL) - 100, SubnetID(1)));
+
+ // Create expired-reclaimed lease. The lease has expired 1000 - 60 seconds
+ // ago. It should be removed from the lease database when the "flush" timer
+ // goes off.
+ HWAddrPtr hwaddr_reclaimed(new HWAddr(HWAddr::fromText("01:02:03:04:05:06")));
+ Lease4Ptr lease_reclaimed(new Lease4(IOAddress("10.0.0.2"), hwaddr_reclaimed,
+ ClientIdPtr(), 60,
+ time(NULL) - 1000, SubnetID(1)));
+ lease_reclaimed->state_ = Lease4::STATE_EXPIRED_RECLAIMED;
+
+ // Add leases to the database.
+ LeaseMgr& lease_mgr = LeaseMgrFactory::instance();
+ ASSERT_NO_THROW(lease_mgr.addLease(lease_expired));
+ ASSERT_NO_THROW(lease_mgr.addLease(lease_reclaimed));
+
+ // Make sure they have been added.
+ ASSERT_TRUE(lease_mgr.getLease4(IOAddress("10.0.0.1")));
+ ASSERT_TRUE(lease_mgr.getLease4(IOAddress("10.0.0.2")));
+
+ // Poll the timers for a while to make sure that each of them is executed
+ // at least once.
+ ASSERT_NO_THROW(runTimersWithTimeout(srv->getIOService(), 5000));
+
+ // Verify that the leases in the database have been processed as expected.
+
+ // First lease should be reclaimed, but not removed.
+ ASSERT_NO_THROW(lease_expired = lease_mgr.getLease4(IOAddress("10.0.0.1")));
+ ASSERT_TRUE(lease_expired);
+ EXPECT_TRUE(lease_expired->stateExpiredReclaimed());
+
+ // Second lease should have been removed.
+ ASSERT_NO_THROW(
+ lease_reclaimed = lease_mgr.getLease4(IOAddress("10.0.0.2"));
+ );
+ EXPECT_FALSE(lease_reclaimed);
+}
+
+// This test verifies that the server uses default (Memfile) lease database
+// backend when no backend is explicitly specified in the configuration.
+TEST_F(JSONFileBackendTest, defaultLeaseDbBackend) {
+ // This is basic server configuration which excludes lease database
+ // backend specification. The default Memfile backend should be
+ // initialized in this case.
+ string config =
+ "{ \"Dhcp4\": {"
+ "\"interfaces-config\": {"
+ " \"interfaces\": [ ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, \n"
+ "\"subnet4\": [ ],"
+ "\"valid-lifetime\": 4000 }"
+ "}";
+ writeFile(TEST_FILE, config);
+
+ // Create an instance of the server and initialize it.
+ boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new ControlledDhcpv4Srv(0)));
+ ASSERT_NO_THROW(srv->init(TEST_FILE));
+
+ // The backend should have been created.
+ EXPECT_NO_THROW(static_cast<void>(LeaseMgrFactory::instance()));
+}
+
+// This test verifies that the timer triggering configuration updates
+// is invoked according to the configured value of the
+// config-fetch-wait-time.
+TEST_F(JSONFileBackendTest, configBackendTimer) {
+ testConfigBackendTimer(1);
+}
+
+// This test verifies that the timer for triggering configuration updates
+// is not invoked when the value of the config-fetch-wait-time is set
+// to 0.
+TEST_F(JSONFileBackendTest, configBackendTimerDisabled) {
+ testConfigBackendTimer(0);
+}
+
+// This test verifies that the server will gracefully handle exceptions
+// thrown from the CBControlDHCPv4::databaseConfigFetch, i.e. will
+// reschedule the timer.
+TEST_F(JSONFileBackendTest, configBackendTimerWithThrow) {
+ // The true value instructs the test to throw during the fetch.
+ testConfigBackendTimer(1, true);
+}
+
+// This test verifies that the server will be updated by the
+// config-backend-pull command.
+TEST_F(JSONFileBackendTest, configBackendPullCommand) {
+ testConfigBackendTimer(0, false, true);
+}
+
+// This test verifies that the server will be updated by the
+// config-backend-pull command even when updates fail.
+TEST_F(JSONFileBackendTest, configBackendPullCommandWithThrow) {
+ testConfigBackendTimer(0, true, true);
+}
+
+// This test verifies that the server will be updated by the
+// config-backend-pull command and the timer rescheduled.
+TEST_F(JSONFileBackendTest, configBackendPullCommandWithTimer) {
+ testConfigBackendTimer(1, false, true);
+}
+
+// Starting tests which require MySQL backend availability. Those tests
+// will not be executed if Kea has been compiled without the
+// --with-mysql.
+#ifdef HAVE_MYSQL
+
+/// @brief Test fixture class for the tests utilizing MySQL database
+/// backend.
+class JSONFileBackendMySQLTest : public JSONFileBackendTest {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Recreates MySQL schema for a test.
+ JSONFileBackendMySQLTest() : JSONFileBackendTest() {
+ // Ensure we have the proper schema with no transient data.
+ createMySQLSchema();
+ }
+
+ /// @brief Destructor.
+ ///
+ /// Destroys MySQL schema.
+ virtual ~JSONFileBackendMySQLTest() {
+ // If data wipe enabled, delete transient data otherwise destroy the schema.
+ destroyMySQLSchema();
+ }
+
+ /// @brief Creates server configuration with specified backend type.
+ ///
+ /// @param backend Backend type or empty string to indicate that the
+ /// backend configuration should not be placed in the resulting
+ /// JSON configuration.
+ ///
+ /// @return Server configuration.
+ std::string createConfiguration(const std::string& backend) const;
+
+ /// @brief Test reconfiguration with a backend change.
+ ///
+ /// If any of the parameters is an empty string it indicates that the
+ /// created configuration should exclude backend configuration.
+ ///
+ /// @param backend_first Type of a backend to be used initially.
+ /// @param backend_second Type of a backend to be used after
+ /// reconfiguration.
+ void testBackendReconfiguration(const std::string& backend_first,
+ const std::string& backend_second);
+};
+
+std::string
+JSONFileBackendMySQLTest::createConfiguration(const std::string& backend) const {
+ // This is basic server configuration which excludes lease database
+ // backend specification. The default Memfile backend should be
+ // initialized in this case.
+ std::ostringstream config;
+ config <<
+ "{ \"Dhcp4\": {"
+ "\"interfaces-config\": {"
+ " \"interfaces\": [ ]"
+ "},";
+
+ // For non-empty lease backend type we have to add a backend configuration
+ // section.
+ if (!backend.empty()) {
+ config <<
+ "\"lease-database\": {"
+ " \"type\": \"" << backend << "\"";
+
+ // SQL backends require database credentials.
+ if (backend != "memfile") {
+ config <<
+ ","
+ " \"name\": \"keatest\","
+ " \"user\": \"keatest\","
+ " \"password\": \"keatest\"";
+ }
+ config << "},";
+ }
+
+ // Append the rest of the configuration.
+ config <<
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, \n"
+ "\"subnet4\": [ ],"
+ "\"valid-lifetime\": 4000 }"
+ "}";
+
+ return (config.str());
+}
+
+void
+JSONFileBackendMySQLTest::
+testBackendReconfiguration(const std::string& backend_first,
+ const std::string& backend_second) {
+ writeFile(TEST_FILE, createConfiguration(backend_first));
+
+ // Create an instance of the server and initialize it.
+ boost::scoped_ptr<NakedControlledDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new NakedControlledDhcpv4Srv()));
+ srv->setConfigFile(TEST_FILE);
+ ASSERT_NO_THROW(srv->init(TEST_FILE));
+
+ // The backend should have been created and its type should be
+ // correct.
+ ASSERT_NO_THROW(static_cast<void>(LeaseMgrFactory::instance()));
+ EXPECT_EQ(backend_first.empty() ? "memfile" : backend_first,
+ LeaseMgrFactory::instance().getType());
+
+ // New configuration modifies the lease database backend type.
+ writeFile(TEST_FILE, createConfiguration(backend_second));
+
+ // Explicitly calling signal handler for SIGHUP to trigger server
+ // reconfiguration.
+ raise(SIGHUP);
+
+ // Polling once to be sure that the signal handle has been called.
+ srv->getIOService()->poll();
+
+ // The backend should have been created and its type should be
+ // correct.
+ ASSERT_NO_THROW(static_cast<void>(LeaseMgrFactory::instance()));
+ EXPECT_EQ(backend_second.empty() ? "memfile" : backend_second,
+ LeaseMgrFactory::instance().getType());
+}
+
+
+// This test verifies that backend specification can be added on
+// server reconfiguration.
+TEST_F(JSONFileBackendMySQLTest, reconfigureBackendUndefinedToMySQL) {
+ testBackendReconfiguration("", "mysql");
+}
+
+// This test verifies that when backend specification is removed the
+// default backend is used.
+TEST_F(JSONFileBackendMySQLTest, reconfigureBackendMySQLToUndefined) {
+ testBackendReconfiguration("mysql", "");
+}
+
+// This test verifies that backend type can be changed from Memfile
+// to MySQL.
+TEST_F(JSONFileBackendMySQLTest, reconfigureBackendMemfileToMySQL) {
+ testBackendReconfiguration("memfile", "mysql");
+}
+
+#endif
+
+} // End of anonymous namespace
diff --git a/src/bin/dhcp4/tests/marker_file.cc b/src/bin/dhcp4/tests/marker_file.cc
new file mode 100644
index 0000000..066e1f7
--- /dev/null
+++ b/src/bin/dhcp4/tests/marker_file.cc
@@ -0,0 +1,60 @@
+// Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include "marker_file.h"
+
+#include <gtest/gtest.h>
+
+#include <fstream>
+#include <string>
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+using namespace std;
+
+// Check the marker file.
+
+bool
+checkMarkerFile(const char* name, const char* expected) {
+ // Open the file for input
+ fstream file(name, fstream::in);
+
+ // Is it open?
+ if (!file.is_open()) {
+ ADD_FAILURE() << "Unable to open " << name << ". It was expected "
+ << "to be present and to contain the string '"
+ << expected << "'";
+ return (false);
+ }
+
+ // OK, is open, so read the data and see what we have. Compare it
+ // against what is expected.
+ string content;
+ getline(file, content);
+
+ string expected_str(expected);
+ EXPECT_EQ(expected_str, content) << "Marker file " << name
+ << "did not contain the expected data";
+ file.close();
+
+ return (expected_str == content);
+}
+
+// Check if the marker file exists - this is a wrapper for "access(2)" and
+// really tests if the file exists and is accessible
+
+bool
+checkMarkerFileExists(const char* name) {
+ return (access(name, F_OK) == 0);
+}
+
+} // namespace test
+} // namespace dhcp
+} // namespace isc
diff --git a/src/bin/dhcp4/tests/marker_file.h.in b/src/bin/dhcp4/tests/marker_file.h.in
new file mode 100644
index 0000000..06f60d8
--- /dev/null
+++ b/src/bin/dhcp4/tests/marker_file.h.in
@@ -0,0 +1,62 @@
+// Copyright (C) 2013-2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#ifndef MARKER_FILE_H
+#define MARKER_FILE_H
+
+/// @file
+/// Define a marker file that is used in tests to prove that an "unload"
+/// function has been called
+
+namespace {
+const char* const LOAD_MARKER_FILE = "@abs_builddir@/load_marker.txt";
+const char* const UNLOAD_MARKER_FILE = "@abs_builddir@/unload_marker.txt";
+const char* const SRV_CONFIG_MARKER_FILE = "@abs_builddir@/srv_config_marker_file.txt";
+}
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @brief Check marker file
+///
+/// This function is used in some of the DHCP server tests.
+///
+/// Marker files are used by the load/unload functions in the hooks
+/// libraries in these tests to signal whether they have been loaded or
+/// unloaded. The file (if present) contains a single line holding
+/// a set of characters.
+///
+/// This convenience function checks the file to see if the characters
+/// are those expected.
+///
+/// @param name Name of the marker file.
+/// @param expected Characters expected. If a marker file is present,
+/// it is expected to contain characters.
+///
+/// @return true if all tests pass, false if not (in which case a failure
+/// will have been logged).
+bool
+checkMarkerFile(const char* name, const char* expected);
+
+/// @brief Check marker file exists
+///
+/// This function is used in some of the DHCP server tests.
+///
+/// Checks that the specified file does NOT exist.
+///
+/// @param name Name of the marker file.
+///
+/// @return true if file exists, false if not.
+bool
+checkMarkerFileExists(const char* name);
+
+} // namespace test
+} // namespace dhcp
+} // namespace isc
+
+#endif // MARKER_FILE_H
+
diff --git a/src/bin/dhcp4/tests/out_of_range_unittest.cc b/src/bin/dhcp4/tests/out_of_range_unittest.cc
new file mode 100644
index 0000000..b0dba8d
--- /dev/null
+++ b/src/bin/dhcp4/tests/out_of_range_unittest.cc
@@ -0,0 +1,712 @@
+// Copyright (C) 2015-2023 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <asiolink/io_address.h>
+#include <cc/data.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp_ddns/ncr_msg.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/subnet_id.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <dhcp4/tests/dhcp4_client.h>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+
+namespace {
+
+/// @brief Set of JSON configurations used throughout the out of range
+/// tests.
+///
+/// - Configuration 0 - Reference configuration, all tests start with the
+/// server configured with this configuration:
+/// - 1 subnet: 10.0.0.0/24
+/// - with one pool 10.0.0.10 - 10.0.0.100
+/// - 1 address reservation (fixed host) for HW address:
+/// ff:ff:ff:ff:ff:01
+/// - 1 hostname reservation (dynamic host) for HW address:
+/// dd:dd:dd:dd:dd:01,
+/// - DDNS enabled
+/// - Configuration 1 - same subnet, different pool
+/// - Configuration 2 - same subnet, different pool, no reservations
+/// - Configuration 3 - different subnet with reservations
+/// - Configuration 4 - different subnet with no reservations
+/// - Configuration 5 - same as reference, no reservations
+///
+const char* OOR_CONFIGS[] = {
+// Configuration 0 - reference configuration
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1, "
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"reservations\": [ "
+ " {"
+ " \"hw-address\": \"ff:ff:ff:ff:ff:01\","
+ " \"ip-address\": \"10.0.0.7\""
+ " },"
+ " {"
+ " \"hw-address\": \"dd:dd:dd:dd:dd:01\","
+ " \"hostname\": \"test.example.com\""
+ " }"
+ " ]"
+ "} ],"
+ "\"dhcp-ddns\": {"
+ " \"enable-updates\": true,"
+ " \"qualifying-suffix\": \"\""
+ "}"
+ "}",
+
+// Configuration 1 - same subnet as reference, different pool
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1, "
+ " \"pools\": [ { \"pool\": \"10.0.0.101-10.0.0.200\" } ],"
+ " \"reservations\": [ "
+ " {"
+ " \"hw-address\": \"ff:ff:ff:ff:ff:01\","
+ " \"ip-address\": \"10.0.0.7\""
+ " },"
+ " {"
+ " \"hw-address\": \"dd:dd:dd:dd:dd:01\","
+ " \"hostname\": \"reserved.example.com\""
+ " }"
+ " ]"
+ "} ],"
+ "\"dhcp-ddns\": {"
+ " \"enable-updates\": true,"
+ " \"qualifying-suffix\": \"\""
+ "}"
+ "}",
+
+// Configuration 2 - same subnet as reference, different pool, no reservations
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1, "
+ " \"pools\": [ { \"pool\": \"10.0.0.101-10.0.0.200\" } ],"
+ "} ],"
+ "\"dhcp-ddns\": {"
+ " \"enable-updates\": true,"
+ " \"qualifying-suffix\": \"\""
+ "}"
+ "}",
+
+// Configuration 3 - different subnet with reservations
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"id\": 1, "
+ " \"pools\": [ { \"pool\": \"192.0.2.101-192.0.2.200\" } ],"
+ " \"reservations\": [ "
+ " {"
+ " \"hw-address\": \"ff:ff:ff:ff:ff:01\","
+ " \"ip-address\": \"192.0.2.7\""
+ " },"
+ " {"
+ " \"hw-address\": \"dd:dd:dd:dd:dd:01\","
+ " \"hostname\": \"reserved.example.com\""
+ " }"
+ " ]"
+ "} ],"
+ "\"dhcp-ddns\": {"
+ " \"enable-updates\": true,"
+ " \"qualifying-suffix\": \"\""
+ "}"
+ "}",
+
+// Configuration 4 - different subnet with no reservations
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"id\": 1, "
+ " \"pools\": [ { \"pool\": \"192.0.2.101-192.0.2.200\" } ]"
+ "} ],"
+ "\"dhcp-ddns\": {"
+ " \"enable-updates\": true,"
+ " \"qualifying-suffix\": \"\""
+ "}"
+ "}",
+
+// Configuration 5 - same as reference, no reservations
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1, "
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ]"
+ "} ],"
+ "\"dhcp-ddns\": {"
+ " \"enable-updates\": true,"
+ " \"qualifying-suffix\": \"\""
+ "}"
+ "}",
+
+};
+
+/// @brief Test fixture class for testing various exchanges when the client's
+/// leased address is out of range due to configuration changes.
+class OutOfRangeTest : public Dhcpv4SrvTest {
+public:
+
+ /// @brief Enum for indexing into the array of configurations.
+ /// These were created to make the test cases easier to follow.
+ enum CfgIndex {
+ REF_CFG = 0,
+ DIFF_POOL,
+ DIFF_POOL_NO_HR,
+ DIFF_SUBNET,
+ DIFF_SUBNET_NO_HR,
+ NO_HR
+ };
+
+ /// @brief Enum for specifying expected response to client renewal attempt.
+ enum RenewOutcome {
+ DOES_RENEW,
+ DOES_NOT_RENEW,
+ DOES_NOT_NAK
+ };
+
+ /// @brief Enum for specifying expected response to client release attempt.
+ enum ReleaseOutcome {
+ DOES_RELEASE_EXPIRE,
+ DOES_RELEASE_DELETE,
+ DOES_NOT_RELEASE
+ };
+
+ /// @brief Constructor.
+ ///
+ /// Sets up fake interfaces.
+ OutOfRangeTest()
+ : Dhcpv4SrvTest(),
+ d2_mgr_(CfgMgr::instance().getD2ClientMgr()),
+ iface_mgr_test_config_(true) {
+ }
+
+ /// @brief Destructor.
+ ///
+ /// Cleans up statistics after the test.
+ ~OutOfRangeTest() {
+ }
+
+ /// @brief Configure specified DHCP server using JSON string.
+ ///
+ /// @param config String holding server configuration in JSON format.
+ /// @param client Instance of the client.
+ /// @param lease_affinity A flag which indicates if lease affinity should
+ /// be enabled or disabled.
+ void configure(const std::string& config, Dhcp4Client& client,
+ const LeaseAffinity lease_affinity) {
+ NakedDhcpv4Srv& server = *client.getServer();
+ ASSERT_NO_FATAL_FAILURE(Dhcpv4SrvTest::configure(config, server, true,
+ true, true, false,
+ lease_affinity));
+ if (d2_mgr_.ddnsEnabled()) {
+ ASSERT_NO_THROW(server.startD2());
+ }
+ }
+
+ /// @briefVerify that a NameChangeRequest has been queued
+ ///
+ /// Checks that a NCR of a given type (ADD or REMOVE) for a given
+ /// ip address has been queued. If the NCR exits, it is processed
+ /// off the queue. Note the function expects there to be 1 and only
+ /// 1 NCR queued.
+ ///
+ /// @param type - NCR type expected, either CHG_ADD or CHG_REMOVE
+ /// @param addr - string containing the ip address expected in the NCR
+ void verifyNameChangeRequest(const isc::dhcp_ddns::NameChangeType type,
+ const std::string& addr) {
+ ASSERT_TRUE(d2_mgr_.getQueueSize() > 0);
+
+ isc::dhcp_ddns::NameChangeRequestPtr ncr;
+ ASSERT_NO_THROW(ncr = d2_mgr_.peekAt(0));
+ ASSERT_TRUE(ncr);
+
+ EXPECT_EQ(type, ncr->getChangeType());
+ EXPECT_EQ(addr, ncr->getIpAddress());
+
+ // Process the message off the queue
+ ASSERT_NO_THROW(d2_mgr_.runReadyIO());
+ }
+
+ /// @brief Conducts a single out-of-range test scenario
+ ///
+ /// Each test cycles consists of the following two stages, the first is
+ /// a set-up stage during which the server is configured with an initial,
+ /// reference, configuration and a client then verifies that it can acquire
+ /// and renew a lease. The second stage verifies that the server, having
+ /// been reconfigured such that the original lease is now "out-of-range",
+ /// responds correctly to the same client first attempting to renew the
+ /// lease and then attempting to release the lease.
+ ///
+ /// The test also expects the configuration to enable DDNS, and verifies
+ /// that a DNS add is done during stage one, and a DNS remove is done
+ /// in stage two as part of the release request processing.
+ ///
+ /// @param cfg_idx - index of the "replacement" configuration used to
+ /// reconfigure the server during stage two
+ /// @param hwaddress - text value, if not empty, to use as the hardware
+ /// address. This is used for host reservation tests.
+ /// in client queries
+ /// @param expected_address - text value of the expected address. If not
+ /// empty the test will fail if the server responds with a different lease
+ /// address
+ /// @param renew_outcome - expected server reaction in response to the
+ /// client's stage two renewal attempt.
+ /// @param release_outcome - expected server reaction in response to the
+ /// client's stage two release attempt. Currently defaults to
+ /// DOES_RELEASE_DELETE as no cases have been identified which do otherwise.
+ /// @param lease_affinity A flag which indicates if lease affinity should
+ /// be enabled or disabled. In the case lease affinity is enabled, the lease
+ /// is not removed, but instead it is expired, and no DNS update is
+ /// performed.
+ void oorRenewReleaseTest(CfgIndex cfg_idx,
+ const std::string& hwaddress,
+ const std::string& expected_address,
+ RenewOutcome renew_outcome,
+ ReleaseOutcome release_outcome,
+ const LeaseAffinity lease_affinity);
+
+ /// @brief D2 client manager.
+ D2ClientMgr& d2_mgr_;
+
+ /// @brief Interface Manager's fake configuration control.
+ IfaceMgrTestConfig iface_mgr_test_config_;
+};
+
+void
+OutOfRangeTest::oorRenewReleaseTest(CfgIndex cfg_idx,
+ const std::string& hwaddress,
+ const std::string& expected_address,
+ RenewOutcome renew_outcome,
+ ReleaseOutcome release_outcome,
+ const LeaseAffinity lease_affinity) {
+ // STAGE ONE:
+
+ // Step 1 is to acquire the lease
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ // Configure DHCP server.
+ configure(OOR_CONFIGS[REF_CFG], client, lease_affinity);
+
+ // Set the host name so DNS updates will be performed
+ client.includeHostname("test.example.com");
+
+ // Set the hw address if given one
+ if (!hwaddress.empty()) {
+ client.setHWAddress(hwaddress);
+ }
+
+ // Acquire the lease via DORA
+ ASSERT_NO_THROW(client.doDORA());
+
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+ // Verify a lease was created
+ IOAddress leased_address = client.config_.lease_.addr_;
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(leased_address);
+ ASSERT_TRUE(lease);
+
+ // Check the expected address if given
+ if (!expected_address.empty()) {
+ ASSERT_EQ(leased_address, expected_address);
+ }
+
+ // Verify that a DNS add was requested
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, leased_address.toText());
+
+ // The address is still valid, let's verify the lease can be renewed
+ // Set the unicast destination address to indicate that it is a renewal.
+ client.setState(Dhcp4Client::RENEWING);
+ client.setDestAddress(IOAddress("10.0.0.1"));
+ ASSERT_NO_THROW(client.doRequest());
+
+ // Verify that we received an ACK to our renewal
+ resp = client.getContext().response_;
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ EXPECT_EQ(0, d2_mgr_.getQueueSize());
+
+ // STAGE TWO:
+
+ // Now reconfigure which should render our leased address out-of-range
+ configure(OOR_CONFIGS[cfg_idx], client, lease_affinity);
+
+ // Try to renew after the configuration change..
+ ASSERT_NO_THROW(client.doRequest());
+ resp = client.getContext().response_;
+
+ switch(renew_outcome) {
+ case DOES_RENEW:
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ break;
+ case DOES_NOT_RENEW:
+ ASSERT_TRUE(resp);
+ ASSERT_EQ(DHCPNAK, static_cast<int>(resp->getType()));
+ break;
+ case DOES_NOT_NAK:
+ ASSERT_FALSE(resp);
+ break;
+ }
+
+ // Verify that the lease still exists in the database as it has not
+ // been explicitly released.
+ lease = LeaseMgrFactory::instance().getLease4(leased_address);
+ ASSERT_TRUE(lease);
+
+ // Recreate the client lease info, a preceding NAK will have wiped it out
+ client.createLease(leased_address, lease->valid_lft_);
+
+ // Send the release. The server should remove it from the DB and
+ // request DNS remove.
+ ASSERT_NO_THROW(client.doRelease());
+
+ lease = LeaseMgrFactory::instance().getLease4(leased_address);
+
+ if (release_outcome == DOES_RELEASE_DELETE) {
+ EXPECT_FALSE(lease);
+ // Verify the DNS remove was queued.
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE,
+ leased_address.toText());
+ } else {
+ // Lease should still exist, and no NCR should be queued.
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(0, d2_mgr_.getQueueSize());
+ if (release_outcome == DOES_RELEASE_EXPIRE) {
+ EXPECT_EQ(lease->valid_lft_, 0);
+ } else {
+ EXPECT_FALSE(lease->expired());
+ EXPECT_TRUE(lease->valid_lft_ > 0);
+ }
+ }
+}
+
+// Verifies that once-valid lease, whose address is no longer
+// within the subnet's pool:
+//
+// a: Is NAKed upon a renewal attempt
+// b: Is deleted properly upon release, including DNS removal
+//
+TEST_F(OutOfRangeTest, dynamicOutOfPool) {
+
+ std::string hwaddress = "";
+ std::string expected_address = "";
+
+ oorRenewReleaseTest(DIFF_POOL, hwaddress, expected_address, DOES_NOT_NAK,
+ DOES_RELEASE_DELETE, LEASE_AFFINITY_DISABLED);
+}
+
+// Verifies that once-valid lease, whose address is no longer
+// within the subnet's pool:
+//
+// a: Is NAKed upon a renewal attempt
+// b: Is expired properly upon release, including no DNS removal
+//
+TEST_F(OutOfRangeTest, dynamicOutOfPoolNoDelete) {
+
+ std::string hwaddress = "";
+ std::string expected_address = "";
+
+ oorRenewReleaseTest(DIFF_POOL, hwaddress, expected_address, DOES_NOT_NAK,
+ DOES_RELEASE_EXPIRE, LEASE_AFFINITY_ENABLED);
+}
+
+// Verifies that once-valid lease whose address is no longer
+// within any configured subnet:
+//
+// a: Is NAKed upon a renewal attempt
+// b: Is deleted properly upon release, including DNS removal
+//
+TEST_F(OutOfRangeTest, dynamicOutOfSubnet) {
+
+ std::string hwaddress = "";
+ std::string expected_address = "";
+
+ oorRenewReleaseTest(DIFF_SUBNET, hwaddress, expected_address, DOES_NOT_RENEW,
+ DOES_RELEASE_DELETE, LEASE_AFFINITY_DISABLED);
+}
+
+// Verifies that once-valid lease whose address is no longer
+// within any configured subnet:
+//
+// a: Is NAKed upon a renewal attempt
+// b: Is expired properly upon release, including no DNS removal
+//
+TEST_F(OutOfRangeTest, dynamicOutOfSubnetNoDelete) {
+
+ std::string hwaddress = "";
+ std::string expected_address = "";
+
+ oorRenewReleaseTest(DIFF_SUBNET, hwaddress, expected_address, DOES_NOT_RENEW,
+ DOES_RELEASE_EXPIRE, LEASE_AFFINITY_ENABLED);
+}
+
+// Test verifies that once-valid dynamic address host reservation,
+// whose address is no longer within the subnet's pool:
+//
+// a: Is NAKed upon a renewal attempt
+// b: Is deleted properly upon release, including DNS removal
+//
+TEST_F(OutOfRangeTest, dynamicHostOutOfPool) {
+ std::string hwaddress = "dd:dd:dd:dd:dd:01";
+ std::string expected_address = "";
+
+ oorRenewReleaseTest(DIFF_POOL, hwaddress, expected_address, DOES_NOT_NAK,
+ DOES_RELEASE_DELETE, LEASE_AFFINITY_DISABLED);
+}
+
+// Test verifies that once-valid dynamic address host reservation,
+// whose address is no longer within the subnet's pool:
+//
+// a: Is NAKed upon a renewal attempt
+// b: Is expired properly upon release, including no DNS removal
+//
+TEST_F(OutOfRangeTest, dynamicHostOutOfPoolNoDelete) {
+ std::string hwaddress = "dd:dd:dd:dd:dd:01";
+ std::string expected_address = "";
+
+ oorRenewReleaseTest(DIFF_POOL, hwaddress, expected_address, DOES_NOT_NAK,
+ DOES_RELEASE_EXPIRE, LEASE_AFFINITY_ENABLED);
+}
+
+// Test verifies that once-valid dynamic address host reservation,
+// whose address is no longer within any configured subnet:
+//
+// a: Is NAKed upon a renewal attempt
+// b: Is deleted properly upon release, including DNS removal
+//
+TEST_F(OutOfRangeTest, dynamicHostOutOfSubnet) {
+ std::string hwaddress = "dd:dd:dd:dd:dd:01";
+ std::string expected_address = "";
+
+ oorRenewReleaseTest(DIFF_SUBNET, hwaddress, expected_address, DOES_NOT_RENEW,
+ DOES_RELEASE_DELETE, LEASE_AFFINITY_DISABLED);
+}
+
+// Test verifies that once-valid dynamic address host reservation,
+// whose address is no longer within any configured subnet:
+//
+// a: Is NAKed upon a renewal attempt
+// b: Is expired properly upon release, including no DNS removal
+//
+TEST_F(OutOfRangeTest, dynamicHostOutOfSubnetNoDelete) {
+ std::string hwaddress = "dd:dd:dd:dd:dd:01";
+ std::string expected_address = "";
+
+ oorRenewReleaseTest(DIFF_SUBNET, hwaddress, expected_address, DOES_NOT_RENEW,
+ DOES_RELEASE_EXPIRE, LEASE_AFFINITY_ENABLED);
+}
+
+// Test verifies that once-valid dynamic address host reservation,
+// whose address is within the configured subnet and pool, but whose
+// reservation has been removed:
+//
+// a: Is allowed to renew
+// b: Is deleted properly upon release, including DNS removal
+//
+TEST_F(OutOfRangeTest, dynamicHostReservationRemoved) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ std::string hwaddress = "dd:dd:dd:dd:dd:01";
+ std::string expected_address = "";
+
+ oorRenewReleaseTest(NO_HR, hwaddress, expected_address, DOES_RENEW,
+ DOES_RELEASE_DELETE, LEASE_AFFINITY_DISABLED);
+}
+
+// Test verifies that once-valid dynamic address host reservation,
+// whose address is within the configured subnet and pool, but whose
+// reservation has been removed:
+//
+// a: Is allowed to renew
+// b: Is expired properly upon release, including no DNS removal
+//
+TEST_F(OutOfRangeTest, dynamicHostReservationRemovedNoDelete) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ std::string hwaddress = "dd:dd:dd:dd:dd:01";
+ std::string expected_address = "";
+
+ oorRenewReleaseTest(NO_HR, hwaddress, expected_address, DOES_RENEW,
+ DOES_RELEASE_EXPIRE, LEASE_AFFINITY_ENABLED);
+}
+
+// Test verifies that once-valid dynamic address host reservation,
+// whose address is no longer within any configured subnet, and which
+// no longer has reservation defined:
+//
+// a: Is NAKed upon a renewal attempt
+// b: Is deleted properly upon release, including DNS removal
+//
+TEST_F(OutOfRangeTest, dynamicHostOutOfSubnetReservationRemoved) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ std::string hwaddress = "dd:dd:dd:dd:dd:01";
+ std::string expected_address = "";
+
+ oorRenewReleaseTest(DIFF_SUBNET_NO_HR, hwaddress, expected_address, DOES_NOT_RENEW,
+ DOES_RELEASE_DELETE, LEASE_AFFINITY_DISABLED);
+}
+
+// Test verifies that once-valid dynamic address host reservation,
+// whose address is no longer within any configured subnet, and which
+// no longer has reservation defined:
+//
+// a: Is NAKed upon a renewal attempt
+// b: Is expired properly upon release, including no DNS removal
+//
+TEST_F(OutOfRangeTest, dynamicHostOutOfSubnetReservationRemovedNoDelete) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ std::string hwaddress = "dd:dd:dd:dd:dd:01";
+ std::string expected_address = "";
+
+ oorRenewReleaseTest(DIFF_SUBNET_NO_HR, hwaddress, expected_address, DOES_NOT_RENEW,
+ DOES_RELEASE_EXPIRE, LEASE_AFFINITY_ENABLED);
+}
+
+// Test verifies that once-valid in-subnet fixed-address host reservation,
+// after the subnet pool changes:
+//
+// a: Is NAK'd upon a renewal attempt
+// b: Is deleted properly upon release, including DNS removal
+//
+TEST_F(OutOfRangeTest, fixedHostOutOfSubnet) {
+ std::string hwaddress = "ff:ff:ff:ff:ff:01";
+ std::string expected_address = "10.0.0.7";
+
+ oorRenewReleaseTest(DIFF_SUBNET, hwaddress, expected_address, DOES_NOT_RENEW,
+ DOES_RELEASE_DELETE, LEASE_AFFINITY_DISABLED);
+}
+
+// Test verifies that once-valid in-subnet fixed-address host reservation,
+// after the subnet pool changes:
+//
+// a: Is NAK'd upon a renewal attempt
+// b: Is expired properly upon release, including no DNS removal
+//
+TEST_F(OutOfRangeTest, fixedHostOutOfSubnetNoDelete) {
+ std::string hwaddress = "ff:ff:ff:ff:ff:01";
+ std::string expected_address = "10.0.0.7";
+
+ oorRenewReleaseTest(DIFF_SUBNET, hwaddress, expected_address, DOES_NOT_RENEW,
+ DOES_RELEASE_EXPIRE, LEASE_AFFINITY_ENABLED);
+}
+
+// Test verifies that once-valid in-subnet fixed-address host reservation,
+// after the subnet pool is changed:
+//
+// a: Is ACK'd upon a renewal attempt
+// b: Is deleted properly upon release, including DNS removal
+//
+TEST_F(OutOfRangeTest, fixedHostDifferentPool) {
+ std::string hwaddress = "ff:ff:ff:ff:ff:01";
+ std::string expected_address = "10.0.0.7";
+
+ oorRenewReleaseTest(DIFF_POOL, hwaddress, expected_address, DOES_RENEW,
+ DOES_RELEASE_DELETE, LEASE_AFFINITY_DISABLED);
+}
+
+// Test verifies that once-valid in-subnet fixed-address host reservation,
+// after the subnet pool is changed:
+//
+// a: Is ACK'd upon a renewal attempt
+// b: Is expired properly upon release, including no DNS removal
+//
+TEST_F(OutOfRangeTest, fixedHostDifferentPoolNoDelete) {
+ std::string hwaddress = "ff:ff:ff:ff:ff:01";
+ std::string expected_address = "10.0.0.7";
+
+ oorRenewReleaseTest(DIFF_POOL, hwaddress, expected_address, DOES_RENEW,
+ DOES_RELEASE_EXPIRE, LEASE_AFFINITY_ENABLED);
+}
+
+// Test verifies that once-valid in-subnet fixed-address host reservation,
+// whose reservation has been removed from the configuration
+//
+// a: Is NAK'd upon a renewal attempt
+// b: Is deleted properly upon release, including DNS removal
+//
+TEST_F(OutOfRangeTest, fixedHostReservationRemoved) {
+ std::string hwaddress = "ff:ff:ff:ff:ff:01";
+ std::string expected_address = "10.0.0.7";
+
+ oorRenewReleaseTest(NO_HR, hwaddress, expected_address, DOES_NOT_NAK,
+ DOES_RELEASE_DELETE, LEASE_AFFINITY_DISABLED);
+}
+
+
+// Test verifies that once-valid in-subnet fixed-address host reservation,
+// whose reservation has been removed from the configuration
+//
+// a: Is NAK'd upon a renewal attempt
+// b: Is expired properly upon release, including no DNS removal
+//
+TEST_F(OutOfRangeTest, fixedHostReservationRemovedNoDelete) {
+ std::string hwaddress = "ff:ff:ff:ff:ff:01";
+ std::string expected_address = "10.0.0.7";
+
+ oorRenewReleaseTest(NO_HR, hwaddress, expected_address, DOES_NOT_NAK,
+ DOES_RELEASE_EXPIRE, LEASE_AFFINITY_ENABLED);
+}
+
+// Test verifies that once-valid fixed address host reservation,
+// whose address is no longer within any configured subnet
+//
+// a: Is NAKed upon a renewal attempt
+// b: Is deleted properly upon release, including DNS removal
+//
+TEST_F(OutOfRangeTest, fixedHostOutOfSubnetReservationRemoved) {
+ std::string hwaddress = "ff:ff:ff:ff:ff:01";
+ std::string expected_address = "10.0.0.7";
+
+ oorRenewReleaseTest(DIFF_SUBNET_NO_HR, hwaddress, expected_address, DOES_NOT_RENEW,
+ DOES_RELEASE_DELETE, LEASE_AFFINITY_DISABLED);
+}
+
+// Test verifies that once-valid fixed address host reservation,
+// whose address is no longer within any configured subnet
+//
+// a: Is NAKed upon a renewal attempt
+// b: Is expired properly upon release, including no DNS removal
+//
+TEST_F(OutOfRangeTest, fixedHostOutOfSubnetReservationRemovedNoDelete) {
+ std::string hwaddress = "ff:ff:ff:ff:ff:01";
+ std::string expected_address = "10.0.0.7";
+
+ oorRenewReleaseTest(DIFF_SUBNET_NO_HR, hwaddress, expected_address, DOES_NOT_RENEW,
+ DOES_RELEASE_EXPIRE, LEASE_AFFINITY_ENABLED);
+}
+
+} // end of anonymous namespace
diff --git a/src/bin/dhcp4/tests/parser_unittest.cc b/src/bin/dhcp4/tests/parser_unittest.cc
new file mode 100644
index 0000000..426cf8e
--- /dev/null
+++ b/src/bin/dhcp4/tests/parser_unittest.cc
@@ -0,0 +1,990 @@
+// Copyright (C) 2016-2023 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <dhcp4/parser_context.h>
+#include <dhcpsrv/parsers/simple_parser4.h>
+#include <testutils/gtest_utils.h>
+#include <testutils/io_utils.h>
+#include <testutils/log_utils.h>
+#include <testutils/test_to_element.h>
+#include <testutils/user_context_utils.h>
+
+#include <gtest/gtest.h>
+
+#include <fstream>
+#include <set>
+
+#include <boost/algorithm/string.hpp>
+
+using namespace isc::data;
+using namespace isc::test;
+using namespace std;
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @brief compares two JSON trees
+///
+/// If differences are discovered, gtest failure is reported (using EXPECT_EQ)
+///
+/// @param a first to be compared
+/// @param b second to be compared
+void compareJSON(ConstElementPtr a, ConstElementPtr b) {
+ ASSERT_TRUE(a);
+ ASSERT_TRUE(b);
+ EXPECT_EQ(a->str(), b->str())
+#ifdef HAVE_CREATE_UNIFIED_DIFF
+ << "\nDiff:\n" << generateDiff(prettyPrint(a), prettyPrint(b)) << "\n"
+#endif
+ ;
+}
+
+/// @brief Tests if the input string can be parsed with specific parser
+///
+/// The input text will be passed to bison parser of specified type.
+/// Then the same input text is passed to legacy JSON parser and outputs
+/// from both parsers are compared. The legacy comparison can be disabled,
+/// if the feature tested is not supported by the old parser (e.g.
+/// new comment styles)
+///
+/// @param txt text to be compared
+/// @param parser_type bison parser type to be instantiated
+/// @param compare whether to compare the output with legacy JSON parser
+void testParser(const std::string& txt, Parser4Context::ParserType parser_type,
+ bool compare = true) {
+ SCOPED_TRACE("\n=== tested config ===\n" + txt + "=====================");
+
+ ConstElementPtr test_json;
+ ASSERT_NO_THROW({
+ try {
+ Parser4Context ctx;
+ test_json = ctx.parseString(txt, parser_type);
+ } catch (const std::exception &e) {
+ cout << "EXCEPTION: " << e.what() << endl;
+ throw;
+ }
+
+ });
+
+ if (!compare) {
+ return;
+ };
+
+ // Now compare if both representations are the same.
+ ElementPtr reference_json;
+ ASSERT_NO_THROW(reference_json = Element::fromJSON(txt, true));
+ compareJSON(reference_json, test_json);
+}
+
+TEST(ParserTest, mapInMap) {
+ string txt = "{ \"xyzzy\": { \"foo\": 123, \"baz\": 456 } }";
+ testParser(txt, Parser4Context::PARSER_JSON);
+}
+
+TEST(ParserTest, listInList) {
+ string txt = "[ [ \"Britain\", \"Wales\", \"Scotland\" ], "
+ "[ \"Pomorze\", \"Wielkopolska\", \"Tatry\"] ]";
+ testParser(txt, Parser4Context::PARSER_JSON);
+}
+
+TEST(ParserTest, nestedMaps) {
+ string txt = "{ \"europe\": { \"UK\": { \"London\": { \"street\": \"221B Baker\" }}}}";
+ testParser(txt, Parser4Context::PARSER_JSON);
+}
+
+TEST(ParserTest, nestedLists) {
+ string txt = "[ \"half\", [ \"quarter\", [ \"eighth\", [ \"sixteenth\" ]]]]";
+ testParser(txt, Parser4Context::PARSER_JSON);
+}
+
+TEST(ParserTest, listsInMaps) {
+ string txt = "{ \"constellations\": { \"orion\": [ \"rigel\", \"betelgeuse\" ], "
+ "\"cygnus\": [ \"deneb\", \"albireo\"] } }";
+ testParser(txt, Parser4Context::PARSER_JSON);
+}
+
+TEST(ParserTest, mapsInLists) {
+ string txt = "[ { \"body\": \"earth\", \"gravity\": 1.0 },"
+ " { \"body\": \"mars\", \"gravity\": 0.376 } ]";
+ testParser(txt, Parser4Context::PARSER_JSON);
+}
+
+TEST(ParserTest, types) {
+ string txt = "{ \"string\": \"foo\","
+ "\"integer\": 42,"
+ "\"boolean\": true,"
+ "\"map\": { \"foo\": \"bar\" },"
+ "\"list\": [ 1, 2, 3 ],"
+ "\"null\": null }";
+ testParser(txt, Parser4Context::PARSER_JSON);
+}
+
+TEST(ParserTest, keywordJSON) {
+ string txt = "{ \"name\": \"user\","
+ "\"type\": \"password\","
+ "\"user\": \"name\","
+ "\"password\": \"type\" }";
+ testParser(txt, Parser4Context::PARSER_JSON);
+}
+
+TEST(ParserTest, keywordDhcp4) {
+ string txt = "{ \"Dhcp4\": { \"interfaces-config\": {"
+ " \"interfaces\": [ \"type\", \"htype\" ] },\n"
+ "\"rebind-timer\": 2000, \n"
+ "\"renew-timer\": 1000, \n"
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"interface\": \"test\" } ],\n"
+ "\"valid-lifetime\": 4000 } }";
+ testParser(txt, Parser4Context::PARSER_DHCP4);
+}
+
+// Tests if bash (#) comments are supported. That's the only comment type that
+// was supported by the old parser.
+TEST(ParserTest, bashComments) {
+ string txt= "{ \"Dhcp4\": { \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},\n"
+ "# this is a comment\n"
+ "\"rebind-timer\": 2000, \n"
+ "# lots of comments here\n"
+ "# and here\n"
+ "\"renew-timer\": 1000, \n"
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"interface\": \"eth0\""
+ " } ],"
+ "\"valid-lifetime\": 4000 } }";
+ testParser(txt, Parser4Context::PARSER_DHCP4, false);
+}
+
+// Tests if C++ (//) comments can start anywhere, not just in the first line.
+TEST(ParserTest, cppComments) {
+ string txt= "{ \"Dhcp4\": { \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},\n"
+ "\"rebind-timer\": 2000, // everything after // is ignored\n"
+ "\"renew-timer\": 1000, // this will be ignored, too\n"
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"interface\": \"eth0\""
+ " } ],"
+ "\"valid-lifetime\": 4000 } }";
+ testParser(txt, Parser4Context::PARSER_DHCP4, false);
+}
+
+// Tests if bash (#) comments can start anywhere, not just in the first line.
+TEST(ParserTest, bashCommentsInline) {
+ string txt= "{ \"Dhcp4\": { \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},\n"
+ "\"rebind-timer\": 2000, # everything after # is ignored\n"
+ "\"renew-timer\": 1000, # this will be ignored, too\n"
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"interface\": \"eth0\""
+ " } ],"
+ "\"valid-lifetime\": 4000 } }";
+ testParser(txt, Parser4Context::PARSER_DHCP4, false);
+}
+
+// Tests if multi-line C style comments are handled correctly.
+TEST(ParserTest, multilineComments) {
+ string txt= "{ \"Dhcp4\": { \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},\n"
+ " /* this is a C style comment\n"
+ "that\n can \n span \n multiple \n lines */ \n"
+ "\"rebind-timer\": 2000,\n"
+ "\"renew-timer\": 1000, \n"
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"interface\": \"eth0\""
+ " } ],"
+ "\"valid-lifetime\": 4000 } }";
+ testParser(txt, Parser4Context::PARSER_DHCP4, false);
+}
+
+// Tests if embedded comments are handled correctly.
+TEST(ParserTest, embbededComments) {
+ string txt= "{ \"Dhcp4\": { \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},\n"
+ "\"comment\": \"a comment\",\n"
+ "\"rebind-timer\": 2000,\n"
+ "\"renew-timer\": 1000, \n"
+ "\"subnet4\": [ { "
+ " \"user-context\": { \"comment\": \"indirect\" },"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"interface\": \"eth0\""
+ " } ],"
+ "\"user-context\": { \"compatible\": true },"
+ "\"valid-lifetime\": 4000 } }";
+ testParser(txt, Parser4Context::PARSER_DHCP4, false);
+}
+
+/// @brief Loads specified example config file
+///
+/// This test loads specified example file twice: first, using the legacy
+/// JSON file and then second time using bison parser. Two created Element
+/// trees are then compared. The input is decommented before it is passed
+/// to legacy parser (as legacy support for comments is very limited).
+///
+/// @param fname name of the file to be loaded
+void testFile(const std::string& fname) {
+ ElementPtr json;
+ ElementPtr reference_json;
+ ConstElementPtr test_json;
+
+ string decommented = decommentJSONfile(fname);
+
+ cout << "Parsing file " << fname << " (" << decommented << ")" << endl;
+
+ EXPECT_NO_THROW_LOG(json = Element::fromJSONFile(decommented, true));
+ reference_json = moveComments(json);
+
+ // remove the temporary file
+ EXPECT_NO_THROW(::remove(decommented.c_str()));
+
+ EXPECT_NO_THROW(
+ try {
+ Parser4Context ctx;
+ test_json = ctx.parseFile(fname, Parser4Context::PARSER_DHCP4);
+ } catch (const std::exception &x) {
+ cout << "EXCEPTION: " << x.what() << endl;
+ throw;
+ });
+
+ ASSERT_TRUE(reference_json);
+ ASSERT_TRUE(test_json);
+
+ compareJSON(reference_json, test_json);
+}
+
+// This test loads all available existing files. Each config is loaded
+// twice: first with the existing Element::fromJSONFile() and then
+// the second time with Parser4. Both JSON trees are then compared.
+TEST(ParserTest, file) {
+ vector<string> configs = { "advanced.json" ,
+ "all-keys.json",
+ "all-options.json",
+ "backends.json",
+ "classify.json",
+ "classify2.json",
+ "comments.json",
+ "config-backend.json",
+ "dhcpv4-over-dhcpv6.json",
+ "dnr.json",
+ "global-reservations.json",
+ "ha-load-balancing-server1-mt-with-tls.json",
+ "ha-load-balancing-server2-mt.json",
+ "hooks-radius.json",
+ "hooks.json",
+ "leases-expiration.json",
+ "multiple-options.json",
+ "mysql-reservations.json",
+ "pgsql-reservations.json",
+ "reservations.json",
+ "several-subnets.json",
+ "shared-network.json",
+ "single-subnet.json",
+ "vendor-specific.json",
+ "vivso.json",
+ "with-ddns.json" };
+
+ for (int i = 0; i<configs.size(); i++) {
+ testFile(string(CFG_EXAMPLES) + "/" + configs[i]);
+ }
+}
+
+// This test loads the all-keys.json file and checks global parameters.
+TEST(ParserTest, globalParameters) {
+ ConstElementPtr json;
+ Parser4Context ctx;
+ string fname = string(CFG_EXAMPLES) + "/" + "all-keys.json";
+ EXPECT_NO_THROW(json = ctx.parseFile(fname, Parser4Context::PARSER_DHCP4));
+ EXPECT_NO_THROW(json = json->get("Dhcp4"));
+ SimpleParser4 parser;
+ EXPECT_NO_THROW(parser.checkKeywords(parser.GLOBAL4_PARAMETERS, json));
+}
+
+// Basic test that checks if it's possible to specify outbound-interface.
+TEST(ParserTest, outboundIface) {
+ std::string fname = string(CFG_EXAMPLES) + "/" + "advanced.json";
+ Parser4Context ctx;
+ ConstElementPtr test_json = ctx.parseFile(fname, Parser4Context::PARSER_DHCP4);
+
+ ConstElementPtr tmp;
+ tmp = test_json->get("Dhcp4");
+ ASSERT_TRUE(tmp);
+
+ tmp = tmp->get("interfaces-config");
+ ASSERT_TRUE(tmp);
+
+ tmp = tmp->get("outbound-interface");
+ ASSERT_TRUE(tmp);
+ EXPECT_EQ(Element::string, tmp->getType());
+ EXPECT_EQ("use-routing", tmp->stringValue());
+}
+
+/// @brief Tests error conditions in Dhcp4Parser
+///
+/// @param txt text to be parsed
+/// @param parser_type type of the parser to be used in the test
+/// @param msg expected content of the exception
+void testError(const std::string& txt,
+ Parser4Context::ParserType parser_type,
+ const std::string& msg) {
+ SCOPED_TRACE("\n=== tested config ===\n" + txt + "=====================");
+
+ try {
+ Parser4Context ctx;
+ ConstElementPtr parsed = ctx.parseString(txt, parser_type);
+ FAIL() << "Expected Dhcp4ParseError but nothing was raised (expected: "
+ << msg << ")";
+ }
+ catch (const Dhcp4ParseError& ex) {
+ EXPECT_EQ(msg, ex.what());
+ }
+ catch (...) {
+ FAIL() << "Expected Dhcp4ParseError but something else was raised";
+ }
+}
+
+// Verify that error conditions are handled correctly.
+TEST(ParserTest, errors) {
+ // no input
+ testError("", Parser4Context::PARSER_JSON,
+ "<string>:1.1: syntax error, unexpected end of file");
+ testError(" ", Parser4Context::PARSER_JSON,
+ "<string>:1.2: syntax error, unexpected end of file");
+ testError("\n", Parser4Context::PARSER_JSON,
+ "<string>:2.1: syntax error, unexpected end of file");
+ testError("\t", Parser4Context::PARSER_JSON,
+ "<string>:1.2: syntax error, unexpected end of file");
+ testError("\r", Parser4Context::PARSER_JSON,
+ "<string>:1.2: syntax error, unexpected end of file");
+
+ // comments
+ testError("# nothing\n",
+ Parser4Context::PARSER_JSON,
+ "<string>:2.1: syntax error, unexpected end of file");
+ testError(" #\n",
+ Parser4Context::PARSER_JSON,
+ "<string>:2.1: syntax error, unexpected end of file");
+ testError("// nothing\n",
+ Parser4Context::PARSER_JSON,
+ "<string>:2.1: syntax error, unexpected end of file");
+ testError("/* nothing */\n",
+ Parser4Context::PARSER_JSON,
+ "<string>:2.1: syntax error, unexpected end of file");
+ testError("/* no\nthing */\n",
+ Parser4Context::PARSER_JSON,
+ "<string>:3.1: syntax error, unexpected end of file");
+ testError("/* no\nthing */\n\n",
+ Parser4Context::PARSER_JSON,
+ "<string>:4.1: syntax error, unexpected end of file");
+ testError("/* nothing\n",
+ Parser4Context::PARSER_JSON,
+ "Comment not closed. (/* in line 1");
+ testError("\n\n\n/* nothing\n",
+ Parser4Context::PARSER_JSON,
+ "Comment not closed. (/* in line 4");
+ testError("{ /* */*/ }\n",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.3-8: Invalid character: *");
+ testError("{ /* // *// }\n",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.3-11: Invalid character: /");
+ testError("{ /* // */// }\n",
+ Parser4Context::PARSER_JSON,
+ "<string>:2.1: syntax error, unexpected end of file, "
+ "expecting }");
+
+ // includes
+ testError("<?\n",
+ Parser4Context::PARSER_JSON,
+ "Directive not closed.");
+ testError("<?include\n",
+ Parser4Context::PARSER_JSON,
+ "Directive not closed.");
+ string file = string(CFG_EXAMPLES) + "/" + "single-subnet.json";
+ testError("<?include \"" + file + "\"\n",
+ Parser4Context::PARSER_JSON,
+ "Directive not closed.");
+ testError("<?include \"/foo/bar\" ?>/n",
+ Parser4Context::PARSER_JSON,
+ "Can't open include file /foo/bar");
+
+ // JSON keywords
+ testError("{ \"foo\": True }",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.10-13: JSON true reserved keyword is lower case only");
+ testError("{ \"foo\": False }",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.10-14: JSON false reserved keyword is lower case only");
+ testError("{ \"foo\": NULL }",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.10-13: JSON null reserved keyword is lower case only");
+ testError("{ \"foo\": Tru }",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.10: Invalid character: T");
+ testError("{ \"foo\": nul }",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.10: Invalid character: n");
+
+ // numbers
+ testError("123",
+ Parser4Context::PARSER_DHCP4,
+ "<string>:1.1-3: syntax error, unexpected integer, "
+ "expecting {");
+ testError("-456",
+ Parser4Context::PARSER_DHCP4,
+ "<string>:1.1-4: syntax error, unexpected integer, "
+ "expecting {");
+ testError("-0001",
+ Parser4Context::PARSER_DHCP4,
+ "<string>:1.1-5: syntax error, unexpected integer, "
+ "expecting {");
+ testError("1234567890123456789012345678901234567890",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.1-40: Failed to convert "
+ "1234567890123456789012345678901234567890"
+ " to an integer.");
+ testError("-3.14e+0",
+ Parser4Context::PARSER_DHCP4,
+ "<string>:1.1-8: syntax error, unexpected floating point, "
+ "expecting {");
+ testError("1e50000",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.1-7: Failed to convert 1e50000 "
+ "to a floating point.");
+
+ // strings
+ testError("\"aabb\"",
+ Parser4Context::PARSER_DHCP4,
+ "<string>:1.1-6: syntax error, unexpected constant string, "
+ "expecting {");
+ testError("{ \"aabb\"err",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.9: Invalid character: e");
+ testError("{ err\"aabb\"",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.3: Invalid character: e");
+ testError("\"a\n\tb\"",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.1-6 (near 2): Invalid control in \"a\n\tb\"");
+ testError("\"a\n\\u12\"",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.1-8 (near 2): Invalid control in \"a\n\\u12\"");
+ testError("\"a\\n\\tb\"",
+ Parser4Context::PARSER_DHCP4,
+ "<string>:1.1-8: syntax error, unexpected constant string, "
+ "expecting {");
+ testError("\"a\\x01b\"",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.1-8 (near 3): Bad escape in \"a\\x01b\"");
+ testError("\"a\\u0162\"",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.1-9 (near 4): Unsupported unicode escape "
+ "in \"a\\u0162\"");
+ testError("\"a\\u062z\"",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.1-9 (near 3): Bad escape in \"a\\u062z\"");
+ testError("\"abc\\\"",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.1-6 (near 6): Overflow escape in \"abc\\\"");
+ testError("\"a\\u006\"",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.1-8 (near 3): Overflow unicode escape "
+ "in \"a\\u006\"");
+ testError("\"\\u\"",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.1-4 (near 2): Overflow unicode escape in \"\\u\"");
+ testError("\"\\u\x02\"",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.1-5 (near 2): Bad escape in \"\\u\x02\"");
+ testError("\"\\u\\\"foo\"",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.1-5 (near 2): Bad escape in \"\\u\\\"...");
+ testError("\"\x02\\u\"",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.1-5 (near 1): Invalid control in \"\x02\\u\"");
+
+ // from data_unittest.c
+ testError("\\a",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.1: Invalid character: \\");
+ testError("\\",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.1: Invalid character: \\");
+ testError("\\\"\\\"",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.1: Invalid character: \\");
+
+ // want a map
+ testError("[]\n",
+ Parser4Context::PARSER_DHCP4,
+ "<string>:1.1: syntax error, unexpected [, "
+ "expecting {");
+ testError("[]\n",
+ Parser4Context::PARSER_DHCP4,
+ "<string>:1.1: syntax error, unexpected [, "
+ "expecting {");
+ testError("{ 123 }\n",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.3-5: syntax error, unexpected integer, "
+ "expecting }");
+ testError("{ 123 }\n",
+ Parser4Context::PARSER_DHCP4,
+ "<string>:1.3-5: syntax error, unexpected integer, "
+ "expecting Dhcp4");
+ testError("{ \"foo\" }\n",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.9: syntax error, unexpected }, "
+ "expecting :");
+ testError("{ \"foo\" }\n",
+ Parser4Context::PARSER_DHCP4,
+ "<string>:1.3-7: syntax error, unexpected constant string, "
+ "expecting Dhcp4");
+ testError("{ \"foo\":null }\n",
+ Parser4Context::PARSER_DHCP4,
+ "<string>:1.3-7: syntax error, unexpected constant string, "
+ "expecting Dhcp4");
+ testError("{ \"Logging\":null }\n",
+ Parser4Context::PARSER_DHCP4,
+ "<string>:1.3-11: syntax error, unexpected constant string, "
+ "expecting Dhcp4");
+ testError("{ \"Dhcp4\" }\n",
+ Parser4Context::PARSER_DHCP4,
+ "<string>:1.11: syntax error, unexpected }, "
+ "expecting :");
+ testError("{}{}\n",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.3: syntax error, unexpected {, "
+ "expecting end of file");
+
+ // duplicate in map
+ testError("{ \"foo\": 1, \"foo\": true }\n",
+ Parser4Context::PARSER_JSON,
+ "<string>:1:13: duplicate foo entries in "
+ "JSON map (previous at <string>:1:10)");
+
+ // bad commas
+ testError("{ , }\n",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.3: syntax error, unexpected \",\", "
+ "expecting }");
+ testError("{ , \"foo\":true }\n",
+ Parser4Context::PARSER_JSON,
+ "<string>:1.3: syntax error, unexpected \",\", "
+ "expecting }");
+
+ // bad type
+ testError("{ \"Dhcp4\":{\n"
+ " \"valid-lifetime\":false }}\n",
+ Parser4Context::PARSER_DHCP4,
+ "<string>:2.20-24: syntax error, unexpected boolean, "
+ "expecting integer");
+
+ // unknown keyword
+ testError("{ \"Dhcp4\":{\n"
+ " \"valid_lifetime\":600 }}\n",
+ Parser4Context::PARSER_DHCP4,
+ "<string>:2.2-17: got unexpected keyword "
+ "\"valid_lifetime\" in Dhcp4 map.");
+
+ // missing parameter
+ testError("{ \"name\": \"foo\",\n"
+ " \"code\": 123 }\n",
+ Parser4Context::PARSER_OPTION_DEF,
+ "missing parameter 'type' (<string>:1:1) "
+ "[option-def map between <string>:1:1 and <string>:2:15]");
+
+ // user context and embedded comments
+ testError("{ \"Dhcp4\":{\n"
+ " \"comment\": true,\n"
+ " \"valid-lifetime\": 600 }}\n",
+ Parser4Context::PARSER_DHCP4,
+ "<string>:2.14-17: syntax error, unexpected boolean, "
+ "expecting constant string");
+
+ testError("{ \"Dhcp4\":{\n"
+ " \"user-context\": \"a comment\",\n"
+ " \"valid-lifetime\": 600 }}\n",
+ Parser4Context::PARSER_DHCP4,
+ "<string>:2.19-29: syntax error, unexpected constant string, "
+ "expecting {");
+
+ testError("{ \"Dhcp4\":{\n"
+ " \"comment\": \"a comment\",\n"
+ " \"comment\": \"another one\",\n"
+ " \"valid-lifetime\": 600 }}\n",
+ Parser4Context::PARSER_DHCP4,
+ "<string>:3.3-11: duplicate user-context/comment entries "
+ "(previous at <string>:2:3)");
+
+ testError("{ \"Dhcp4\":{\n"
+ " \"user-context\": { \"version\": 1 },\n"
+ " \"user-context\": { \"one\": \"only\" },\n"
+ " \"valid-lifetime\": 600 }}\n",
+ Parser4Context::PARSER_DHCP4,
+ "<string>:3.3-16: duplicate user-context entries "
+ "(previous at <string>:2:19)");
+
+ testError("{ \"Dhcp4\":{\n"
+ " \"user-context\": { \"comment\": \"indirect\" },\n"
+ " \"comment\": \"a comment\",\n"
+ " \"valid-lifetime\": 600 }}\n",
+ Parser4Context::PARSER_DHCP4,
+ "<string>:3.3-11: duplicate user-context/comment entries "
+ "(previous at <string>:2:19)");
+
+ // duplicate Dhcp4 entries
+ testError("{ \"Dhcp4\":{\n"
+ " \"comment\": \"first\" },\n"
+ " \"Dhcp4\":{\n"
+ " \"comment\": \"second\" }}\n",
+ Parser4Context::PARSER_DHCP4,
+ "<string>:3.3-9: syntax error, unexpected Dhcp4, expecting \",\" or }");
+
+ // duplicate of not string entries
+ testError("{ \"Dhcp4\":{\n"
+ " \"subnet4\": [],\n"
+ " \"subnet4\": [] }}\n",
+ Parser4Context::PARSER_DHCP4,
+ "<string>:3:2: duplicate subnet4 entries in "
+ "Dhcp4 map (previous at <string>:2:2)");
+
+ // duplicate of string entries
+ testError("{\n"
+ " \"server-hostname\": \"nohost\",\n"
+ " \"server-hostname\": \"nofile\" }\n",
+ Parser4Context::PARSER_HOST_RESERVATION,
+ "<string>:3:2: duplicate server-hostname entries in "
+ "reservations map (previous at <string>:2:21)");
+}
+
+// Check unicode escapes
+TEST(ParserTest, unicodeEscapes) {
+ ConstElementPtr result;
+ string json;
+
+ // check we can reread output
+ for (char c = -128; c < 127; ++c) {
+ string ins(" ");
+ ins[1] = c;
+ ConstElementPtr e(new StringElement(ins));
+ json = e->str();
+ ASSERT_NO_THROW(
+ try {
+ Parser4Context ctx;
+ result = ctx.parseString(json, Parser4Context::PARSER_JSON);
+ } catch (const std::exception &x) {
+ cout << "EXCEPTION: " << x.what() << endl;
+ throw;
+ });
+ ASSERT_EQ(Element::string, result->getType());
+ EXPECT_EQ(ins, result->stringValue());
+ }
+}
+
+// This test checks that all representations of a slash are recognized properly.
+TEST(ParserTest, unicodeSlash) {
+ // check the 4 possible encodings of solidus '/'
+ ConstElementPtr result;
+ string json = "\"/\\/\\u002f\\u002F\"";
+ ASSERT_NO_THROW(
+ try {
+ Parser4Context ctx;
+ result = ctx.parseString(json, Parser4Context::PARSER_JSON);
+ } catch (const std::exception &x) {
+ cout << "EXCEPTION: " << x.what() << endl;
+ throw;
+ });
+ ASSERT_EQ(Element::string, result->getType());
+ EXPECT_EQ("////", result->stringValue());
+}
+
+/// @brief Load a file into a JSON element.
+///
+/// @param fname The name of the file to load.
+/// @param list The JSON element list to add the parsing result to.
+void loadFile(const string& fname, ElementPtr list) {
+ Parser4Context ctx;
+ ElementPtr json;
+ EXPECT_NO_THROW(json = ctx.parseFile(fname, Parser4Context::PARSER_DHCP4));
+ ASSERT_TRUE(json);
+ list->add(json);
+}
+
+// This test checks that all map entries are in the example files.
+TEST(ParserTest, mapEntries) {
+ // Type of keyword set.
+ typedef set<string> KeywordSet;
+
+ // Get keywords from the syntax file (dhcp4_parser.yy).
+ ifstream syntax_file(SYNTAX_FILE);
+ EXPECT_TRUE(syntax_file.is_open());
+ string line;
+ KeywordSet syntax_keys = { "user-context" };
+ // Code setting the map entry.
+ const string pattern = "ctx.stack_.back()->set(\"";
+ while (getline(syntax_file, line)) {
+ // Skip comments.
+ size_t comment = line.find("//");
+ if (comment <= pattern.size()) {
+ continue;
+ }
+ if (comment != string::npos) {
+ line.resize(comment);
+ }
+ // Search for the code pattern.
+ size_t key_begin = line.find(pattern);
+ if (key_begin == string::npos) {
+ continue;
+ }
+ // Extract keywords.
+ line = line.substr(key_begin + pattern.size());
+ size_t key_end = line.find_first_of('"');
+ EXPECT_NE(string::npos, key_end);
+ string keyword = line.substr(0, key_end);
+ // Ignore result when adding the keyword to the syntax keyword set.
+ static_cast<void>(syntax_keys.insert(keyword));
+ }
+ syntax_file.close();
+
+ // Get keywords from the example files.
+ string sample_dir(CFG_EXAMPLES);
+ sample_dir += "/";
+ ElementPtr sample_json = Element::createList();
+ loadFile(sample_dir + "all-keys.json", sample_json);
+ loadFile(sample_dir + "reservations.json", sample_json);
+ loadFile(sample_dir + "all-keys-netconf.json", sample_json);
+ KeywordSet sample_keys = {
+ "hosts-database",
+ "reservation-mode"
+ };
+ // Recursively extract keywords.
+ static void (*extract)(ConstElementPtr, KeywordSet&) =
+ [] (ConstElementPtr json, KeywordSet& set) {
+ if (json->getType() == Element::list) {
+ // Handle lists.
+ for (auto elem : json->listValue()) {
+ extract(elem, set);
+ }
+ } else if (json->getType() == Element::map) {
+ // Handle maps.
+ for (auto elem : json->mapValue()) {
+ static_cast<void>(set.insert(elem.first));
+ // Skip entries with free content.
+ if ((elem.first != "user-context") &&
+ (elem.first != "parameters")) {
+ extract(elem.second, set);
+ }
+ }
+ }
+ };
+ extract(sample_json, sample_keys);
+
+ // Compare.
+ auto print_keys = [](const KeywordSet& keys) {
+ string s = "{";
+ bool first = true;
+ for (auto key : keys) {
+ if (first) {
+ first = false;
+ s += " ";
+ } else {
+ s += ", ";
+ }
+ s += "\"" + key + "\"";
+ }
+ return (s + " }");
+ };
+ EXPECT_EQ(syntax_keys, sample_keys)
+ << "syntax has: " << print_keys(syntax_keys) << endl
+ << "sample has: " << print_keys(sample_keys) << endl;
+}
+
+/// @brief Tests a duplicate entry.
+///
+/// The entry was duplicated by adding a new <name>DDDD entry.
+/// An error is expected, usually it is a duplicate but there are
+/// a few syntax errors when the syntax allows only one parameter.
+///
+/// @param json the JSON configuration with the duplicate entry.
+void testDuplicate(ConstElementPtr json) {
+ string config = json->str();
+ size_t where = config.find("DDDD");
+ ASSERT_NE(string::npos, where);
+ string before = config.substr(0, where);
+ string after = config.substr(where + 4, string::npos);
+ Parser4Context ctx;
+ EXPECT_THROW(ctx.parseString(before + after,
+ Parser4Context::PARSER_DHCP4),
+ Dhcp4ParseError) << "config: " << config;
+}
+
+// This test checks that duplicate entries make parsing to fail.
+TEST(ParserTest, duplicateMapEntries) {
+ // Get the config to work with from the all keys file.
+ string sample_fname(CFG_EXAMPLES);
+ sample_fname += "/all-keys.json";
+ Parser4Context ctx;
+ ElementPtr sample_json;
+ EXPECT_NO_THROW(sample_json =
+ ctx.parseFile(sample_fname, Parser4Context::PARSER_DHCP4));
+ ASSERT_TRUE(sample_json);
+
+ // Recursively check duplicates.
+ static void (*test)(ElementPtr, ElementPtr, size_t&) =
+ [] (ElementPtr config, ElementPtr json, size_t& cnt) {
+ if (json->getType() == Element::list) {
+ // Handle lists.
+ for (auto elem : json->listValue()) {
+ test(config, elem, cnt);
+ }
+ } else if (json->getType() == Element::map) {
+ // Handle maps.
+ for (auto elem : json->mapValue()) {
+ // Skip entries with free content.
+ if ((elem.first == "user-context") ||
+ (elem.first == "parameters")) {
+ continue;
+ }
+
+ // Perform tests.
+ string dup = elem.first + "DDDD";
+ json->set(dup, elem.second);
+ testDuplicate(config);
+ json->remove(dup);
+ ++cnt;
+
+ // Recursive call.
+ ElementPtr mutable_json =
+ boost::const_pointer_cast<Element>(elem.second);
+ ASSERT_TRUE(mutable_json);
+ test(config, mutable_json, cnt);
+ }
+ }
+ };
+ size_t cnt = 0;
+ test(sample_json, sample_json, cnt);
+ cout << "checked " << cnt << " duplicated map entries\n";
+}
+
+/// @brief Test fixture for trailing commas.
+class TrailingCommasTest : public isc::dhcp::test::LogContentTest {
+public:
+ /// @brief Add a log entry.
+ ///
+ /// @param loc Location of the trailing comma.
+ void addLog(const string& loc) {
+ string log = "DHCP4_CONFIG_SYNTAX_WARNING configuration syntax ";
+ log += "warning: " + loc;
+ log += ": Extraneous comma. ";
+ log += "A piece of configuration may have been omitted.";
+ addString(log);
+ }
+};
+
+// Test that trailing commas are allowed.
+TEST_F(TrailingCommasTest, tests) {
+ string txt(R"({
+ "Dhcp4": {
+ "control-socket": {
+ "socket-name": "/tmp/kea-dhcp4-ctrl.sock",
+ "socket-type": "unix",
+ },
+ "hooks-libraries": [
+ {
+ "library": "/usr/local/lib/kea/hooks/libdhcp_dummy.so",
+ },
+ ],
+ "interfaces-config": {
+ "interfaces": [
+ "eth0",
+ ],
+ },
+ "lease-database": {
+ "name": "/tmp/kea-dhcp4.csv",
+ "persist": true,
+ "type": "memfile",
+ },
+ "loggers": [
+ {
+ "debuglevel": 99,
+ "name": "kea-dhcp4",
+ "output_options": [
+ {
+ "output": "stdout",
+ },
+ ],
+ "severity": "DEBUG",
+ },
+ ],
+ "multi-threading": {
+ "enable-multi-threading": false,
+ "packet-queue-size": 0,
+ "thread-pool-size": 0
+ },
+ "subnet4": [
+ {
+ "pools": [
+ {
+ "pool": "192.168.0.0/24",
+ },
+ ],
+ "id": 1,
+ "subnet": "192.168.0.0/24",
+ },
+ ],
+ },
+})");
+ testParser(txt, Parser4Context::PARSER_DHCP4, false);
+
+ addLog("<string>:5.28");
+ addLog("<string>:9.63");
+ addLog("<string>:10.8");
+ addLog("<string>:14.15");
+ addLog("<string>:15.8");
+ addLog("<string>:20.24");
+ addLog("<string>:28.31");
+ addLog("<string>:29.12");
+ addLog("<string>:31.28");
+ addLog("<string>:32.8");
+ addLog("<string>:43.37");
+ addLog("<string>:44.12");
+ addLog("<string>:47.35");
+ addLog("<string>:48.8");
+ addLog("<string>:49.6");
+ addLog("<string>:50.4");
+ EXPECT_TRUE(checkFile());
+
+ // Test with many consecutive commas.
+ boost::replace_all(txt, ",", ",,,,");
+ testParser(txt, Parser4Context::PARSER_DHCP4, false);
+}
+
+} // namespace test
+} // namespace dhcp
+} // namespace isc
diff --git a/src/bin/dhcp4/tests/release_unittest.cc b/src/bin/dhcp4/tests/release_unittest.cc
new file mode 100644
index 0000000..c59bd45
--- /dev/null
+++ b/src/bin/dhcp4/tests/release_unittest.cc
@@ -0,0 +1,418 @@
+// Copyright (C) 2015-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <asiolink/io_address.h>
+#include <cc/data.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/subnet_id.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <dhcp4/tests/dhcp4_client.h>
+#include <stats/stats_mgr.h>
+#include <boost/shared_ptr.hpp>
+#include <sstream>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::stats;
+
+namespace {
+
+/// @brief Set of JSON configurations used throughout the Release tests.
+///
+/// - Configuration 0:
+/// - Used for testing Release message processing
+/// - 1 subnet: 10.0.0.0/24
+/// - 1 pool: 10.0.0.10-10.0.0.100
+/// - Router option present: 10.0.0.200 and 10.0.0.201
+const char* RELEASE_CONFIGS[] = {
+// Configuration 0
+ "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"valid-lifetime\": 600,"
+ "\"subnet4\": [ { "
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"id\": 1,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+ " \"option-data\": [ {"
+ " \"name\": \"routers\","
+ " \"data\": \"10.0.0.200,10.0.0.201\""
+ " } ]"
+ " } ]"
+ "}"
+};
+
+/// @brief Test fixture class for testing 4-way (DORA) exchanges.
+///
+/// @todo Currently there is a limit number of test cases covered here.
+/// In the future it is planned that the tests from the
+/// dhcp4_srv_unittest.cc will be migrated here and will use the
+/// @c Dhcp4Client class.
+class ReleaseTest : public Dhcpv4SrvTest {
+public:
+
+ enum ExpectedResult {
+ SHOULD_PASS_EXPIRED,
+ SHOULD_PASS_DELETED,
+ SHOULD_FAIL
+ };
+
+ /// @brief Constructor.
+ ///
+ /// Sets up fake interfaces.
+ ReleaseTest()
+ : Dhcpv4SrvTest(), iface_mgr_test_config_(true) {
+ }
+
+ /// @brief Performs 4-way exchange to obtain new lease.
+ ///
+ /// @param client Client to be used to obtain a lease.
+ void acquireLease(Dhcp4Client& client);
+
+ /// @brief Tests if the acquired lease is or is not released.
+ ///
+ /// @param hw_address_1 HW Address to be used to acquire the lease.
+ /// @param client_id_1 Client id to be used to acquire the lease.
+ /// @param hw_address_2 HW Address to be used to release the lease.
+ /// @param client_id_2 Client id to be used to release the lease.
+ /// @param expected_result SHOULD_PASS_EXPIRED if the lease is expected to
+ /// be successfully released and expired, SHOULD_PASS_DELETED if the lease
+ /// is expected to be successfully released and deleted, or SHOULD_FAIL if
+ /// the lease is expected to not be released.
+ /// @param lease_affinity A flag which indicates if lease affinity should
+ /// be enabled or disabled.
+ void acquireAndRelease(const std::string& hw_address_1,
+ const std::string& client_id_1,
+ const std::string& hw_address_2,
+ const std::string& client_id_2,
+ ExpectedResult expected_result,
+ const LeaseAffinity lease_affinity);
+
+ /// @brief Interface Manager's fake configuration control.
+ IfaceMgrTestConfig iface_mgr_test_config_;
+};
+
+void
+ReleaseTest::acquireLease(Dhcp4Client& client) {
+ // Perform 4-way exchange with the server but to not request any
+ // specific address in the DHCPDISCOVER message.
+ ASSERT_NO_THROW(client.doDORA());
+ // Make sure that the server responded.
+ ASSERT_TRUE(client.getContext().response_);
+ Pkt4Ptr resp = client.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // Response must not be relayed.
+ EXPECT_FALSE(resp->isRelayed());
+ // Make sure that the server id is present.
+ EXPECT_EQ("10.0.0.1", client.config_.serverid_.toText());
+ // Make sure that the client has got the lease with the requested address.
+ ASSERT_NE(client.config_.lease_.addr_.toText(), "0.0.0.0");
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(client.config_.lease_.addr_);
+ ASSERT_TRUE(lease);
+}
+
+void
+ReleaseTest::acquireAndRelease(const std::string& hw_address_1,
+ const std::string& client_id_1,
+ const std::string& hw_address_2,
+ const std::string& client_id_2,
+ ExpectedResult expected_result,
+ const LeaseAffinity lease_affinity) {
+ CfgMgr::instance().clear();
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Configure DHCP server.
+ configure(RELEASE_CONFIGS[0], *client.getServer(), true, true, true, false, lease_affinity);
+ // Explicitly set the client id.
+ client.includeClientId(client_id_1);
+ // Explicitly set the HW address.
+ client.setHWAddress(hw_address_1);
+ // Perform 4-way exchange to obtain a new lease.
+ acquireLease(client);
+
+ std::stringstream name;
+
+ // Let's get the subnet-id and generate statistics name out of it
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ ASSERT_EQ(1, subnets->size());
+ name << "subnet[" << (*subnets->begin())->getID() << "].assigned-addresses";
+
+ ObservationPtr assigned_cnt = StatsMgr::instance().getObservation(name.str());
+ ASSERT_TRUE(assigned_cnt);
+ uint64_t before = assigned_cnt->getInteger().first;
+
+ // Remember the acquired address.
+ IOAddress leased_address = client.config_.lease_.addr_;
+
+ // Explicitly set the client id for DHCPRELEASE.
+ client.includeClientId(client_id_2);
+ // Explicitly set the HW address for DHCPRELEASE.
+ client.setHWAddress(hw_address_2);
+
+ // Send the release and make sure that the lease is removed from the
+ // server.
+ ASSERT_NO_THROW(client.doRelease());
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(leased_address);
+
+ assigned_cnt = StatsMgr::instance().getObservation(name.str());
+ ASSERT_TRUE(assigned_cnt);
+ uint64_t after = assigned_cnt->getInteger().first;
+
+ // We check if the release process was successful by checking if the
+ // lease is in the database. It is expected that it is not present,
+ // i.e. has been deleted with the release.
+ if (expected_result == SHOULD_PASS_EXPIRED) {
+ ASSERT_TRUE(lease);
+
+ // The update succeeded, so the assigned-address should be expired
+ EXPECT_EQ(lease->valid_lft_, 0);
+
+ // No lease has been removed, so the assigned-address should be the same
+ // as before
+ EXPECT_EQ(before, after);
+ } else if (expected_result == SHOULD_PASS_DELETED) {
+ EXPECT_FALSE(lease);
+
+ // The removal succeeded, so the assigned-addresses statistic should
+ // be decreased by one
+ EXPECT_EQ(before, after + 1);
+ } else {
+ EXPECT_TRUE(lease);
+
+ // The removal failed, so the assigned-address should be the same
+ // as before
+ EXPECT_EQ(before, after);
+ }
+}
+
+// This test checks that the client can acquire and release the lease.
+TEST_F(ReleaseTest, releaseNoIdentifierChange) {
+ acquireAndRelease("01:02:03:04:05:06", "12:14",
+ "01:02:03:04:05:06", "12:14",
+ SHOULD_PASS_DELETED, LEASE_AFFINITY_DISABLED);
+}
+
+// This test checks that the client can acquire and release the lease.
+TEST_F(ReleaseTest, releaseNoDeleteNoIdentifierChange) {
+ acquireAndRelease("01:02:03:04:05:06", "12:14",
+ "01:02:03:04:05:06", "12:14",
+ SHOULD_PASS_EXPIRED, LEASE_AFFINITY_ENABLED);
+}
+
+// This test verifies the release correctness in the following case:
+// - Client acquires new lease using HW address only
+// - Client sends the DHCPRELEASE with valid HW address and without
+// client identifier.
+// - The server successfully releases the lease.
+TEST_F(ReleaseTest, releaseHWAddressOnly) {
+ acquireAndRelease("01:02:03:04:05:06", "",
+ "01:02:03:04:05:06", "",
+ SHOULD_PASS_DELETED, LEASE_AFFINITY_DISABLED);
+}
+
+// This test verifies the release correctness in the following case:
+// - Client acquires new lease using HW address only
+// - Client sends the DHCPRELEASE with valid HW address and without
+// client identifier.
+// - The server successfully releases the lease.
+TEST_F(ReleaseTest, releaseNoDeleteHWAddressOnly) {
+ acquireAndRelease("01:02:03:04:05:06", "",
+ "01:02:03:04:05:06", "",
+ SHOULD_PASS_EXPIRED, LEASE_AFFINITY_ENABLED);
+}
+
+// This test verifies the release correctness in the following case:
+// - Client acquires new lease using the client identifier and HW address
+// - Client sends the DHCPRELEASE with valid HW address but with no
+// client identifier.
+// - The server successfully releases the lease.
+TEST_F(ReleaseTest, releaseNoClientId) {
+ acquireAndRelease("01:02:03:04:05:06", "12:14",
+ "01:02:03:04:05:06", "",
+ SHOULD_PASS_DELETED, LEASE_AFFINITY_DISABLED);
+}
+
+// This test verifies the release correctness in the following case:
+// - Client acquires new lease using the client identifier and HW address
+// - Client sends the DHCPRELEASE with valid HW address but with no
+// client identifier.
+// - The server successfully releases the lease.
+TEST_F(ReleaseTest, releaseNoDeleteNoClientId) {
+ acquireAndRelease("01:02:03:04:05:06", "12:14",
+ "01:02:03:04:05:06", "",
+ SHOULD_PASS_EXPIRED, LEASE_AFFINITY_ENABLED);
+}
+
+// This test verifies the release correctness in the following case:
+// - Client acquires new lease using HW address
+// - Client sends the DHCPRELEASE with valid HW address and some
+// client identifier.
+// - The server identifies the lease using HW address and releases
+// this lease.
+TEST_F(ReleaseTest, releaseNoClientId2) {
+ acquireAndRelease("01:02:03:04:05:06", "",
+ "01:02:03:04:05:06", "12:14",
+ SHOULD_PASS_DELETED, LEASE_AFFINITY_DISABLED);
+}
+
+// This test verifies the release correctness in the following case:
+// - Client acquires new lease using HW address
+// - Client sends the DHCPRELEASE with valid HW address and some
+// client identifier.
+// - The server identifies the lease using HW address and releases
+// this lease.
+TEST_F(ReleaseTest, releaseNoDeleteNoClientId2) {
+ acquireAndRelease("01:02:03:04:05:06", "",
+ "01:02:03:04:05:06", "12:14",
+ SHOULD_PASS_EXPIRED, LEASE_AFFINITY_ENABLED);
+}
+
+// This test checks the server's behavior in the following case:
+// - Client acquires new lease using the client identifier and HW address
+// - Client sends the DHCPRELEASE with the valid HW address but with invalid
+// client identifier.
+// - The server should not remove the lease.
+TEST_F(ReleaseTest, releaseNonMatchingClientId) {
+ acquireAndRelease("01:02:03:04:05:06", "12:14",
+ "01:02:03:04:05:06", "12:15:16",
+ SHOULD_FAIL, LEASE_AFFINITY_DISABLED);
+}
+
+// This test checks the server's behavior in the following case:
+// - Client acquires new lease using client identifier and HW address
+// - Client sends the DHCPRELEASE with the same client identifier but
+// different HW address.
+// - The server uses client identifier to find the client's lease and
+// releases it.
+TEST_F(ReleaseTest, releaseNonMatchingHWAddress) {
+ acquireAndRelease("01:02:03:04:05:06", "12:14",
+ "06:06:06:06:06:06", "12:14",
+ SHOULD_PASS_DELETED, LEASE_AFFINITY_DISABLED);
+}
+
+// This test checks the server's behavior in the following case:
+// - Client acquires new lease using client identifier and HW address
+// - Client sends the DHCPRELEASE with the same client identifier but
+// different HW address.
+// - The server uses client identifier to find the client's lease and
+// releases it.
+TEST_F(ReleaseTest, releaseNoDeleteNonMatchingHWAddress) {
+ acquireAndRelease("01:02:03:04:05:06", "12:14",
+ "06:06:06:06:06:06", "12:14",
+ SHOULD_PASS_EXPIRED, LEASE_AFFINITY_ENABLED);
+}
+
+// This test checks the server's behavior in the following case:
+// - Client acquires new lease.
+// - Client sends DHCPRELEASE with the ciaddr set to a different
+// address than it has acquired from the server.
+// - Server determines that the client is trying to release a
+// wrong address and will refuse to release.
+TEST_F(ReleaseTest, releaseNonMatchingIPAddress) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Configure DHCP server.
+ configure(RELEASE_CONFIGS[0], *client.getServer());
+ // Perform 4-way exchange to obtain a new lease.
+ acquireLease(client);
+
+ // Remember the acquired address.
+ IOAddress leased_address = client.config_.lease_.addr_;
+
+ // Modify the client's address to force it to release a different address
+ // than it has obtained from the server.
+ client.config_.lease_.addr_ = IOAddress(leased_address.toUint32() + 1);
+
+ // Send DHCPRELEASE and make sure it was unsuccessful, i.e. the lease
+ // remains in the database.
+ ASSERT_NO_THROW(client.doRelease());
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(leased_address);
+ ASSERT_TRUE(lease);
+}
+
+// This test checks the server's behavior in the following case:
+// - Client acquires new lease.
+// - Client sends DHCPRELEASE with the ciaddr set to a different
+// address than it has acquired from the server.
+// - Server determines that the client is trying to release a
+// wrong address and will refuse to release.
+TEST_F(ReleaseTest, releaseNoDeleteNonMatchingIPAddress) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Configure DHCP server.
+ configure(RELEASE_CONFIGS[0], *client.getServer(), true, true, true, false, LEASE_AFFINITY_ENABLED);
+ // Perform 4-way exchange to obtain a new lease.
+ acquireLease(client);
+
+ // Remember the acquired address.
+ IOAddress leased_address = client.config_.lease_.addr_;
+
+ // Modify the client's address to force it to release a different address
+ // than it has obtained from the server.
+ client.config_.lease_.addr_ = IOAddress(leased_address.toUint32() + 1);
+
+ // Send DHCPRELEASE and make sure it was unsuccessful, i.e. the lease
+ // remains in the database.
+ ASSERT_NO_THROW(client.doRelease());
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(leased_address);
+ ASSERT_TRUE(lease);
+}
+
+// This test verifies that an incoming RELEASE for an address within
+// a subnet that has been removed can still be released.
+TEST_F(ReleaseTest, releaseNoSubnet) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Configure DHCP server.
+ configure(RELEASE_CONFIGS[0], *client.getServer());
+ // Perform 4-way exchange to obtain a new lease.
+ acquireLease(client);
+
+ // Remember the acquired address.
+ IOAddress leased_address = client.config_.lease_.addr_;
+
+ // Release is as it was relayed
+ client.useRelay(true);
+
+ // Send the release
+ ASSERT_NO_THROW(client.doRelease());
+
+ // Check that the lease was removed
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(leased_address);
+ EXPECT_FALSE(lease);
+}
+
+// This test verifies that an incoming RELEASE for an address within
+// a subnet that has been removed can still be released.
+TEST_F(ReleaseTest, releaseNoDeleteNoSubnet) {
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ // Configure DHCP server.
+ configure(RELEASE_CONFIGS[0], *client.getServer(), true, true, true, false, LEASE_AFFINITY_ENABLED);
+ // Perform 4-way exchange to obtain a new lease.
+ acquireLease(client);
+
+ // Remember the acquired address.
+ IOAddress leased_address = client.config_.lease_.addr_;
+
+ // Release is as it was relayed
+ client.useRelay(true);
+
+ // Send the release
+ ASSERT_NO_THROW(client.doRelease());
+
+ // Check that the lease was not removed
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(leased_address);
+ ASSERT_TRUE(lease);
+
+ // Check That the lease has been expired
+ EXPECT_EQ(lease->valid_lft_, 0);
+}
+
+} // end of anonymous namespace
diff --git a/src/bin/dhcp4/tests/shared_network_unittest.cc b/src/bin/dhcp4/tests/shared_network_unittest.cc
new file mode 100644
index 0000000..a5cefbe
--- /dev/null
+++ b/src/bin/dhcp4/tests/shared_network_unittest.cc
@@ -0,0 +1,3013 @@
+// Copyright (C) 2017-2023 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <asiolink/io_address.h>
+#include <cc/data.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcp/option.h>
+#include <dhcp/option_int.h>
+#include <dhcp/option_string.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/cfg_subnets4.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <dhcp4/json_config_parser.h>
+#include <dhcp4/tests/dhcp4_client.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <stats/stats_mgr.h>
+#include <boost/pointer_cast.hpp>
+#include <boost/shared_ptr.hpp>
+#include <functional>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::stats;
+
+namespace {
+
+/// @brief Array of server configurations used throughout the tests.
+const char* NETWORKS_CONFIG[] = {
+// Configuration #0
+// - 1 shared network with 2 subnets (interface specified)
+// - 1 "plain" subnet (different interface specified)
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"interface\": \"eth1\","
+ " \"comment\": \"example\","
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/26\","
+ " \"id\": 10,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.63 - 192.0.2.63\""
+ " }"
+ " ]"
+ " },"
+ " {"
+ " \"subnet\": \"10.0.0.0/24\","
+ " \"id\": 100,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"10.0.0.16 - 10.0.0.16\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ],"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.64/26\","
+ " \"id\": 1000,"
+ " \"interface\": \"eth0\","
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.65 - 192.0.2.65\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}",
+
+// Configuration #1
+// - 1 shared networks with 1 subnet, relay ip specified
+// - 1 "plain" subnet, relay ip specified
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"relay\": {"
+ " \"ip-address\": \"192.3.5.6\""
+ " },"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/26\","
+ " \"id\": 10,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.63 - 192.0.2.63\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ],"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.64/26\","
+ " \"id\": 1000,"
+ " \"relay\": {"
+ " \"ip-address\": \"192.1.2.3\""
+ " },"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.65 - 192.0.2.65\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}",
+
+// Configuration #2
+// - 2 classes defined
+// - 1 shared network with 2 subnets (first has class restriction)
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"client-classes\": ["
+ " {"
+ " \"name\": \"a-devices\","
+ " \"test\": \"option[93].hex == 0x0001\""
+ " },"
+ " {"
+ " \"name\": \"b-devices\","
+ " \"test\": \"option[93].hex == 0x0002\""
+ " }"
+ " ],"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"relay\": {"
+ " \"ip-address\": \"192.3.5.6\""
+ " },"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/26\","
+ " \"id\": 10,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.63 - 192.0.2.63\""
+ " }"
+ " ],"
+ " \"client-class\": \"a-devices\""
+ " },"
+ " {"
+ " \"subnet\": \"10.0.0.0/24\","
+ " \"id\": 100,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"10.0.0.16 - 10.0.0.16\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}",
+
+// Configuration #3
+// - 2 classes specified
+// - 1 shared network with 2 subnets (each with class restriction)
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"client-classes\": ["
+ " {"
+ " \"name\": \"a-devices\","
+ " \"test\": \"option[93].hex == 0x0001\""
+ " },"
+ " {"
+ " \"name\": \"b-devices\","
+ " \"test\": \"option[93].hex == 0x0002\""
+ " }"
+ " ],"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"relay\": {"
+ " \"ip-address\": \"192.3.5.6\""
+ " },"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/26\","
+ " \"id\": 10,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.63 - 192.0.2.63\""
+ " }"
+ " ],"
+ " \"client-class\": \"a-devices\""
+ " },"
+ " {"
+ " \"subnet\": \"10.0.0.0/24\","
+ " \"id\": 100,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"10.0.0.16 - 10.0.0.16\""
+ " }"
+ " ],"
+ " \"client-class\": \"b-devices\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}",
+
+// Configuration #4
+// - 1 shared network with 2 subnets, each has one host reservation
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"relay\": {"
+ " \"ip-address\": \"192.3.5.6\""
+ " },"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/26\","
+ " \"id\": 10,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.1 - 192.0.2.63\""
+ " }"
+ " ],"
+ " \"reservations\": ["
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"ip-address\": \"192.0.2.28\""
+ " }"
+ " ]"
+ " },"
+ " {"
+ " \"subnet\": \"10.0.0.0/24\","
+ " \"id\": 100,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"10.0.0.1 - 10.0.0.254\""
+ " }"
+ " ],"
+ " \"reservations\": ["
+ " {"
+ " \"hw-address\": \"11:22:33:44:55:66\","
+ " \"ip-address\": \"10.0.0.29\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}",
+
+// Configuration #5
+// - 1 shared network, with 2 subnets. Each has host reservation
+// - similar to config #4, but with different hw-address reserved
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"relay\": {"
+ " \"ip-address\": \"192.3.5.6\""
+ " },"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/26\","
+ " \"id\": 10,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.1 - 192.0.2.63\""
+ " }"
+ " ],"
+ " \"reservations\": ["
+ " {"
+ " \"hw-address\": \"11:22:33:44:55:66\","
+ " \"ip-address\": \"192.0.2.28\""
+ " }"
+ " ]"
+ " },"
+ " {"
+ " \"subnet\": \"10.0.0.0/24\","
+ " \"id\": 100,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"10.0.0.1 - 10.0.0.254\""
+ " }"
+ " ],"
+ " \"reservations\": ["
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"ip-address\": \"10.0.0.29\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}",
+
+// Configuration #6
+// - 1 class
+// - 1 shared network, with 2 subnets. First has class restriction and
+// host reservation
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"client-classes\": ["
+ " {"
+ " \"name\": \"a-devices\","
+ " \"test\": \"option[93].hex == 0x0001\""
+ " }"
+ " ],"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"relay\": {"
+ " \"ip-address\": \"192.3.5.6\""
+ " },"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/26\","
+ " \"id\": 10,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.1 - 192.0.2.63\""
+ " }"
+ " ],"
+ " \"client-class\": \"a-devices\","
+ " \"reservations\": ["
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"ip-address\": \"192.0.2.28\""
+ " }"
+ " ]"
+ " },"
+ " {"
+ " \"subnet\": \"10.0.0.0/24\","
+ " \"id\": 100,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"10.0.0.16 - 10.0.0.16\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}",
+
+// Configuration #7
+// - 1 global option
+// - 1 shared network with some options and 2 subnets (the first one has extra
+// options)
+// - 1 plain subnet (that has an option)
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"log-servers\","
+ " \"data\": \"1.2.3.4\""
+ " }"
+ " ],"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"interface\": \"eth1\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"10.1.2.3\""
+ " },"
+ " {"
+ " \"name\": \"cookie-servers\","
+ " \"data\": \"10.6.5.4\""
+ " }"
+ " ],"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/26\","
+ " \"id\": 10,"
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"routers\","
+ " \"data\": \"192.0.2.5\""
+ " },"
+ " {"
+ " \"name\": \"cookie-servers\","
+ " \"data\": \"10.5.4.3\""
+ " }"
+ " ],"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.63 - 192.0.2.63\""
+ " }"
+ " ]"
+ " },"
+ " {"
+ " \"subnet\": \"10.0.0.0/24\","
+ " \"id\": 100,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"10.0.0.16 - 10.0.0.16\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ],"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.64/26\","
+ " \"id\": 1000,"
+ " \"interface\": \"eth0\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"cookie-servers\","
+ " \"data\": \"10.1.1.1\""
+ " }"
+ " ],"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.65 - 192.0.2.65\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}",
+
+// Configuration #8
+// - two shared networks, each with two subnets (each with interface specified)
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"interface\": \"eth1\","
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/26\","
+ " \"id\": 10,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.1 - 192.0.2.63\""
+ " }"
+ " ]"
+ " },"
+ " {"
+ " \"subnet\": \"192.0.2.64/26\","
+ " \"id\": 100,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.65 - 192.0.2.127\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " },"
+ " {"
+ " \"name\": \"dog\","
+ " \"interface\": \"eth0\","
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"10.0.0.0/26\","
+ " \"id\": 1000,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"10.0.0.1 - 10.0.0.63\""
+ " }"
+ " ]"
+ " },"
+ " {"
+ " \"subnet\": \"10.0.0.64/26\","
+ " \"id\": 10000,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"10.0.0.65 - 10.0.0.127\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}",
+
+// Configuration #9
+// - 2 shared networks, each with relay ip address and 2 subnets
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"relay\": { \"ip-address\": \"10.1.2.3\" },"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/26\","
+ " \"id\": 10,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.1 - 192.0.2.63\""
+ " }"
+ " ]"
+ " },"
+ " {"
+ " \"subnet\": \"192.0.2.64/26\","
+ " \"id\": 100,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.65 - 192.0.2.127\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " },"
+ " {"
+ " \"name\": \"dog\","
+ " \"relay\": { \"ip-address\": \"192.1.2.3\" },"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"10.0.0.0/26\","
+ " \"id\": 1000,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"10.0.0.1 - 10.0.0.63\""
+ " }"
+ " ]"
+ " },"
+ " {"
+ " \"subnet\": \"10.0.0.64/26\","
+ " \"id\": 10000,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"10.0.0.65 - 10.0.0.127\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}",
+// Configuration #10.
+// - 1 client class
+// - 1 shared network with two subnets (second has a host reservation)
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"client-classes\": ["
+ " {"
+ " \"name\": \"class-with-bootfile\","
+ " \"boot-file-name\": \"/dev/null\""
+ " }"
+ " ],"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"relay\": {"
+ " \"ip-address\": \"192.3.5.6\""
+ " },"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/26\","
+ " \"id\": 10,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.1 - 192.0.2.63\""
+ " }"
+ " ]"
+ " },"
+ " {"
+ " \"subnet\": \"10.0.0.0/24\","
+ " \"id\": 100,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"10.0.0.1 - 10.0.0.254\""
+ " }"
+ " ],"
+ " \"reservations\": ["
+ " {"
+ " \"hw-address\": \"11:22:33:44:55:66\","
+ " \"ip-address\": \"10.0.0.29\","
+ " \"hostname\": \"test.example.org\","
+ " \"next-server\": \"10.10.10.10\","
+ " \"client-classes\": [ \"class-with-bootfile\" ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}",
+
+// Configuration #11.
+// - global value of match-client-id set to false
+// - 1 shared network (match-client-id set to true) with 2 subnets
+// - the first subnet has match-client-id set to false
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"match-client-id\": false,"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"interface\": \"eth1\","
+ " \"match-client-id\": true,"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/26\","
+ " \"id\": 10,"
+ " \"match-client-id\": false"
+ " },"
+ " {"
+ " \"subnet\": \"192.0.2.64/26\","
+ " \"id\": 100,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.65 - 192.0.2.127\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}",
+
+// Configuration #12.
+// - global value of match-client-id set to false
+// - 1 shared network (match-client-id set to false) with 2 subnets
+// - the first subnet has match-client-id set to false
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"match-client-id\": false,"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"interface\": \"eth1\","
+ " \"match-client-id\": false,"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/26\","
+ " \"id\": 10,"
+ " \"match-client-id\": false"
+ " },"
+ " {"
+ " \"subnet\": \"192.0.2.64/26\","
+ " \"id\": 100,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.65 - 192.0.2.127\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}",
+
+// Configuration #13.
+// - 2 classes
+// - 2 shared networks, each with 1 subnet and client class restriction
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"client-classes\": ["
+ " {"
+ " \"name\": \"a-devices\","
+ " \"test\": \"option[93].hex == 0x0001\""
+ " },"
+ " {"
+ " \"name\": \"b-devices\","
+ " \"test\": \"option[93].hex == 0x0002\""
+ " }"
+ " ],"
+ " \"valid-lifetime\": 600,"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"interface\": \"eth1\","
+ " \"client-class\": \"a-devices\","
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/26\","
+ " \"id\": 10,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.63 - 192.0.2.63\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " },"
+ " {"
+ " \"name\": \"dog\","
+ " \"interface\": \"eth1\","
+ " \"client-class\": \"b-devices\","
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"10.0.0.0/26\","
+ " \"id\": 1000,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"10.0.0.63 - 10.0.0.63\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}",
+// Configuration #14
+// - 1 shared networks with 2 subnets, relay ip specified,
+// each relay has its own relay ip specified
+// - 1 "plain" subnet, relay ip specified
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"relay\": {"
+ " \"ip-address\": \"192.3.5.6\""
+ " },"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/26\","
+ " \"id\": 10,"
+ " \"relay\": {"
+ " \"ip-address\": \"192.1.1.1\""
+ " },"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.63 - 192.0.2.63\""
+ " }"
+ " ]"
+ " },"
+ " {"
+ " \"subnet\": \"10.0.0.0/24\","
+ " \"id\": 100,"
+ " \"relay\": {"
+ " \"ip-address\": \"192.2.2.2\""
+ " },"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"10.0.0.16 - 10.0.0.16\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ],"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.64/26\","
+ " \"id\": 1000,"
+ " \"relay\": {"
+ " \"ip-address\": \"192.3.3.3\""
+ " },"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.65 - 192.0.2.65\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}",
+
+// Configuration #15
+// - two shared networks, each comes with its own server identifier.
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"interface\": \"eth1\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"dhcp-server-identifier\","
+ " \"data\": \"1.2.3.4\""
+ " }"
+ " ],"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/26\","
+ " \"id\": 10,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.1 - 192.0.2.63\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " },"
+ " {"
+ " \"name\": \"dog\","
+ " \"interface\": \"eth0\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"dhcp-server-identifier\","
+ " \"data\": \"2.3.4.5\""
+ " }"
+ " ],"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"10.0.0.0/26\","
+ " \"id\": 1000,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"10.0.0.1 - 10.0.0.63\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}",
+
+// Configuration #16
+// - 1 shared network with 1 subnet and 2 pools (first pool has class restriction)
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"client-classes\": ["
+ " {"
+ " \"name\": \"a-devices\","
+ " \"test\": \"option[93].hex == 0x0001\""
+ " },"
+ " {"
+ " \"name\": \"b-devices\","
+ " \"test\": \"option[93].hex == 0x0002\""
+ " }"
+ " ],"
+ " \"valid-lifetime\": 600,"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"interface\": \"eth1\","
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/24\","
+ " \"id\": 10,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.1 - 192.0.2.63\","
+ " \"client-class\": \"a-devices\""
+ " },"
+ " {"
+ " \"pool\": \"192.0.2.100 - 192.0.2.100\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}",
+
+// Configuration #17
+// - 1 shared network with 1 subnet and 2 pools (each with class restriction)
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"client-classes\": ["
+ " {"
+ " \"name\": \"a-devices\","
+ " \"test\": \"option[93].hex == 0x0001\""
+ " },"
+ " {"
+ " \"name\": \"b-devices\","
+ " \"test\": \"option[93].hex == 0x0002\""
+ " }"
+ " ],"
+ " \"valid-lifetime\": 600,"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"interface\": \"eth1\","
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/24\","
+ " \"id\": 10,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.1 - 192.0.2.63\","
+ " \"client-class\": \"a-devices\""
+ " },"
+ " {"
+ " \"pool\": \"192.0.2.100 - 192.0.2.100\","
+ " \"client-class\": \"b-devices\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}",
+
+// Configuration #18
+// - plain subnet and 2 pools (first pool has class restriction)
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"client-classes\": ["
+ " {"
+ " \"name\": \"a-devices\","
+ " \"test\": \"option[93].hex == 0x0001\""
+ " },"
+ " {"
+ " \"name\": \"b-devices\","
+ " \"test\": \"option[93].hex == 0x0002\""
+ " }"
+ " ],"
+ " \"valid-lifetime\": 600,"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/24\","
+ " \"id\": 10,"
+ " \"interface\": \"eth1\","
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.1 - 192.0.2.63\","
+ " \"client-class\": \"a-devices\""
+ " },"
+ " {"
+ " \"pool\": \"192.0.2.100 - 192.0.2.100\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}",
+
+// Configuration #19
+// - plain subnet and 2 pools (each with class restriction)
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"client-classes\": ["
+ " {"
+ " \"name\": \"a-devices\","
+ " \"test\": \"option[93].hex == 0x0001\""
+ " },"
+ " {"
+ " \"name\": \"b-devices\","
+ " \"test\": \"option[93].hex == 0x0002\""
+ " }"
+ " ],"
+ " \"valid-lifetime\": 600,"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/24\","
+ " \"id\": 10,"
+ " \"interface\": \"eth1\","
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.1 - 192.0.2.63\","
+ " \"client-class\": \"a-devices\""
+ " },"
+ " {"
+ " \"pool\": \"192.0.2.100 - 192.0.2.100\","
+ " \"client-class\": \"b-devices\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}",
+
+// Configuration #20
+// - a shared network with two subnets
+// - first subnet has a dynamic address pool
+// - second subnet has no address pool but it has a host reservation
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"relay\": {"
+ " \"ip-address\": \"192.3.5.6\""
+ " },"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"10.0.0.0/24\","
+ " \"id\": 100,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"10.0.0.1 - 10.0.0.1\""
+ " }"
+ " ]"
+ " },"
+ " {"
+ " \"subnet\": \"192.0.2.0/26\","
+ " \"id\": 10,"
+ " \"reservations\": ["
+ " {"
+ " \"circuit-id\": \"'charter950'\","
+ " \"ip-address\": \"192.0.2.28\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}",
+
+// Configuration #21
+// - a shared network with two subnets
+// - first subnet uses the FLQ allocator
+// - second subnet uses the random allocator
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"relay\": {"
+ " \"ip-address\": \"192.3.5.6\""
+ " },"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/24\","
+ " \"id\": 100,"
+ " \"allocator\": \"flq\","
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.1 - 192.0.2.10\""
+ " }"
+ " ]"
+ " },"
+ " {"
+ " \"subnet\": \"192.0.3.0/24\","
+ " \"id\": 10,"
+ " \"allocator\": \"random\","
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.3.1 - 192.0.3.10\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}",
+// Configuration #22
+// - a shared network with two subnets
+// - first subnet uses the random allocator
+// - second subnet uses the FLQ allocator
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"relay\": {"
+ " \"ip-address\": \"192.3.5.6\""
+ " },"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/24\","
+ " \"id\": 100,"
+ " \"allocator\": \"random\","
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.1 - 192.0.2.10\""
+ " }"
+ " ]"
+ " },"
+ " {"
+ " \"subnet\": \"192.0.3.0/24\","
+ " \"id\": 10,"
+ " \"allocator\": \"flq\","
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.3.1 - 192.0.3.10\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}"
+};
+
+/// @Brief Test fixture class for DHCPv4 server using shared networks.
+class Dhcpv4SharedNetworkTest : public Dhcpv4SrvTest {
+public:
+
+ /// @brief Constructor.
+ Dhcpv4SharedNetworkTest()
+ : Dhcpv4SrvTest(),
+ iface_mgr_test_config_(true) {
+ StatsMgr::instance().removeAll();
+ }
+
+ /// @brief Specifies authoritative flag value
+ ///
+ /// Used to generate authoritative configs
+ typedef enum AuthoritativeFlag {
+ AUTH_DEFAULT, // explicit value not specified (use default)
+ AUTH_YES, // defined explicitly as yes
+ AUTH_NO // defined explicitly as no
+ } AuthoritativeFlag;
+
+ /// @brief Returns subnet having specified address in range.
+ ///
+ /// @param address Address for which subnet is being searched.
+ /// @return Pointer to the subnet having an address in range or null pointer
+ /// if no subnet found.
+ Subnet4Ptr getConfiguredSubnet(const IOAddress& address) {
+ CfgSubnets4Ptr cfg = CfgMgr::instance().getCurrentCfg()->getCfgSubnets4();
+ const Subnet4Collection* subnets = cfg->getAll();
+ for (auto subnet_it = subnets->cbegin(); subnet_it != subnets->cend();
+ ++subnet_it) {
+ if ((*subnet_it)->inRange(address)) {
+ return (*subnet_it);
+ }
+ }
+ return (Subnet4Ptr());
+ }
+
+ /// @brief Perform DORA exchange and checks the result
+ ///
+ /// This convenience method conducts DORA exchange with client
+ /// packets using hint values specified by third parameter.
+ /// The response is expected to be either ACK (ack = true) or
+ /// NAK (ack = false). The received address is checked against
+ /// exp_addr
+ ///
+ /// @param client client to perform the DORA exchange
+ /// @param exp_addr expected address (in yiaddr field)
+ /// @param hint the address the client is supposed to sent
+ /// (empty string means send 0.0.0.0)
+ /// @param ack expected response (true = ACK, false = NAK)
+ void
+ doDORA(Dhcp4Client& client, std::string exp_addr, std::string hint = "",
+ bool ack = true) {
+
+ if (hint.empty()) {
+ ASSERT_NO_THROW(client.doDORA());
+ } else {
+ boost::shared_ptr<IOAddress> addr(new IOAddress(hint));
+ ASSERT_NO_THROW(client.doDORA(addr));
+ }
+ Pkt4Ptr resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ EXPECT_EQ((ack ? DHCPACK : DHCPNAK), resp->getType());
+ if (ack) {
+ EXPECT_EQ(exp_addr, resp->getYiaddr().toText());
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(IOAddress(resp->getYiaddr()));
+ ASSERT_TRUE(lease);
+ // Make sure that the subnet id in the lease database is not messed up.
+ Subnet4Ptr subnet = getConfiguredSubnet(resp->getYiaddr());
+ ASSERT_TRUE(subnet);
+ ASSERT_EQ(subnet->getID(), lease->subnet_id_);
+
+ } else {
+ EXPECT_EQ("0.0.0.0", resp->getYiaddr().toText());
+ }
+ }
+
+ /// @brief Perform Discover/Offer exchange and checks the result
+ ///
+ /// This convenience method conducts Discover/Offer exchange with client
+ /// packets using hint values specified by third parameter.
+ /// The response is expected to be either ACK (ack = true) or
+ /// NAK (ack = false). The received address is checked against
+ /// exp_addr
+ ///
+ /// @param client client to perform the DORA exchange
+ /// @param exp_addr expected address (in yiaddr field)
+ /// @param hint the address the client is supposed to sent
+ /// (empty string means send 0.0.0.0)
+ /// @param ack expected response (true = ACK, false = NAK)
+ void
+ doDiscover(Dhcp4Client& client, std::string exp_addr, std::string hint,
+ bool ack = true) {
+
+ if (hint.empty()) {
+ ASSERT_NO_THROW(client.doDiscover());
+ } else {
+ boost::shared_ptr<IOAddress> addr(new IOAddress(hint));
+ ASSERT_NO_THROW(client.doDiscover(addr));
+ }
+ Pkt4Ptr resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ EXPECT_EQ((ack ? DHCPOFFER : DHCPNAK), resp->getType());
+ if (ack) {
+ EXPECT_EQ(exp_addr, resp->getYiaddr().toText());
+ } else {
+ EXPECT_EQ("0.0.0.0", resp->getYiaddr().toText());
+ }
+ }
+
+ /// @brief Perform Request/Reply exchange and checks the result
+ ///
+ /// This convenience method conducts Request/Reply exchange with client
+ /// packets using hint values specified by a parameter. The response is
+ /// expected to be either ACK if exp_addr has a non-empty length,
+ /// or NAK when exp_addr context is empty.
+ ///
+ /// @param client client to perform the DORA exchange
+ /// @param exp_addr expected address (in yiaddr field)
+ void
+ doRequest(Dhcp4Client& client, std::string exp_addr) {
+ ASSERT_NO_THROW(client.doRequest());
+ Pkt4Ptr resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ if (exp_addr.empty()) {
+ EXPECT_EQ(DHCPNAK, resp->getType());
+ EXPECT_EQ("0.0.0.0", resp->getYiaddr().toText());
+ } else {
+ EXPECT_EQ(DHCPACK, resp->getType());
+ EXPECT_EQ(exp_addr, resp->getYiaddr().toText());
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(IOAddress(resp->getYiaddr()));
+ ASSERT_TRUE(lease);
+ // Make sure that the subnet id in the lease database is not messed up.
+ Subnet4Ptr subnet = getConfiguredSubnet(resp->getYiaddr());
+ ASSERT_TRUE(subnet);
+ ASSERT_EQ(subnet->getID(), lease->subnet_id_);
+ }
+ }
+
+ /// @brief Verifies lease statistics against values held by StatsMgr.
+ ///
+ /// This method retrieves lease statistics from the database and then compares it
+ /// against values held by the StatsMgr. The compared statistics are number of
+ /// assigned addresses and prefixes for a subnet.
+ void verifyAssignedStats() {
+ LeaseStatsQueryPtr query = LeaseMgrFactory::instance().startLeaseStatsQuery4();
+ LeaseStatsRow row;
+ while (query->getNextRow(row)) {
+ // Only check valid leases.
+ if (row.lease_state_ == Lease::STATE_DEFAULT) {
+ ASSERT_EQ(row.state_count_, getStatsAssignedAddresses(row.subnet_id_))
+ << "test failed for subnet id " << row.subnet_id_;
+ }
+ }
+ }
+
+ /// @brief Retrieves subnet[id].assigned-addresses statistics for a subnet.
+ ///
+ /// @param subnet_id Identifier of a subnet for which statistics should be
+ /// retrieved.
+ /// @return Number of assigned addresses for a subnet.
+ int64_t getStatsAssignedAddresses(const SubnetID& subnet_id) const {
+ // Retrieve statistics name, e.g. subnet[1234].assigned-addresses.
+ const std::string stats_name = StatsMgr::generateName("subnet", subnet_id, "assigned-addresses");
+ // Top element is a map with a subnet[id].assigned-addresses parameter.
+ ConstElementPtr top_element = StatsMgr::instance().get(stats_name);
+ if (top_element && (top_element->getType() == Element::map)) {
+ // It contains two lists (nested).
+ ConstElementPtr first_list = top_element->get(stats_name);
+ if (first_list && (first_list->getType() == Element::list) &&
+ (first_list->size() > 0)) {
+ // Get the nested list which should have two elements, of which first
+ // is the statistics value we're looking for.
+ ConstElementPtr second_list = first_list->get(0);
+ if (second_list && (second_list->getType() == Element::list)) {
+ ConstElementPtr addresses_element = second_list->get(0);
+ if (addresses_element && (addresses_element->getType() == Element::integer)) {
+ return (addresses_element->intValue());
+ }
+ }
+ }
+ }
+
+ // Statistics invalid or not found.
+ return (0);
+ }
+
+ /// @brief Launches specific operation and verifies lease statistics before and
+ /// after this operation.
+ ///
+ /// @param operation Operation to be launched.
+ void testAssigned(const std::function<void()>& operation) {
+ ASSERT_NO_FATAL_FAILURE(verifyAssignedStats());
+ operation();
+ ASSERT_NO_FATAL_FAILURE(verifyAssignedStats());
+ }
+
+ /// @brief Check precedence.
+ ///
+ /// @param config the configuration.
+ /// @param ns_address expected name server address.
+ void testPrecedence(const std::string& config, const std::string& ns_address) {
+ // Create client and set MAC address to the one that has a reservation.
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ client.setIfaceName("eth1");
+ client.setIfaceIndex(ETH1_INDEX);
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+ // Request domain-name-servers.
+ client.requestOptions(DHO_DOMAIN_NAME_SERVERS);
+
+ // Create server configuration.
+ configure(config, *client.getServer());
+
+ // Perform a DORA.
+ doDORA(client, "192.0.2.28", "192.0.2.28");
+
+ // Check response.
+ Pkt4Ptr resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ EXPECT_EQ(DHCPACK, resp->getType());
+ EXPECT_EQ("192.0.2.28", resp->getYiaddr().toText());
+
+ // Check domain-name-servers option.
+ OptionPtr opt = resp->getOption(DHO_DOMAIN_NAME_SERVERS);
+ ASSERT_TRUE(opt);
+ Option4AddrLstPtr servers =
+ boost::dynamic_pointer_cast<Option4AddrLst>(opt);
+ ASSERT_TRUE(servers);
+ auto addrs = servers->getAddresses();
+ ASSERT_EQ(1, addrs.size());
+ EXPECT_EQ(ns_address, addrs[0].toText());
+ }
+
+ /// @brief returns authoritative flag as JSON string
+ /// @param f value to be specified (default, true or false)
+ string auth(AuthoritativeFlag f) {
+ switch (f) {
+ case AUTH_DEFAULT:
+ return ("");
+ case AUTH_YES:
+ return (" \"authoritative\": true,");
+ break;
+ case AUTH_NO:
+ return (" \"authoritative\": false,");
+ }
+ return ("");
+ }
+
+ /// @brief generates Config file with specified authoritative flag values
+ ///
+ /// The config file has the same structure:
+ /// - 1 shared network with 2 subnets (global authoritative flag)
+ /// - first subnet: authoritative (subnet1 flag here)
+ /// - second subnet: authoritative (subnet2 flag here)
+ ///
+ /// @param global governs presence/value of global authoritative flag
+ /// @param subnet1 governs presence/value of authoritative flag in subnet1
+ /// @param subnet2 governs presence/value of authoritative flag in subnet2
+ string generateAuthConfig(AuthoritativeFlag global, AuthoritativeFlag subnet1,
+ AuthoritativeFlag subnet2) {
+ string cfg = "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"comment\": \"example\",";
+ cfg += auth(global);
+ cfg +=
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/26\","
+ " \"id\": 10,";
+ cfg += auth(subnet1);
+
+ cfg +=
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.63 - 192.0.2.63\""
+ " }"
+ " ]"
+ " },"
+ " {"
+ " \"subnet\": \"10.0.0.0/24\","
+ " \"id\": 100,";
+ cfg += auth(subnet2);
+ cfg +=
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"10.0.0.16 - 10.0.0.16\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}";
+
+ return (cfg);
+ }
+
+ // @brief Test that different allocator types can be used within a shared network.
+ //
+ // All available addresses should be assigned from the subnets belonging to
+ // the shared network.
+ //
+ /// @param config server configuration that should contain a shared network with
+ /// two subnets. Each subnet should contain an address pool with 10 addresses.
+ void testDifferentAllocatorsInNetwork(const std::string& config) {
+ // Create the base client and server configuration.
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ configure(config, *client.getServer());
+
+ // Record what addresses have been allocated.
+ std::set<std::string> allocated_set;
+
+ // Simulate allocations from different clients.
+ for (auto i = 0; i < 20; ++i) {
+ // Create a client from the base client.
+ Dhcp4Client next_client(client.getServer(), Dhcp4Client::SELECTING);
+ next_client.useRelay(true, IOAddress("192.3.5.6"), IOAddress("10.0.0.2"));
+ // Run 4-way exchange.
+ ASSERT_NO_THROW(next_client.doDORA());
+ // Make sure that the server responded.
+ ASSERT_TRUE(next_client.getContext().response_);
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(next_client.getContext().response_->getType()));
+ // Make sure that the address is not zero.
+ ASSERT_FALSE(next_client.config_.lease_.addr_.isV4Zero());
+ // Remember allocated address uniqueness.
+ allocated_set.insert(next_client.config_.lease_.addr_.toText());
+ }
+ // Make sure that we have 20 distinct allocations.
+ ASSERT_EQ(20, allocated_set.size());
+
+ // Try one more time. This time no leases should be allocated because
+ // the pools are exhausted.
+ Dhcp4Client next_client(client.getServer(), Dhcp4Client::SELECTING);
+ next_client.useRelay(true, IOAddress("192.3.5.6"), IOAddress("10.0.0.2"));
+ ASSERT_NO_THROW(next_client.doDiscover());
+ EXPECT_FALSE(next_client.getContext().response_);
+ }
+
+ /// @brief Destructor.
+ virtual ~Dhcpv4SharedNetworkTest() {
+ StatsMgr::instance().removeAll();
+ }
+
+ /// @brief Interface Manager's fake configuration control.
+ IfaceMgrTestConfig iface_mgr_test_config_;
+};
+
+// Check user-context parsing
+TEST_F(Dhcpv4SharedNetworkTest, parse) {
+ // Create client
+ Dhcp4Client client1(Dhcp4Client::SELECTING);
+
+ // Don't use configure from utils
+ Parser4Context ctx;
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(NETWORKS_CONFIG[0], true));
+ ConstElementPtr status;
+ disableIfacesReDetect(json);
+ EXPECT_NO_THROW(status = configureDhcp4Server(*client1.getServer(), json));
+ ASSERT_TRUE(status);
+ int rcode;
+ ConstElementPtr comment = config::parseAnswer(rcode, status);
+ ASSERT_EQ(0, rcode) << " comment: " << comment->stringValue();
+ ASSERT_NO_THROW( {
+ CfgDbAccessPtr cfg_db = CfgMgr::instance().getStagingCfg()->getCfgDbAccess();
+ cfg_db->setAppendedParameters("universe=4");
+ cfg_db->createManagers();
+ } );
+ CfgMgr::instance().commit();
+
+ CfgSharedNetworks4Ptr cfg = CfgMgr::instance().getCurrentCfg()->getCfgSharedNetworks4();
+ SharedNetwork4Ptr network = cfg->getByName("frog");
+ ConstElementPtr context = network->getContext();
+ ASSERT_TRUE(context);
+ ASSERT_EQ(1, context->size());
+ ASSERT_TRUE(context->get("comment"));
+ EXPECT_EQ("\"example\"", context->get("comment")->str());
+}
+
+// Running out of addresses within a subnet in a shared network.
+TEST_F(Dhcpv4SharedNetworkTest, poolInSharedNetworkShortage) {
+ // Create client #1
+ Dhcp4Client client1(Dhcp4Client::SELECTING);
+ client1.setIfaceName("eth1");
+ client1.setIfaceIndex(ETH1_INDEX);
+
+ // Configure the server with one shared network including two subnets and
+ // one subnet outside of the shared network.
+ configure(NETWORKS_CONFIG[0], *client1.getServer());
+
+ // Client #1 requests an address in first subnet within a shared network.
+ // We'll send a hint of 192.0.2.63 and expect to get it.
+ testAssigned([this, &client1]() {
+ doDORA(client1, "192.0.2.63", "192.0.2.63");
+ });
+
+ // Client #2 The second client will request a lease and should be assigned
+ // an address from the second subnet.
+ Dhcp4Client client2(client1.getServer(), Dhcp4Client::SELECTING);
+ client2.setIfaceName("eth1");
+ client2.setIfaceIndex(ETH1_INDEX);
+ testAssigned([this, &client2]() {
+ doDORA(client2, "10.0.0.16");
+ });
+
+ // Client #3. It sends DHCPDISCOVER which should be dropped by the server because
+ // the server has no more addresses to assign.
+ Dhcp4Client client3(client1.getServer(), Dhcp4Client::SELECTING);
+ client3.setIfaceName("eth1");
+ client3.setIfaceIndex(ETH1_INDEX);
+ testAssigned([&client3]() {
+ ASSERT_NO_THROW(client3.doDiscover());
+ Pkt4Ptr resp3 = client3.getContext().response_;
+ ASSERT_FALSE(resp3);
+ });
+
+ // Client #3 should be assigned an address if subnet 3 is selected for this client.
+ client3.setIfaceName("eth0");
+ client3.setIfaceIndex(ETH0_INDEX);
+ testAssigned([this, &client3]() {
+ doDORA(client3, "192.0.2.65");
+ });
+
+ // Client #1 should be able to renew its address.
+ client1.setState(Dhcp4Client::RENEWING);
+ testAssigned([this, &client1]() {
+ doRequest(client1, "192.0.2.63");
+ });
+
+ // Client #2 should be able to renew its address.
+ client2.setState(Dhcp4Client::RENEWING);
+ testAssigned([this, &client2]() {
+ doRequest(client2, "10.0.0.16");
+ });
+}
+
+// Returning client sends 4-way exchange.
+TEST_F(Dhcpv4SharedNetworkTest, returningClientStartsOver) {
+ // Create client.
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ client.setIfaceName("eth1");
+ client.setIfaceIndex(ETH1_INDEX);
+ client.includeClientId("01:02:03:04");
+
+ // Configure the server with one shared network including two subnets and
+ // one subnet outside of the shared network.
+ configure(NETWORKS_CONFIG[0], *client.getServer());
+
+ // Client requests an address in first subnet within a shared network.
+ // We'll send a hint of 192.0.2.63 and expect to get it.
+ testAssigned([this, &client]() {
+ doDORA(client, "192.0.2.63", "192.0.2.63");
+ });
+
+
+ // The client reboots and performs 4-way exchange again without a hint.
+ // It should be assigned the same (existing) lease.
+ testAssigned([this, &client]() {
+ doDORA(client, "192.0.2.63");
+ });
+}
+
+// Shared network is selected based on giaddr value (relay specified
+// on shared network level)
+TEST_F(Dhcpv4SharedNetworkTest, sharedNetworkSelectedByRelay1) {
+ // Create client #1. This is a relayed client which is using relay
+ // address matching configured shared network.
+ Dhcp4Client client1(Dhcp4Client::SELECTING);
+ client1.useRelay(true, IOAddress("192.3.5.6"), IOAddress("10.0.0.2"));
+
+ // Configure the server with one shared network and one subnet outside of the
+ // shared network.
+ configure(NETWORKS_CONFIG[1], *client1.getServer());
+
+ // Client #1 should be assigned an address from shared network.
+ testAssigned([this, &client1] {
+ doDORA(client1, "192.0.2.63", "192.0.2.63");
+ });
+
+ // Create client #2. This is a relayed client which is using relay
+ // address matching subnet outside of the shared network.
+ Dhcp4Client client2(client1.getServer(), Dhcp4Client::SELECTING);
+ client2.useRelay(true, IOAddress("192.1.2.3"), IOAddress("10.0.0.3"));
+ testAssigned([this, &client2] {
+ doDORA(client2, "192.0.2.65", "192.0.2.63");
+ });
+}
+
+// Shared network is selected based on giaddr value (relay specified
+// on subnet in shared network level). Note the relay ip is specified
+// on the shared network level, but its value is overridden on subnet
+// level.
+TEST_F(Dhcpv4SharedNetworkTest, sharedNetworkSelectedByRelay2) {
+ // Create client #1. This is a relayed client which is using relay
+ // address matching configured subnet 1 in shared network.
+ Dhcp4Client client1(Dhcp4Client::SELECTING);
+ client1.useRelay(true, IOAddress("192.1.1.1"), IOAddress("10.0.0.2"));
+
+ // Configure the server with one shared network and one subnet outside of the
+ // shared network.
+ configure(NETWORKS_CONFIG[14], *client1.getServer());
+
+ // Client #1 should be assigned an address from shared network.
+ testAssigned([this, &client1] {
+ doDORA(client1, "192.0.2.63");
+ });
+
+ // Create client #2. This is a relayed client which is using relay
+ // address that is used for subnet 2 in the shared network.
+ Dhcp4Client client2(client1.getServer(), Dhcp4Client::SELECTING);
+ client2.useRelay(true, IOAddress("192.2.2.2"), IOAddress("10.0.0.3"));
+ testAssigned([this, &client2] {
+ doDORA(client2, "10.0.0.16");
+ });
+
+ // Create client #3. This is a relayed client which is using relay
+ // address matching subnet outside of the shared network.
+ Dhcp4Client client3(client1.getServer(), Dhcp4Client::SELECTING);
+ client3.useRelay(true, IOAddress("192.3.3.3"), IOAddress("10.0.0.4"));
+ testAssigned([this, &client3] {
+ doDORA(client3, "192.0.2.65");
+ });
+}
+
+// Providing a hint for any address belonging to a shared network.
+TEST_F(Dhcpv4SharedNetworkTest, hintWithinSharedNetwork) {
+ // Create client.
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ client.setIfaceName("eth1");
+ client.setIfaceIndex(ETH1_INDEX);
+
+ // Configure the server with one shared network including two subnets and
+ // one subnet outside of the shared network.
+ configure(NETWORKS_CONFIG[0], *client.getServer());
+
+ // Provide a hint to an existing address within first subnet. This address
+ // should be offered out of this subnet.
+ testAssigned([this, &client] {
+ doDiscover(client, "192.0.2.63", "192.0.2.63");
+ });
+
+ // Similarly, we should be offered an address from another subnet within
+ // the same shared network when we ask for it.
+ testAssigned([this, &client] {
+ doDiscover(client, "10.0.0.16", "10.0.0.16");
+ });
+
+ // Asking for an address that is not in address pool should result in getting
+ // an address from one of the subnets, but generally hard to tell from which one.
+ testAssigned([&client] {
+ ASSERT_NO_THROW(client.doDiscover(boost::shared_ptr<IOAddress>(new IOAddress("10.0.0.23"))));
+ });
+
+ Pkt4Ptr resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+
+ // We expect one of the two addresses available in this shared network.
+ EXPECT_EQ(DHCPOFFER, resp->getType());
+ if ((resp->getYiaddr() != IOAddress("10.0.0.16")) &&
+ (resp->getYiaddr() != IOAddress("192.0.2.63"))) {
+ ADD_FAILURE() << "Unexpected address offered by the server " << resp->getYiaddr();
+ }
+}
+
+// Access to a subnet within shared network is restricted by client
+// classification.
+TEST_F(Dhcpv4SharedNetworkTest, subnetInSharedNetworkSelectedByClass) {
+ // Create client #1
+ Dhcp4Client client1(Dhcp4Client::SELECTING);
+ client1.useRelay(true, IOAddress("192.3.5.6"));
+
+ // Configure the server with one shared network including two subnets in
+ // it. The access to one of the subnets is restricted by client classification.
+ configure(NETWORKS_CONFIG[2], *client1.getServer());
+
+ // Client #1 requests an address in the restricted subnet but can't be assigned
+ // this address because the client doesn't belong to a certain class.
+ testAssigned([this, &client1] {
+ doDORA(client1, "10.0.0.16", "192.0.2.63");
+ });
+
+ // Release the lease that the client has got, because we'll need this address
+ // further in the test.
+ testAssigned([&client1] {
+ ASSERT_NO_THROW(client1.doRelease());
+ });
+
+ // Add option93 which would cause the client to be classified as "a-devices".
+ OptionPtr option93(new OptionUint16(Option::V4, 93, 0x0001));
+ client1.addExtraOption(option93);
+
+ // This time, the allocation of the address provided as hint should be successful.
+ testAssigned([this, &client1] {
+ doDORA(client1, "192.0.2.63", "192.0.2.63");
+ });
+
+ // Client 2 should be assigned an address from the unrestricted subnet.
+ Dhcp4Client client2(client1.getServer(), Dhcp4Client::SELECTING);
+ client2.useRelay(true, IOAddress("192.3.5.6"));
+ client2.setIfaceName("eth1");
+ client2.setIfaceIndex(ETH1_INDEX);
+ testAssigned([this, &client2] {
+ doDORA(client2, "10.0.0.16");
+ });
+
+ // Now, let's reconfigure the server to also apply restrictions on the
+ // subnet to which client2 now belongs.
+ configure(NETWORKS_CONFIG[3], *client1.getServer());
+
+ // The client should be refused to renew the lease because it doesn't belong
+ // to "b-devices" class.
+ client2.setState(Dhcp4Client::RENEWING);
+ testAssigned([this, &client2] {
+ doRequest(client2, "");
+ });
+
+ // If we add option93 with a value matching this class, the lease should
+ // get renewed.
+ OptionPtr option93_bis(new OptionUint16(Option::V4, 93, 0x0002));
+ client2.addExtraOption(option93_bis);
+
+ testAssigned([this, &client2] {
+ doRequest(client2, "10.0.0.16");
+ });
+}
+
+// IPv4 address reservation exists in one of the subnets within
+// shared network. This test also verifies that conflict resolution for
+// reserved addresses is working properly in case of shared networks.
+TEST_F(Dhcpv4SharedNetworkTest, reservationInSharedNetwork) {
+ // Create client #1. Explicitly set client's MAC address to the one that
+ // has a reservation in the first subnet within shared network.
+ Dhcp4Client client1(Dhcp4Client::SELECTING);
+ client1.useRelay(true, IOAddress("192.3.5.6"));
+ client1.setHWAddress("11:22:33:44:55:66");
+
+ // Create server configuration with a shared network including two subnets. There
+ // is an IP address reservation in each subnet for two respective clients.
+ configure(NETWORKS_CONFIG[4], *client1.getServer());
+
+ // Client #1 should get his reserved address from the second subnet.
+ testAssigned([this, &client1] {
+ doDORA(client1, "10.0.0.29", "192.0.2.28");
+ });
+
+ // Create client #2
+ Dhcp4Client client2(client1.getServer(), Dhcp4Client::SELECTING);
+ client2.useRelay(true, IOAddress("192.3.5.6"));
+ client2.setHWAddress("aa:bb:cc:dd:ee:ff");
+
+ // Client #2 should get its reserved address from the first subnet.
+ testAssigned([this, &client2] {
+ doDORA(client2, "192.0.2.28");
+ });
+
+ // Reconfigure the server. Now, the first client gets second client's
+ // reservation and vice versa.
+ configure(NETWORKS_CONFIG[5], *client1.getServer());
+
+ // The first client is trying to renew the lease and should get a DHCPNAK.
+ client1.setState(Dhcp4Client::RENEWING);
+ testAssigned([this, &client1] {
+ doRequest(client1, "");
+ });
+
+ // Similarly, the second client is trying to renew the lease and should
+ // get a DHCPNAK.
+ client2.setState(Dhcp4Client::RENEWING);
+ testAssigned([this, &client2] {
+ doRequest(client2, "");
+ });
+
+ // But the client should get a lease, if it does 4-way exchange. However, it
+ // must not get any of the reserved addresses because one of them is reserved
+ // for another client and for another one there is a valid lease.
+ client1.setState(Dhcp4Client::SELECTING);
+ testAssigned([this, &client1] {
+ ASSERT_NO_THROW(doDORA(client1, "192.0.2.29", "192.0.2.29"));
+ });
+ Pkt4Ptr resp1 = client1.getContext().response_;
+ ASSERT_TRUE(resp1);
+ EXPECT_EQ(DHCPACK, resp1->getType());
+ EXPECT_NE("10.0.0.29", resp1->getYiaddr().toText());
+ EXPECT_NE("192.0.2.28", resp1->getYiaddr().toText());
+ // Ensure stats are being recorded for HR conflicts
+ ObservationPtr subnet_conflicts = StatsMgr::instance().getObservation(
+ "subnet[10].v4-reservation-conflicts");
+ ASSERT_TRUE(subnet_conflicts);
+ ASSERT_EQ(1, subnet_conflicts->getInteger().first);
+ subnet_conflicts = StatsMgr::instance().getObservation("v4-reservation-conflicts");
+ ASSERT_TRUE(subnet_conflicts);
+ ASSERT_EQ(1, subnet_conflicts->getInteger().first);
+
+ // Client #2 is now doing 4-way exchange and should get its newly reserved
+ // address, released by the 4-way transaction of client 1.
+ client2.setState(Dhcp4Client::SELECTING);
+ testAssigned([this, &client2] {
+ doDORA(client2, "10.0.0.29");
+ });
+
+ // Same for client #1.
+ client1.setState(Dhcp4Client::SELECTING);
+ testAssigned([this, &client1] {
+ doDORA(client1, "192.0.2.28");
+ });
+}
+
+// Two clients use the same circuit ID and this circuit ID is used to assign a
+// host reservation. The clients compete for the reservation, and one of them
+// gets it and the other one gets an address from the dynamic pool. This test
+// checks that the assigned leases have appropriate subnet IDs. Previously, the
+// client getting the lease from the dynamic pool had a wrong subnet ID (not
+// matching the address from the dynamic pool).
+TEST_F(Dhcpv4SharedNetworkTest, reservationInSharedNetworkTwoClientsSameIdentifier) {
+ // Create client #1 which uses a circuit ID as a host identifier.
+ Dhcp4Client client1(Dhcp4Client::SELECTING);
+ client1.useRelay(true, IOAddress("192.3.5.6"));
+ client1.includeClientId("01:02:03:04");
+ client1.setCircuitId("charter950");
+
+ // Create server configuration with a shared network including two subnets.
+ // One of the subnets includes a reservation identified by the client's
+ // circuit ID.
+ configure(NETWORKS_CONFIG[20], *client1.getServer());
+
+ // Client #1 should get the reserved address.
+ testAssigned([this, &client1] {
+ doDORA(client1, "192.0.2.28", "");
+ });
+
+ // Create client #2 with the same circuit ID.
+ Dhcp4Client client2(client1.getServer(), Dhcp4Client::SELECTING);
+ client2.useRelay(true, IOAddress("192.3.5.6"));
+ client2.includeClientId("02:03:04:05");
+ client2.setCircuitId("charter950");
+
+ // Client #2 presents the same circuit ID but the reserved address has been
+ // already allocated. The client should get an address from the dynamic pool.
+ testAssigned([this, &client2] {
+ doDORA(client2, "10.0.0.1", "192.0.2.28");
+ });
+
+ // Client #1 renews the lease.
+ client1.setState(Dhcp4Client::RENEWING);
+ testAssigned([this, &client1]() {
+ doRequest(client1, "192.0.2.28");
+ });
+
+ // Client #2 renews the lease.
+ client2.setState(Dhcp4Client::RENEWING);
+ // Check if the client successfully renewed its address and that the
+ // subnet id in the renewed lease is not messed up.
+ testAssigned([this, &client2]() {
+ doRequest(client2, "10.0.0.1");
+ });
+
+ // Ensure stats are being recorded for HR conflicts
+ ObservationPtr subnet_conflicts = StatsMgr::instance().getObservation(
+ "subnet[10].v4-reservation-conflicts");
+ ASSERT_TRUE(subnet_conflicts);
+ ASSERT_EQ(1, subnet_conflicts->getInteger().first);
+ subnet_conflicts = StatsMgr::instance().getObservation("v4-reservation-conflicts");
+ ASSERT_TRUE(subnet_conflicts);
+ ASSERT_EQ(1, subnet_conflicts->getInteger().first);
+}
+
+// Reserved address can't be assigned as long as access to a subnet is
+// restricted by classification.
+TEST_F(Dhcpv4SharedNetworkTest, reservationAccessRestrictedByClass) {
+ // Create a client and set explicit MAC address for which there is a reservation
+ // in first subnet belonging to a shared network.
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ client.useRelay(true, IOAddress("192.3.5.6"));
+ client.setHWAddress("aa:bb:cc:dd:ee:ff");
+
+ // Create configuration with a shared network including two subnets. Access to
+ // one of the subnets is restricted by client classification.
+ configure(NETWORKS_CONFIG[6], *client.getServer());
+
+ // Assigned address should be allocated from the second subnet, because the
+ // client doesn't belong to the "a-devices" class.
+ testAssigned([this, &client] {
+ doDORA(client, "10.0.0.16");
+ });
+
+ // Add option 93 which would cause the client to be classified as "a-devices".
+ OptionPtr option93(new OptionUint16(Option::V4, 93, 0x0001));
+ client.addExtraOption(option93);
+
+ // Client renews its lease and should get DHCPNAK because this client now belongs
+ // to the "a-devices" class and can be assigned a reserved address instead.
+ client.setState(Dhcp4Client::RENEWING);
+ testAssigned([this, &client] {
+ doRequest(client, "");
+ });
+
+ // Perform 4-way exchange again. It should be assigned a reserved address this time.
+ client.setState(Dhcp4Client::SELECTING);
+ testAssigned([this, &client] {
+ doDORA(client, "192.0.2.28");
+ });
+}
+
+// Some options are specified on the shared subnet level, some on the
+// subnets level.
+TEST_F(Dhcpv4SharedNetworkTest, optionsDerivation) {
+ // Client #1.
+ Dhcp4Client client1(Dhcp4Client::SELECTING);
+ client1.setIfaceName("eth1");
+ client1.setIfaceIndex(ETH1_INDEX);
+ client1.requestOptions(DHO_LOG_SERVERS, DHO_COOKIE_SERVERS, DHO_DOMAIN_NAME_SERVERS);
+
+ configure(NETWORKS_CONFIG[7], *client1.getServer());
+
+ // Client #1 belongs to shared network. By providing a hint "192.0.2.63" we force
+ // the server to select first subnet within the shared network for this client.
+ doDORA(client1, "192.0.2.63", "192.0.2.63");
+
+ // This option is specified at the global level.
+ ASSERT_EQ(1, client1.config_.log_servers_.size());
+ EXPECT_EQ("1.2.3.4", client1.config_.log_servers_[0].toText());
+
+ // This option is specified on the subnet level.
+ ASSERT_EQ(1, client1.config_.routers_.size());
+ EXPECT_EQ("192.0.2.5", client1.config_.routers_[0].toText());
+
+ // This option is specified on the shared network level and the subnet level.
+ // The instance on the subnet level should take precedence.
+ ASSERT_EQ(1, client1.config_.quotes_servers_.size());
+ EXPECT_EQ("10.5.4.3", client1.config_.quotes_servers_[0].toText());
+
+ // This option is only specified on the shared network level and should be
+ // inherited by all subnets within this network.
+ ASSERT_EQ(1, client1.config_.dns_servers_.size());
+ EXPECT_EQ("10.1.2.3", client1.config_.dns_servers_[0].toText());
+
+ // Client #2.
+ Dhcp4Client client2(Dhcp4Client::SELECTING);
+ client2.setIfaceName("eth1");
+ client2.setIfaceIndex(ETH1_INDEX);
+ client2.requestOptions(DHO_LOG_SERVERS, DHO_COOKIE_SERVERS, DHO_DOMAIN_NAME_SERVERS);
+
+ // Request an address from the second subnet within the shared network.
+ doDORA(client2, "10.0.0.16", "10.0.0.16");
+
+ // This option is specified at the global level.
+ ASSERT_EQ(1, client2.config_.log_servers_.size());
+ EXPECT_EQ("1.2.3.4", client2.config_.log_servers_[0].toText());
+
+ // This option is only specified on the shared network level and should be
+ // inherited by all subnets within this network.
+ ASSERT_EQ(1, client2.config_.quotes_servers_.size());
+ EXPECT_EQ("10.6.5.4", client2.config_.quotes_servers_[0].toText());
+
+ // This option is only specified on the shared network level and should be
+ // inherited by all subnets within this network.
+ ASSERT_EQ(1, client2.config_.dns_servers_.size());
+ EXPECT_EQ("10.1.2.3", client2.config_.dns_servers_[0].toText());
+
+ // Client #3.
+ Dhcp4Client client3(Dhcp4Client::SELECTING);
+ client3.setIfaceName("eth0");
+ client3.setIfaceIndex(ETH0_INDEX);
+ client3.requestOptions(DHO_LOG_SERVERS, DHO_COOKIE_SERVERS, DHO_DOMAIN_NAME_SERVERS);
+
+ // Client 3 should get an address from the subnet defined outside of the shared network.
+ doDORA(client3, "192.0.2.65");
+
+ // This option is specified at the global level.
+ ASSERT_EQ(1, client3.config_.log_servers_.size());
+ EXPECT_EQ("1.2.3.4", client3.config_.log_servers_[0].toText());
+
+ // This option is specified on the subnet level.
+ ASSERT_EQ(1, client3.config_.quotes_servers_.size());
+ EXPECT_EQ("10.1.1.1", client3.config_.quotes_servers_[0].toText());
+
+ // This option is only specified on the shared network level and thus it should
+ // not be returned to this client, because the client doesn't belong to the
+ // shared network.
+ ASSERT_EQ(0, client3.config_.dns_servers_.size());
+}
+
+// Client has a lease in a subnet within shared network.
+TEST_F(Dhcpv4SharedNetworkTest, initReboot) {
+ // Create client #1.
+ Dhcp4Client client1(Dhcp4Client::SELECTING);
+ client1.setIfaceName("eth1");
+ client1.setIfaceIndex(ETH1_INDEX);
+
+ configure(NETWORKS_CONFIG[0], *client1.getServer());
+
+ // Perform 4-way exchange to obtain a lease. The client should get the lease from
+ // the second subnet.
+ testAssigned([this, &client1] {
+ doDORA(client1, "10.0.0.16", "10.0.0.16");
+ });
+
+ // The client1 transitions to INIT-REBOOT state in which the client1 remembers the
+ // lease and sends DHCPREQUEST to all servers (server id) is not specified. If
+ // the server doesn't know the client1 (doesn't have its lease), it should
+ // drop the request. We want to make sure that the server responds (resp1) regardless
+ // of the subnet from which the lease has been allocated.
+ client1.setState(Dhcp4Client::INIT_REBOOT);
+ testAssigned([this, &client1] {
+ doRequest(client1, "10.0.0.16");
+ });
+
+ // Create client #2.
+ Dhcp4Client client2(client1.getServer(), Dhcp4Client::SELECTING);
+ client2.setIfaceName("eth1");
+ client2.setIfaceIndex(ETH1_INDEX);
+
+ // Let's make sure that the behavior is the same for the other subnet within the
+ // same shared network.
+ testAssigned([this, &client2] {
+ doDORA(client2, "192.0.2.63", "192.0.2.63");
+ });
+
+ // The client2 transitions to INIT-REBOOT state in which the client2 remembers the
+ // lease and sends DHCPREQUEST to all servers (server id) is not specified. If
+ // the server doesn't know the client2 (doesn't have its lease), it should
+ // drop the request. We want to make sure that the server responds (resp2) regardless
+ // of the subnet from which the lease has been allocated.
+ client2.setState(Dhcp4Client::INIT_REBOOT);
+ testAssigned([this, &client2] {
+ doRequest(client2, "192.0.2.63");
+ });
+}
+
+// Host reservations include hostname, next server and client class.
+TEST_F(Dhcpv4SharedNetworkTest, variousFieldsInReservation) {
+ // Create client.
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ client.setIfaceName("eth1");
+ client.setIfaceIndex(ETH1_INDEX);
+ client.setHWAddress("11:22:33:44:55:66");
+
+ // Include hostname to force the server to return hostname to
+ // the client.
+ client.includeHostname("my.example.org");
+
+ // Configure the server with a shared network including two subnets.
+ // The client has address/hostname reservation in the second subnet.
+ configure(NETWORKS_CONFIG[10], *client.getServer());
+
+ // Perform 4-way exchange.
+ testAssigned([&client] {
+ ASSERT_NO_THROW(client.doDORA());
+ });
+ Pkt4Ptr resp = client.getContext().response_;
+ ASSERT_TRUE(resp);
+ EXPECT_EQ(DHCPACK, resp->getType());
+ EXPECT_EQ("10.0.0.29", resp->getYiaddr().toText());
+
+ // The client should get a hostname from the reservation, rather than
+ // the hostname it has sent to the server. If there is a logic error,
+ // the server would use the first subnet from the shared network to
+ // assign the hostname. This subnet has no reservation so it would
+ // return the same hostname that the client has sent. We expect
+ // that the hostname being sent is the one that is incldued in the
+ // reservations.
+ OptionStringPtr hostname;
+ hostname = boost::dynamic_pointer_cast<OptionString>(resp->getOption(DHO_HOST_NAME));
+ ASSERT_TRUE(hostname);
+ EXPECT_EQ("test.example.org", hostname->getValue());
+
+ // The next server value should also be set according to the settings
+ // in host reservations.
+ EXPECT_EQ("10.10.10.10", resp->getSiaddr().toText());
+
+ // The boot-file-name value should be derived from the client class
+ // based on the static class reservations.
+ const std::string expected_fname = "/dev/null";
+ const OptionBuffer fname = resp->getFile();
+ const std::string converted_fname(fname.cbegin(),
+ fname.cbegin() + expected_fname.size());
+ EXPECT_EQ(expected_fname, converted_fname);
+}
+
+// Different shared network is selected for different local interface.
+TEST_F(Dhcpv4SharedNetworkTest, sharedNetworkSelectionByInterface) {
+ // Create client #1. The server receives requests from this client
+ // via interface eth1 and should assign shared network "frog" for
+ // this client.
+ Dhcp4Client client1(Dhcp4Client::SELECTING);
+ client1.setIfaceName("eth1");
+ client1.setIfaceIndex(ETH1_INDEX);
+
+ // Create server configuration with two shared networks selected
+ // by the local interface: eth1 and eth0.
+ configure(NETWORKS_CONFIG[8], *client1.getServer());
+
+ // Perform 4-way exchange.
+ testAssigned([&client1] {
+ ASSERT_NO_THROW(client1.doDORA());
+ });
+ Pkt4Ptr resp1 = client1.getContext().response_;
+ ASSERT_TRUE(resp1);
+ EXPECT_EQ(DHCPACK, resp1->getType());
+ // The client should be assigned an address from the 192.0.2.X
+ // address range.
+ EXPECT_EQ("192.0.2", resp1->getYiaddr().toText().substr(0, 7));
+
+ // Create client #2 which requests are received on eth0.
+ Dhcp4Client client2(client1.getServer(), Dhcp4Client::SELECTING);
+ client2.setIfaceName("eth0");
+ client2.setIfaceIndex(ETH0_INDEX);
+
+ // Perform 4-way exchange.
+ testAssigned([&client2] {
+ ASSERT_NO_THROW(client2.doDORA());
+ });
+ Pkt4Ptr resp2 = client2.getContext().response_;
+ ASSERT_TRUE(resp2);
+ EXPECT_EQ(DHCPACK, resp2->getType());
+ // The client should be assigned an address from the 10.0.0.X
+ // address range.
+ EXPECT_EQ("10.0.0", resp2->getYiaddr().toText().substr(0, 6));
+}
+
+// Different shared network is selected for different relay address.
+TEST_F(Dhcpv4SharedNetworkTest, sharedNetworkSelectionByRelay) {
+ // Create relayed client #1.
+ Dhcp4Client client1(Dhcp4Client::SELECTING);
+ client1.useRelay(true, IOAddress("10.1.2.3"));
+
+ // Create server configuration with two shared networks selected
+ // by the relay address.
+ configure(NETWORKS_CONFIG[9], *client1.getServer());
+
+ // Perform 4-way exchange.
+ testAssigned([&client1] {
+ ASSERT_NO_THROW(client1.doDORA());
+ });
+ Pkt4Ptr resp1 = client1.getContext().response_;
+ ASSERT_TRUE(resp1);
+ EXPECT_EQ(DHCPACK, resp1->getType());
+ // The client should be assigned an address from the 192.0.2.X
+ // address range.
+ EXPECT_EQ("192.0.2", resp1->getYiaddr().toText().substr(0, 7));
+
+ // Create relayed client #2.
+ Dhcp4Client client2(client1.getServer(), Dhcp4Client::SELECTING);
+ client2.useRelay(true, IOAddress("192.1.2.3"));
+
+ // Perform 4-way exchange.
+ testAssigned([&client2] {
+ ASSERT_NO_THROW(client2.doDORA());
+ });
+ Pkt4Ptr resp2 = client2.getContext().response_;
+ ASSERT_TRUE(resp2);
+ EXPECT_EQ(DHCPACK, resp2->getType());
+ // The client should be assigned an address from the 10.0.0.X
+ // address range.
+ EXPECT_EQ("10.0.0", resp2->getYiaddr().toText().substr(0, 6));
+}
+
+// Client id matching gets disabled on the shared network level.
+TEST_F(Dhcpv4SharedNetworkTest, matchClientId) {
+ // Create client using client identifier besides MAC address.
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+ client.includeClientId("01:02:03:04");
+ client.setIfaceName("eth1");
+ client.setIfaceIndex(ETH1_INDEX);
+
+ // Create server configuration with match-client-id value initially
+ // set to true. The client should be allocated a lease and the
+ // client identifier should be included in this lease.
+ configure(NETWORKS_CONFIG[11], *client.getServer());
+
+ // Perform 4-way exchange.
+ testAssigned([&client] {
+ ASSERT_NO_THROW(client.doDORA());
+ });
+ Pkt4Ptr resp1 = client.getContext().response_;
+ ASSERT_TRUE(resp1);
+ ASSERT_EQ(DHCPACK, resp1->getType());
+
+ // Reconfigure the server and turn off client identifier matching
+ // on the shared network level. The subnet from which the client
+ // is allocated an address should derive the match-client-id value
+ // and ignore the fact that the client identifier is not matching.
+ configure(NETWORKS_CONFIG[12], *client.getServer());
+
+ client.includeClientId("01:01:01:01");
+ client.setState(Dhcp4Client::RENEWING);
+
+ // Try to renew the lease with modified MAC address.
+ testAssigned([&client] {
+ ASSERT_NO_THROW(client.doRequest());
+ });
+ Pkt4Ptr resp2 = client.getContext().response_;
+ ASSERT_TRUE(resp2);
+ ASSERT_EQ(DHCPACK, resp2->getType());
+
+ // The lease should get renewed.
+ EXPECT_EQ(resp2->getYiaddr().toText(), resp1->getYiaddr().toText());
+}
+
+// Shared network is selected based on the client class specified.
+TEST_F(Dhcpv4SharedNetworkTest, sharedNetworkSelectedByClass) {
+ // Create client #1.
+ Dhcp4Client client1(Dhcp4Client::SELECTING);
+ client1.setIfaceName("eth1");
+ client1.setIfaceIndex(ETH1_INDEX);
+
+ // Add option93 which would cause the client1 to be classified as "b-devices".
+ OptionPtr option93(new OptionUint16(Option::V4, 93, 0x0002));
+ client1.addExtraOption(option93);
+
+ // Configure the server with two shared networks which can be accessed
+ // by clients belonging to "a-devices" and "b-devices" classes
+ // respectively.
+ configure(NETWORKS_CONFIG[13], *client1.getServer());
+
+ // Simply send DHCPDISCOVER to avoid allocating a lease.
+ testAssigned([&client1] {
+ ASSERT_NO_THROW(client1.doDiscover());
+ });
+ Pkt4Ptr resp1 = client1.getContext().response_;
+ ASSERT_TRUE(resp1);
+ ASSERT_EQ(DHCPOFFER, resp1->getType());
+ // The client should be offered a lease from the second shared network.
+ EXPECT_EQ("10.0.0.63", resp1->getYiaddr().toText());
+
+ // Create another client which will belong to a different class.
+ Dhcp4Client client2(client1.getServer(), Dhcp4Client::SELECTING);
+ client2.setIfaceName("eth1");
+ client2.setIfaceIndex(ETH1_INDEX);
+
+ // Add option93 which would cause the client1 to be classified as "a-devices".
+ option93.reset(new OptionUint16(Option::V4, 93, 0x0001));
+ client2.addExtraOption(option93);
+
+ // Send DHCPDISCOVER. There is no lease in the lease database so the
+ // client should be offered a lease based on the client class selection.
+ testAssigned([this, &client2] {
+ doDiscover(client2, "192.0.2.63", "");
+ });
+}
+
+// This test verifies that custom server identifier can be specified for a
+// shared network.
+TEST_F(Dhcpv4SharedNetworkTest, customServerIdentifier) {
+ Dhcp4Client client1(Dhcp4Client::SELECTING);
+ client1.setIfaceName("eth1");
+ client1.setIfaceIndex(ETH1_INDEX);
+
+ // Configure DHCP server.
+ ASSERT_NO_THROW(configure(NETWORKS_CONFIG[15], *client1.getServer()));
+
+ testAssigned([&client1] {
+ ASSERT_NO_THROW(client1.doDORA());
+ });
+
+ // Make sure that the server responded.
+ ASSERT_TRUE(client1.getContext().response_);
+ Pkt4Ptr resp = client1.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // The explicitly configured server identifier should take precedence
+ // over generated server identifier.
+ EXPECT_EQ("1.2.3.4", client1.config_.serverid_.toText());
+
+ // Create another client using different interface.
+ Dhcp4Client client2(client1.getServer(), Dhcp4Client::SELECTING);
+ client2.setIfaceName("eth0");
+ client2.setIfaceIndex(ETH0_INDEX);
+
+ testAssigned([&client2] {
+ ASSERT_NO_THROW(client2.doDORA());
+ });
+
+ // Make sure that the server responded.
+ ASSERT_TRUE(client2.getContext().response_);
+ resp = client2.getContext().response_;
+ // Make sure that the server has responded with DHCPACK.
+ ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+ // The explicitly configured server identifier should take precedence
+ // over generated server identifier.
+ EXPECT_EQ("2.3.4.5", client2.config_.serverid_.toText());
+}
+
+// Access to a pool within shared network is restricted by client
+// classification.
+TEST_F(Dhcpv4SharedNetworkTest, poolInSharedNetworkSelectedByClass) {
+ // Create client #1
+ Dhcp4Client client1(Dhcp4Client::SELECTING);
+ client1.setIfaceName("eth1");
+ client1.setIfaceIndex(ETH1_INDEX);
+
+ // Configure the server with one shared network including one subnet and
+ // in 2 pools in it. The access to one of the pools is restricted
+ // by client classification.
+ configure(NETWORKS_CONFIG[16], *client1.getServer());
+
+ // Client #1 requests an address in the restricted pool but can't be assigned
+ // this address because the client doesn't belong to a certain class.
+ testAssigned([this, &client1] {
+ doDORA(client1, "192.0.2.100", "192.0.2.63");
+ });
+
+ // Release the lease that the client has got, because we'll need this address
+ // further in the test.
+ testAssigned([&client1] {
+ ASSERT_NO_THROW(client1.doRelease());
+ });
+
+ // Add option93 which would cause the client to be classified as "a-devices".
+ OptionPtr option93(new OptionUint16(Option::V4, 93, 0x0001));
+ client1.addExtraOption(option93);
+
+ // This time, the allocation of the address provided as hint should be successful.
+ testAssigned([this, &client1] {
+ doDORA(client1, "192.0.2.63", "192.0.2.63");
+ });
+
+ // Client 2 should be assigned an address from the unrestricted pool.
+ Dhcp4Client client2(client1.getServer(), Dhcp4Client::SELECTING);
+ client2.setIfaceName("eth1");
+ client2.setIfaceIndex(ETH1_INDEX);
+ testAssigned([this, &client2] {
+ doDORA(client2, "192.0.2.100");
+ });
+
+ // Now, let's reconfigure the server to also apply restrictions on the
+ // pool to which client2 now belongs.
+ configure(NETWORKS_CONFIG[17], *client1.getServer());
+
+ // The client should be refused to renew the lease because it doesn't belong
+ // to "b-devices" class.
+ client2.setState(Dhcp4Client::RENEWING);
+ testAssigned([this, &client2] {
+ doRequest(client2, "");
+ });
+
+ // If we add option93 with a value matching this class, the lease should
+ // get renewed.
+ OptionPtr option93_bis(new OptionUint16(Option::V4, 93, 0x0002));
+ client2.addExtraOption(option93_bis);
+
+ testAssigned([this, &client2] {
+ doRequest(client2, "192.0.2.100");
+ });
+}
+
+// Access to a pool within plain subnet is restricted by client classification.
+TEST_F(Dhcpv4SharedNetworkTest, poolInSubnetSelectedByClass) {
+ // Create client #1
+ Dhcp4Client client1(Dhcp4Client::SELECTING);
+ client1.setIfaceName("eth1");
+ client1.setIfaceIndex(ETH1_INDEX);
+
+ // Configure the server with one plain subnet including two pools.
+ // The access to one of the pools is restricted by client classification.
+ configure(NETWORKS_CONFIG[18], *client1.getServer());
+
+ // Client #1 requests an address in the restricted pool but can't be assigned
+ // this address because the client doesn't belong to a certain class.
+ testAssigned([this, &client1] {
+ doDORA(client1, "192.0.2.100", "192.0.2.63");
+ });
+
+ // Release the lease that the client has got, because we'll need this address
+ // further in the test.
+ testAssigned([&client1] {
+ ASSERT_NO_THROW(client1.doRelease());
+ });
+
+ // Add option93 which would cause the client to be classified as "a-devices".
+ OptionPtr option93(new OptionUint16(Option::V4, 93, 0x0001));
+ client1.addExtraOption(option93);
+
+ // This time, the allocation of the address provided as hint should be successful.
+ testAssigned([this, &client1] {
+ doDORA(client1, "192.0.2.63", "192.0.2.63");
+ });
+
+ // Client 2 should be assigned an address from the unrestricted pool.
+ Dhcp4Client client2(client1.getServer(), Dhcp4Client::SELECTING);
+ client2.setIfaceName("eth1");
+ client2.setIfaceIndex(ETH1_INDEX);
+ testAssigned([this, &client2] {
+ doDORA(client2, "192.0.2.100");
+ });
+
+ // Now, let's reconfigure the server to also apply restrictions on the
+ // pool to which client2 now belongs.
+ configure(NETWORKS_CONFIG[19], *client1.getServer());
+
+ // The client should be refused to renew the lease because it doesn't belong
+ // to "b-devices" class.
+ client2.setState(Dhcp4Client::RENEWING);
+ testAssigned([this, &client2] {
+ doRequest(client2, "");
+ });
+
+ // If we add option93 with a value matching this class, the lease should
+ // get renewed.
+ OptionPtr option93_bis(new OptionUint16(Option::V4, 93, 0x0002));
+ client2.addExtraOption(option93_bis);
+
+ testAssigned([this, &client2] {
+ doRequest(client2, "192.0.2.100");
+ });
+}
+
+// Shared network is selected based on giaddr value (relay specified
+// on shared network level, but response is send to source address.
+TEST_F(Dhcpv4SharedNetworkTest, sharedNetworkSendToSourceTestingModeEnabled) {
+ // Create client #1. This is a relayed client which is using relay
+ // address matching configured shared network.
+ // Source address is set to unrelated to configuration.
+
+ Dhcp4Client client1(Dhcp4Client::SELECTING);
+ // Put Kea into testing mode.
+ client1.getServer()->setSendResponsesToSource(true);
+ client1.useRelay(true, IOAddress("192.3.5.6"), IOAddress("1.1.1.2"));
+ // Configure the server with one shared network and one subnet outside of the
+ // shared network.
+ configure(NETWORKS_CONFIG[1], *client1.getServer());
+ // Client #1 should be assigned an address from shared network.
+ testAssigned([this, &client1] {
+ doDORA(client1, "192.0.2.63", "192.0.2.63");
+ });
+
+ // normally Kea would send packet to 192.3.5.6 but we want it get from
+ // 1.1.1.2 in send to source testing mode but still with correctly
+ // assigned address.
+ Pkt4Ptr resp1 = client1.getContext().response_;
+ EXPECT_EQ("1.1.1.2", resp1->getLocalAddr().toText());
+
+ // Create client #2. This is a relayed client which is using relay
+ // address matching subnet outside of the shared network.
+ Dhcp4Client client2(client1.getServer(), Dhcp4Client::SELECTING);
+ client2.useRelay(true, IOAddress("192.1.2.3"), IOAddress("2.2.2.3"));
+ testAssigned([this, &client2] {
+ doDORA(client2, "192.0.2.65", "192.0.2.63");
+ });
+
+ Pkt4Ptr resp2 = client2.getContext().response_;
+ EXPECT_EQ("2.2.2.3", resp2->getLocalAddr().toText());
+ // reset testing mode.
+ client1.getServer()->setSendResponsesToSource(false);
+}
+
+// Verify option processing precedence
+// Order is global < class < shared-network < subnet < pool < host reservation
+TEST_F(Dhcpv4SharedNetworkTest, precedenceGlobal) {
+ const std::string config =
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.1\""
+ " }"
+ " ],"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"interface\": \"eth1\","
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/26\","
+ " \"id\": 10,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.1 - 192.0.2.63\""
+ " }"
+ " ],"
+ " \"reservations\": ["
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"ip-address\": \"192.0.2.28\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}";
+
+ testPrecedence(config, "192.0.2.1");
+}
+
+// Verify option processing precedence
+// Order is global < class < shared-network < subnet < pool < host reservation
+TEST_F(Dhcpv4SharedNetworkTest, precedenceClass) {
+ const std::string config =
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.1\""
+ " }"
+ " ],"
+ " \"client-classes\": ["
+ " {"
+ " \"name\": \"alpha\","
+ " \"test\": \"'' == ''\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.2\""
+ " }"
+ " ]"
+ " }"
+ " ],"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"interface\": \"eth1\","
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/26\","
+ " \"id\": 10,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.1 - 192.0.2.63\""
+ " }"
+ " ],"
+ " \"reservations\": ["
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"ip-address\": \"192.0.2.28\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}";
+
+ testPrecedence(config, "192.0.2.2");
+}
+
+// Verify option processing precedence
+// Order is global < class < shared-network < subnet < pool < host reservation
+TEST_F(Dhcpv4SharedNetworkTest, precedenceClasses) {
+ const std::string config =
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.1\""
+ " }"
+ " ],"
+ " \"client-classes\": ["
+ " {"
+ " \"name\": \"beta\","
+ " \"test\": \"'' == ''\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.2\""
+ " }"
+ " ]"
+ " },"
+ " {"
+ " \"name\": \"alpha\","
+ " \"test\": \"'' == ''\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.3\""
+ " }"
+ " ]"
+ " }"
+ " ],"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"interface\": \"eth1\","
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/26\","
+ " \"id\": 10,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.1 - 192.0.2.63\""
+ " }"
+ " ],"
+ " \"reservations\": ["
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"ip-address\": \"192.0.2.28\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}";
+
+ // Class order is the insert order
+ testPrecedence(config, "192.0.2.2");
+}
+
+// Verify option processing precedence
+// Order is global < class < shared-network < subnet < pool < host reservation
+TEST_F(Dhcpv4SharedNetworkTest, precedenceNetwork) {
+ const std::string config =
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.1\""
+ " }"
+ " ],"
+ " \"client-classes\": ["
+ " {"
+ " \"name\": \"alpha\","
+ " \"test\": \"'' == ''\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.2\""
+ " }"
+ " ]"
+ " }"
+ " ],"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"interface\": \"eth1\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.3\""
+ " }"
+ " ],"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/26\","
+ " \"id\": 10,"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.1 - 192.0.2.63\""
+ " }"
+ " ],"
+ " \"reservations\": ["
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"ip-address\": \"192.0.2.28\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}";
+
+ testPrecedence(config, "192.0.2.3");
+}
+
+// Verify option processing precedence
+// Order is global < class < shared-network < subnet < pool < host reservation
+TEST_F(Dhcpv4SharedNetworkTest, precedenceSubnet) {
+ const std::string config =
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.1\""
+ " }"
+ " ],"
+ " \"client-classes\": ["
+ " {"
+ " \"name\": \"alpha\","
+ " \"test\": \"'' == ''\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.2\""
+ " }"
+ " ]"
+ " }"
+ " ],"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"interface\": \"eth1\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.3\""
+ " }"
+ " ],"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/26\","
+ " \"id\": 10,"
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.4\""
+ " }"
+ " ],"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.1 - 192.0.2.63\""
+ " }"
+ " ],"
+ " \"reservations\": ["
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"ip-address\": \"192.0.2.28\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}";
+
+ testPrecedence(config, "192.0.2.4");
+}
+
+// Verify option processing precedence
+// Order is global < class < shared-network < subnet < pool < host reservation
+TEST_F(Dhcpv4SharedNetworkTest, precedencePool) {
+ const std::string config =
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.1\""
+ " }"
+ " ],"
+ " \"client-classes\": ["
+ " {"
+ " \"name\": \"alpha\","
+ " \"test\": \"'' == ''\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.2\""
+ " }"
+ " ]"
+ " }"
+ " ],"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"interface\": \"eth1\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.3\""
+ " }"
+ " ],"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/26\","
+ " \"id\": 10,"
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.4\""
+ " }"
+ " ],"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.1 - 192.0.2.63\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.5\""
+ " }"
+ " ]"
+ " }"
+ " ],"
+ " \"reservations\": ["
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"ip-address\": \"192.0.2.28\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}";
+
+ testPrecedence(config, "192.0.2.5");
+}
+
+// Verify option processing precedence
+// Order is global < class < shared-network < subnet < pool < host reservation
+TEST_F(Dhcpv4SharedNetworkTest, precedenceReservation) {
+ const std::string config =
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"valid-lifetime\": 600,"
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.1\""
+ " }"
+ " ],"
+ " \"client-classes\": ["
+ " {"
+ " \"name\": \"alpha\","
+ " \"test\": \"'' == ''\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.2\""
+ " }"
+ " ]"
+ " }"
+ " ],"
+ " \"shared-networks\": ["
+ " {"
+ " \"name\": \"frog\","
+ " \"interface\": \"eth1\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.3\""
+ " }"
+ " ],"
+ " \"subnet4\": ["
+ " {"
+ " \"subnet\": \"192.0.2.0/26\","
+ " \"id\": 10,"
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.4\""
+ " }"
+ " ],"
+ " \"pools\": ["
+ " {"
+ " \"pool\": \"192.0.2.1 - 192.0.2.63\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.5\""
+ " }"
+ " ]"
+ " }"
+ " ],"
+ " \"reservations\": ["
+ " {"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+ " \"ip-address\": \"192.0.2.28\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.6\""
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ " }"
+ " ]"
+ "}";
+
+ testPrecedence(config, "192.0.2.6");
+}
+
+// Verify authoritative sanitization.
+// This test generates many similar configs. They all have similar
+// structure:
+// - 1 shared-network (with possibly authoritative flag in it)
+// 2 subnets (with each possibly having its own authoritative flag)
+//
+// If the flag is specified on subnet-level, the value must match those
+// specified on subnet level.
+TEST_F(Dhcpv4SharedNetworkTest, authoritative) {
+
+ // Each scenario will be defined using those parameters.
+ typedef struct scenario {
+ bool exp_success;
+ AuthoritativeFlag global;
+ AuthoritativeFlag subnet1;
+ AuthoritativeFlag subnet2;
+ } scenario;
+
+ // We have the following scenarios. The default is no.
+ // The only allowed combinations are those that end up with
+ // having all three values (global, subnet1, subnet2) agree.
+ scenario scenarios[] = {
+ //result, global, subnet1, subnet2
+ { true, AUTH_DEFAULT, AUTH_DEFAULT, AUTH_DEFAULT },
+ { true, AUTH_YES, AUTH_DEFAULT, AUTH_DEFAULT },
+ { true, AUTH_YES, AUTH_YES, AUTH_YES },
+ { true, AUTH_NO, AUTH_DEFAULT, AUTH_DEFAULT },
+ { true, AUTH_NO, AUTH_NO, AUTH_NO },
+ { false, AUTH_DEFAULT, AUTH_YES, AUTH_NO },
+ { false, AUTH_DEFAULT, AUTH_NO, AUTH_YES },
+ { false, AUTH_DEFAULT, AUTH_YES, AUTH_YES },
+ { false, AUTH_YES, AUTH_NO, AUTH_NO },
+ { false, AUTH_YES, AUTH_DEFAULT, AUTH_NO },
+ { false, AUTH_YES, AUTH_NO, AUTH_DEFAULT },
+ { false, AUTH_YES, AUTH_NO, AUTH_NO },
+ { false, AUTH_YES, AUTH_NO, AUTH_YES },
+ { false, AUTH_YES, AUTH_YES, AUTH_NO }
+ };
+
+ // Let's test them one by one
+ int cnt = 0;
+ for ( auto s : scenarios) {
+ cnt++;
+
+ string cfg = generateAuthConfig(s.global, s.subnet1, s.subnet2);
+
+ // Create client and set MAC address to the one that has a reservation.
+ Dhcp4Client client(Dhcp4Client::SELECTING);
+
+ stringstream tmp;
+ tmp << "Testing scenario " << cnt;
+ SCOPED_TRACE(tmp.str());
+ // Create server configuration.
+ auto result = configureWithStatus(cfg, *client.getServer(), true, s.exp_success? 0 : 1);
+ if (s.exp_success) {
+ EXPECT_EQ(result.first, 0) << result.second;
+ } else {
+ EXPECT_EQ(result.first, 1) << "Configuration expected to fail, but succeeded";
+ }
+ }
+}
+
+// Test that different allocator types can be used within a shared network.
+// The first subnet uses the random allocator. The second subnet uses the FLQ
+// allocator.
+TEST_F(Dhcpv4SharedNetworkTest, randomAndFlqAllocation) {
+ testDifferentAllocatorsInNetwork(NETWORKS_CONFIG[21]);
+}
+
+// Test that different allocator types can be used within a shared network.
+// The first subnet uses the FLQ allocator. The second subnet uses the random
+// allocator.
+TEST_F(Dhcpv4SharedNetworkTest, flqAndRandomAllocation) {
+ testDifferentAllocatorsInNetwork(NETWORKS_CONFIG[22]);
+}
+
+} // end of anonymous namespace
diff --git a/src/bin/dhcp4/tests/simple_parser4_unittest.cc b/src/bin/dhcp4/tests/simple_parser4_unittest.cc
new file mode 100644
index 0000000..62d12c4
--- /dev/null
+++ b/src/bin/dhcp4/tests/simple_parser4_unittest.cc
@@ -0,0 +1,215 @@
+// Copyright (C) 2016-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <gtest/gtest.h>
+#include <dhcpsrv/parsers/simple_parser4.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <cc/data.h>
+
+using namespace isc::data;
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @brief DHCP Parser test fixture class
+class SimpleParser4Test : public ::testing::Test {
+public:
+ /// @brief Checks if specified map has an integer parameter with expected value
+ ///
+ /// @param map map to be checked
+ /// @param param_name name of the parameter to be checked
+ /// @param exp_value expected value of the parameter.
+ void checkIntegerValue(const ConstElementPtr& map, const std::string& param_name,
+ int64_t exp_value) {
+
+ // First check if the passed element is a map.
+ ASSERT_EQ(Element::map, map->getType());
+
+ // Now try to get the element being checked
+ ConstElementPtr elem = map->get(param_name);
+ ASSERT_TRUE(elem);
+
+ // Now check if it's indeed integer
+ ASSERT_EQ(Element::integer, elem->getType());
+
+ // Finally, check if its value meets expectation.
+ EXPECT_EQ(exp_value, elem->intValue());
+ }
+
+ /// @brief Checks if specified map has a string parameter with expected value
+ ///
+ /// @param map map to be checked
+ /// @param param_name name of the parameter to be checked
+ /// @param exp_value expected value of the parameter.
+ void checkStringValue(const ConstElementPtr& map, const std::string& param_name,
+ std::string exp_value) {
+
+ // First check if the passed element is a map.
+ ASSERT_EQ(Element::map, map->getType());
+
+ // Now try to get the element being checked
+ ConstElementPtr elem = map->get(param_name);
+ ASSERT_TRUE(elem);
+
+ // Now check if it's indeed integer
+ ASSERT_EQ(Element::string, elem->getType());
+
+ // Finally, check if its value meets expectation.
+ EXPECT_EQ(exp_value, elem->stringValue());
+ }
+
+ /// @brief Checks if specified map has a boolean parameter with expected value
+ ///
+ /// @param map map to be checked
+ /// @param param_name name of the parameter to be checked
+ /// @param exp_value expected value of the parameter.
+ void checkBoolValue(const ConstElementPtr& map, const std::string& param_name,
+ bool exp_value) {
+
+ // First check if the passed element is a map.
+ ASSERT_EQ(Element::map, map->getType());
+
+ // Now try to get the element being checked
+ ConstElementPtr elem = map->get(param_name);
+ ASSERT_TRUE(elem);
+
+ // Now check if it's indeed integer
+ ASSERT_EQ(Element::boolean, elem->getType());
+
+ // Finally, check if its value meets expectation.
+ EXPECT_EQ(exp_value, elem->boolValue());
+ }
+};
+
+// This test checks if global defaults are properly set for DHCPv4.
+TEST_F(SimpleParser4Test, globalDefaults4) {
+
+ ElementPtr empty = parseJSON("{ }");
+ size_t num = 0;
+
+ EXPECT_NO_THROW(num = SimpleParser4::setAllDefaults(empty));
+
+
+ // We expect at least 1 parameter to be inserted.
+ EXPECT_TRUE(num >= 1);
+
+ checkIntegerValue(empty, "valid-lifetime", 7200);
+
+ // Timers are optional and by default are not present
+ EXPECT_FALSE(empty->contains("rebind-timer"));
+ EXPECT_FALSE(empty->contains("renew-timer"));
+
+ // Make sure that preferred-lifetime is not set for v4 (it's v6 only
+ // parameter)
+ EXPECT_FALSE(empty->get("preferred-lifetime"));
+}
+
+// This test checks if the parameters can be inherited from the global
+// scope to the subnet scope.
+TEST_F(SimpleParser4Test, inheritGlobalToSubnet4) {
+ ElementPtr global = parseJSON("{ \"renew-timer\": 1,"
+ " \"rebind-timer\": 2,"
+ " \"valid-lifetime\": 4,"
+ " \"min-valid-lifetime\": 3,"
+ " \"max-valid-lifetime\": 5,"
+ " \"subnet4\": [ { \"renew-timer\": 100 } ] "
+ "}");
+ ConstElementPtr subnets = global->find("subnet4");
+ ASSERT_TRUE(subnets);
+ ConstElementPtr subnet = subnets->get(0);
+ ASSERT_TRUE(subnet);
+
+ // we should inherit 4 parameters. Renew-timer should remain intact,
+ // as it was already defined in the subnet scope.
+ size_t num;
+ EXPECT_NO_THROW(num = SimpleParser4::deriveParameters(global));
+ EXPECT_EQ(4, num);
+
+ // Check the values. 2 of them are inherited, while the third one
+ // was already defined in the subnet, so should not be inherited.
+ checkIntegerValue(subnet, "renew-timer", 100);
+ checkIntegerValue(subnet, "rebind-timer", 2);
+ checkIntegerValue(subnet, "valid-lifetime", 4);
+ checkIntegerValue(subnet, "min-valid-lifetime", 3);
+ checkIntegerValue(subnet, "max-valid-lifetime", 5);
+}
+
+// This test checks if the parameters in "subnet4" are assigned default values
+// if not explicitly specified.
+TEST_F(SimpleParser4Test, subnetDefaults4) {
+ ElementPtr global = parseJSON("{ \"renew-timer\": 1,"
+ " \"rebind-timer\": 2,"
+ " \"valid-lifetime\": 4,"
+ " \"subnet4\": [ { } ] "
+ "}");
+
+ size_t num = 0;
+ EXPECT_NO_THROW(num = SimpleParser4::setAllDefaults(global));
+ EXPECT_LE(1, num); // at least 1 parameter has to be modified
+
+ ConstElementPtr subnets = global->find("subnet4");
+ ASSERT_TRUE(subnets);
+ ConstElementPtr subnet = subnets->get(0);
+ ASSERT_TRUE(subnet);
+
+ // we should have "id" parameter with the default value of 0 added for us.
+ checkIntegerValue(subnet, "id", 0);
+}
+
+// This test checks if the parameters in option-data are assigned default values
+// if not explicitly specified.
+TEST_F(SimpleParser4Test, optionDataDefaults4) {
+ ElementPtr global = parseJSON("{ \"renew-timer\": 1,"
+ " \"rebind-timer\": 2,"
+ " \"valid-lifetime\": 4,"
+ " \"option-data\": [ { } ] "
+ "}");
+
+ size_t num = 0;
+ EXPECT_NO_THROW(num = SimpleParser4::setAllDefaults(global));
+ EXPECT_LE(1, num); // at least 1 parameter has to be modified
+
+ ConstElementPtr options = global->find("option-data");
+ ASSERT_TRUE(options);
+ ConstElementPtr option = options->get(0);
+ ASSERT_TRUE(option);
+
+ // we should have appropriate default value set. See
+ // SimpleParser4::OPTION4_DEFAULTS for a list of default values.
+ checkStringValue(option, "space", "dhcp4");
+ checkBoolValue(option, "csv-format", true);
+}
+
+// This test checks if the parameters in option-data are assigned default values
+// if not explicitly specified.
+TEST_F(SimpleParser4Test, optionDefDefaults4) {
+ ElementPtr global = parseJSON("{ "
+ " \"option-def\": [ { } ] "
+ "}");
+
+ size_t num = 0;
+ EXPECT_NO_THROW(num = SimpleParser4::setAllDefaults(global));
+ EXPECT_LE(1, num); // at least 1 parameter has to be modified
+
+ ConstElementPtr defs = global->find("option-def");
+ ASSERT_TRUE(defs);
+ ASSERT_EQ(1, defs->size());
+ ConstElementPtr def = defs->get(0);
+ ASSERT_TRUE(def);
+
+ // we should have appropriate default value set. See
+ // SimpleParser4::OPTION4_DEFAULTS for a list of default values.
+ checkStringValue(def, "record-types", "");
+ checkStringValue(def, "space", "dhcp4");
+ checkStringValue(def, "encapsulate", "");
+ checkBoolValue(def, "array", false);
+}
+
+};
+};
+};
diff --git a/src/bin/dhcp4/tests/test_data_files_config.h.in b/src/bin/dhcp4/tests/test_data_files_config.h.in
new file mode 100644
index 0000000..dfa1e64
--- /dev/null
+++ b/src/bin/dhcp4/tests/test_data_files_config.h.in
@@ -0,0 +1,9 @@
+// Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+/// @brief Path to dhcp4 source dir so tests against the dhcp4.spec file
+/// can find it reliably.
+#define DHCP4_SRC_DIR "@abs_top_srcdir@/src/bin/dhcp4"
diff --git a/src/bin/dhcp4/tests/test_libraries.h.in b/src/bin/dhcp4/tests/test_libraries.h.in
new file mode 100644
index 0000000..9b9a243
--- /dev/null
+++ b/src/bin/dhcp4/tests/test_libraries.h.in
@@ -0,0 +1,32 @@
+// Copyright (C) 2013-2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#ifndef TEST_LIBRARIES_H
+#define TEST_LIBRARIES_H
+
+#include <config.h>
+
+namespace {
+
+#define DLL_SUFFIX ".so"
+
+// Names of the libraries used in these tests. These libraries are built using
+// libtool, so we need to look in the hidden ".libs" directory to locate the
+// shared library.
+
+// Library with load/unload functions creating marker files to check their
+// operation.
+const char* const CALLOUT_LIBRARY_1 = "@abs_builddir@/.libs/libco1.so";
+const char* const CALLOUT_LIBRARY_2 = "@abs_builddir@/.libs/libco2.so";
+const char* const CALLOUT_LIBRARY_3 = "@abs_builddir@/.libs/libco3.so";
+
+// Name of a library which is not present.
+const char* const NOT_PRESENT_LIBRARY = "@abs_builddir@/.libs/libnothere.so";
+
+} // anonymous namespace
+
+
+#endif // TEST_LIBRARIES_H
diff --git a/src/bin/dhcp4/tests/vendor_opts_unittest.cc b/src/bin/dhcp4/tests/vendor_opts_unittest.cc
new file mode 100644
index 0000000..cf38afc
--- /dev/null
+++ b/src/bin/dhcp4/tests/vendor_opts_unittest.cc
@@ -0,0 +1,2798 @@
+// Copyright (C) 2019-2023 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// This file is dedicated to testing vendor options. There are several
+// vendor options in DHCPv4:
+//
+// vivso (125) - vendor independent vendor specific option. This is by far the
+// most popular.
+// vendor specific (43) - this is probably the second most popular.
+// Unfortunately, its definition is blurry, so there are
+// many similar, but not exact implementations that do
+// things in different ways.
+// vivco (124) - vendor independent vendor class option.
+// class identifier (60) - not exactly vendor specific. It's a string, but the
+// content of that string identifies what kind of vendor
+// device this is.
+// client-class (77) - this specifies (as a plain string) what kind of device
+// this is.
+
+#include <config.h>
+#include <asiolink/io_address.h>
+#include <cc/command_interpreter.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <dhcp4/dhcp4_srv.h>
+#include <dhcp4/json_config_parser.h>
+#include <dhcp4/tests/dhcp4_client.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcp/option_int_array.h>
+#include <dhcp/option_int.h>
+#include <dhcp/option_string.h>
+#include <dhcp/option_vendor.h>
+#include <dhcp/option_vendor_class.h>
+#include <dhcp/tests/pkt_captures.h>
+#include <dhcp/docsis3_option_defs.h>
+#include <dhcp/dhcp4.h>
+#include <dhcpsrv/cfg_multi_threading.h>
+#include <dhcpsrv/cfgmgr.h>
+
+#include <gtest/gtest.h>
+
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::config;
+using namespace isc::dhcp::test;
+
+/// @brief Class dedicated to testing vendor options in DHCPv4
+///
+/// For the time being it does not provide any additional functionality, but it
+/// groups all vendor related tests under a single name. There were too many
+/// tests in Dhcpv4SrvTest class anyway.
+class VendorOptsTest : public Dhcpv4SrvTest {
+public:
+ /// @brief Called before each test
+ void SetUp() override {
+ iface_mgr_test_config_.reset(new IfaceMgrTestConfig(true));
+ IfaceMgr::instance().openSockets4();
+ }
+
+ /// @brief Called after each test
+ void TearDown() override {
+ iface_mgr_test_config_.reset();
+ IfaceMgr::instance().closeSockets();
+ }
+
+ /// @brief Checks if Option Request Option (ORO) in docsis (vendor-id=4491)
+ /// vendor options is parsed correctly and the requested options are
+ /// actually assigned. Also covers negative tests that options are not
+ /// provided when a different vendor ID is given.
+ ///
+ /// @note Kea only knows how to process VENDOR_ID_CABLE_LABS DOCSIS3_V4_ORO
+ /// (suboption 1).
+ ///
+ /// @param configured_vendor_ids The vendor IDs that are configured in the
+ /// server: 4491 or both 4491 and 3561.
+ /// @param requested_vendor_ids Then vendor IDs that are present in ORO.
+ /// @param requested_options The requested options in ORO.
+ void testVendorOptionsORO(std::set<uint32_t> configured_vendor_ids,
+ std::set<uint32_t> requested_vendor_ids,
+ std::set<uint16_t> requested_options) {
+ std::set<uint32_t> result_vendor_ids;
+ ASSERT_FALSE(configured_vendor_ids.empty());
+ ASSERT_TRUE(configured_vendor_ids.find(VENDOR_ID_CABLE_LABS) != configured_vendor_ids.end());
+ for (uint32_t req : requested_vendor_ids) {
+ if (req == VENDOR_ID_CABLE_LABS) {
+ result_vendor_ids.insert(req);
+ }
+ }
+ // Create a config with custom options.
+ string config = R"(
+ {
+ "interfaces-config": {
+ "interfaces": [ "*" ]
+ },
+ "option-data": [
+ {
+ "code": 2,
+ "csv-format": true,
+ "data": "192.0.2.1, 192.0.2.2",
+ "name": "tftp-servers",
+ "space": "vendor-4491"
+ },
+ {
+ "code": 22,
+ "csv-format": true,
+ "data": "first",
+ "name": "tag",
+ "space": "vendor-4491"
+ )";
+ if (configured_vendor_ids.size() > 1) {
+ config += R"(
+ },
+ {
+ "code": 2,
+ "csv-format": true,
+ "data": "10.0.2.1, 10.0.2.2",
+ "name": "custom",
+ "space": "vendor-3561",
+ },
+ {
+ "code": 22,
+ "csv-format": true,
+ "data": "last",
+ "name": "special",
+ "space": "vendor-3561"
+ )";
+ }
+ config += R"(
+ }
+ ],
+ "option-def": [
+ {
+ "code": 22,
+ "name": "tag",
+ "space": "vendor-4491",
+ "type": "string"
+ )";
+ if (configured_vendor_ids.size() > 1) {
+ config += R"(
+ },
+ {
+ "code": 2,
+ "name": "custom",
+ "space": "vendor-3561",
+ "type": "ipv4-address",
+ "array": true
+ },
+ {
+ "code": 22,
+ "name": "special",
+ "space": "vendor-3561",
+ "type": "string"
+ )";
+ }
+ config += R"(
+ }
+ ],
+ "subnet4": [
+ {
+ "interface": "eth0",
+ "pools": [
+ {
+ "pool": "192.0.2.0/25"
+ }
+ ],
+ "subnet": "192.0.2.0/24",
+ "id": 10
+ }
+ ]
+ }
+ )";
+
+ // Parse the configuration.
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+
+ // Configure a mocked server.
+ NakedDhcpv4Srv srv(0);
+ ConstElementPtr x;
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(srv, json));
+ ASSERT_TRUE(x);
+ comment_ = parseAnswer(rcode_, x);
+ ASSERT_EQ(0, rcode_);
+
+ CfgMgr::instance().commit();
+
+ // Set the giaddr and hops to non-zero address as if it was relayed.
+ boost::shared_ptr<Pkt4> dis(new Pkt4(DHCPDISCOVER, 1234));
+ dis->setGiaddr(IOAddress("192.0.2.1"));
+ dis->setHops(1);
+
+ // Set interface. It is required by the server to generate server id.
+ dis->setIface("eth0");
+ dis->setIndex(ETH0_INDEX);
+
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+
+ // Pass it to the server and get an offer
+ Pkt4Ptr offer = srv.processDiscover(dis);
+
+ // Check if we get a response at all.
+ ASSERT_TRUE(offer);
+
+ // We did not include any vendor opts in DISCOVER, so there should be none
+ // in OFFER.
+ ASSERT_FALSE(offer->getOption(DHO_VIVSO_SUBOPTIONS));
+
+ // Let's add a vendor-option (vendor-id=4491) with a single sub-option.
+ // That suboption has code 1 and is a docsis ORO option.
+ OptionUint8ArrayPtr vendor_oro(new OptionUint8Array(Option::V4, DOCSIS3_V4_ORO));
+ for (uint16_t option : requested_options) {
+ vendor_oro->addValue(option);
+ }
+
+ for (uint32_t vendor_id : requested_vendor_ids) {
+ OptionVendorPtr vendor(new OptionVendor(Option::V4, vendor_id));
+ vendor->addOption(vendor_oro);
+ dis->Pkt::addOption(vendor);
+ }
+
+ // Need to process DHCPDISCOVER again after requesting new option.
+ offer = srv.processDiscover(dis);
+ ASSERT_TRUE(offer);
+
+ // Check if there is a vendor option in the response, if the Cable Labs
+ // vendor ID was provided in the request. Otherwise, check that there is
+ // no vendor and stop processing since the following checks are built on
+ // top of the now-absent options.
+ OptionCollection tmp = offer->getOptions(DHO_VIVSO_SUBOPTIONS);
+ ASSERT_EQ(tmp.size(), result_vendor_ids.size());
+ if (result_vendor_ids.empty()) {
+ return;
+ }
+
+ for (auto const& opt : tmp) {
+ // The response should be an OptionVendor.
+ OptionVendorPtr vendor_resp;
+
+ for (uint32_t vendor_id : result_vendor_ids) {
+ vendor_resp = boost::dynamic_pointer_cast<OptionVendor>(opt.second);
+ ASSERT_TRUE(vendor_resp);
+ if (vendor_resp->getVendorId() == vendor_id) {
+ break;
+ }
+ vendor_resp.reset();
+ }
+ ASSERT_TRUE(vendor_resp);
+ if (vendor_resp->getVendorId() == VENDOR_ID_CABLE_LABS) {
+ for (uint16_t option : requested_options) {
+ if (option == DOCSIS3_V4_TFTP_SERVERS) {
+ // Option 2 should be present.
+ OptionPtr docsis2 = vendor_resp->getOption(DOCSIS3_V4_TFTP_SERVERS);
+ ASSERT_TRUE(docsis2);
+
+ // It should be an Option4AddrLst.
+ Option4AddrLstPtr tftp_srvs =
+ boost::dynamic_pointer_cast<Option4AddrLst>(docsis2);
+ ASSERT_TRUE(tftp_srvs);
+
+ // Check that the provided addresses match the ones in configuration.
+ Option4AddrLst::AddressContainer addrs = tftp_srvs->getAddresses();
+ ASSERT_EQ(2, addrs.size());
+ EXPECT_EQ("192.0.2.1", addrs[0].toText());
+ EXPECT_EQ("192.0.2.2", addrs[1].toText());
+ }
+
+ if (option == 22) {
+ // Option 22 should be present.
+ OptionPtr custom = vendor_resp->getOption(22);
+ ASSERT_TRUE(custom);
+
+ // It should be an OptionString.
+ OptionStringPtr tag = boost::dynamic_pointer_cast<OptionString>(custom);
+ ASSERT_TRUE(tag);
+
+ // Check that the provided value match the ones in configuration.
+ EXPECT_EQ(tag->getValue(), "first");
+ }
+ }
+ } else {
+ // If explicitly sending OptionVendor and the vendor is not
+ // VENDOR_ID_CABLE_LABS, options should not be present. Kea only
+ // knows how to process VENDOR_ID_CABLE_LABS DOCSIS3_V4_ORO
+ // (suboption 1).
+ // Option 2 should not be present.
+ OptionPtr docsis2 = vendor_resp->getOption(2);
+ ASSERT_FALSE(docsis2);
+
+ // Option 22 should not be present.
+ OptionPtr custom = vendor_resp->getOption(22);
+ ASSERT_FALSE(custom);
+ }
+ }
+ }
+
+ /// @brief Checks if vendor options are parsed correctly and the persistent
+ /// options are actually assigned. Also covers negative tests that options
+ /// are not provided when a different vendor ID is given.
+ ///
+ /// @param configured_vendor_ids The vendor IDs that are configured in the
+ /// server: 4491 or both 4491 and 3561.
+ /// @param requested_vendor_ids Then vendor IDs that are present in ORO.
+ /// @param configured_options The configured options.
+ /// @param add_vendor_option The flag which indicates if the request should
+ /// contain a OptionVendor option or should the server always send all the
+ /// OptionVendor options and suboptions.
+ void testVendorOptionsPersistent(std::set<uint32_t> configured_vendor_ids,
+ std::set<uint32_t> requested_vendor_ids,
+ std::set<uint16_t> configured_options,
+ bool add_vendor_option) {
+ std::set<uint32_t> result_vendor_ids;
+ ASSERT_FALSE(configured_vendor_ids.empty());
+ ASSERT_TRUE(configured_vendor_ids.find(VENDOR_ID_CABLE_LABS) != configured_vendor_ids.end());
+ if (add_vendor_option) {
+ for (uint32_t req : requested_vendor_ids) {
+ if (configured_vendor_ids.find(req) != configured_vendor_ids.end()) {
+ result_vendor_ids.insert(req);
+ }
+ }
+ } else {
+ result_vendor_ids = configured_vendor_ids;
+ }
+ ASSERT_FALSE(configured_options.empty());
+ ASSERT_TRUE(configured_options.find(DOCSIS3_V4_TFTP_SERVERS) != configured_options.end());
+ // Create a config with custom options.
+ string config = R"(
+ {
+ "interfaces-config": {
+ "interfaces": [ "*" ]
+ },
+ "option-data": [
+ {
+ "always-send": true,
+ "code": 2,
+ "csv-format": true,
+ "data": "192.0.2.1, 192.0.2.2",
+ "name": "tftp-servers",
+ "space": "vendor-4491"
+ )";
+ if (configured_options.size() > 1) {
+ config += R"(
+ },
+ {
+ "always-send": true,
+ "code": 22,
+ "csv-format": true,
+ "data": "first",
+ "name": "tag",
+ "space": "vendor-4491"
+ )";
+ }
+ if (!add_vendor_option) {
+ config += R"(
+ },
+ {
+ "always-send": true,
+ "name": "vivso-suboptions",
+ "data": "4491",
+ "space": "dhcp4"
+ )";
+ }
+ if (configured_vendor_ids.size() > 1) {
+ config += R"(
+ },
+ {
+ "always-send": true,
+ "code": 2,
+ "csv-format": true,
+ "data": "10.0.2.1, 10.0.2.2",
+ "name": "custom",
+ "space": "vendor-3561"
+ )";
+ if (configured_options.size() > 1) {
+ config += R"(
+ },
+ {
+ "always-send": true,
+ "code": 22,
+ "csv-format": true,
+ "data": "last",
+ "name": "special",
+ "space": "vendor-3561"
+ )";
+ }
+ if (!add_vendor_option) {
+ config += R"(
+ },
+ {
+ "always-send": true,
+ "name": "vivso-suboptions",
+ "data": "3561",
+ "space": "dhcp4"
+ )";
+ }
+ }
+ config += R"(
+ }
+ ],
+ "option-def": [
+ {
+ "code": 22,
+ "name": "tag",
+ "space": "vendor-4491",
+ "type": "string"
+ )";
+ if (configured_vendor_ids.size() > 1) {
+ config += R"(
+ },
+ {
+ "code": 2,
+ "name": "custom",
+ "space": "vendor-3561",
+ "type": "ipv4-address",
+ "array": true
+ },
+ {
+ "code": 22,
+ "name": "special",
+ "space": "vendor-3561",
+ "type": "string"
+ )";
+ }
+ config += R"(
+ }
+ ],
+ "subnet4": [
+ {
+ "interface": "eth0",
+ "pools": [
+ {
+ "pool": "192.0.2.0/25"
+ }
+ ],
+ "subnet": "192.0.2.0/24",
+ "id": 10
+ }
+ ]
+ }
+ )";
+
+ // Parse the configuration.
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+
+ // Configure a mocked server.
+ NakedDhcpv4Srv srv(0);
+ ConstElementPtr x;
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(srv, json));
+ ASSERT_TRUE(x);
+ comment_ = parseAnswer(rcode_, x);
+ ASSERT_EQ(0, rcode_);
+
+ CfgMgr::instance().commit();
+
+ // Set the giaddr and hops to non-zero address as if it was relayed.
+ boost::shared_ptr<Pkt4> dis(new Pkt4(DHCPDISCOVER, 1234));
+ dis->setGiaddr(IOAddress("192.0.2.1"));
+ dis->setHops(1);
+
+ // Set interface. It is required by the server to generate server id.
+ dis->setIface("eth0");
+ dis->setIndex(ETH0_INDEX);
+
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+
+ if (add_vendor_option) {
+ for (uint32_t vendor_id : requested_vendor_ids) {
+ // Let's add a vendor-option (vendor-id=4491).
+ OptionVendorPtr vendor(new OptionVendor(Option::V4, vendor_id));
+
+ dis->Pkt::addOption(vendor);
+ }
+ }
+
+ // Pass it to the server and get an offer
+ Pkt4Ptr offer = srv.processDiscover(dis);
+
+ // check if we get response at all
+ ASSERT_TRUE(offer);
+
+ // Check if there is a vendor option response
+ OptionCollection tmp = offer->getOptions(DHO_VIVSO_SUBOPTIONS);
+ ASSERT_EQ(tmp.size(), result_vendor_ids.size());
+
+ for (auto const& opt : tmp) {
+ // The response should be an OptionVendor.
+ OptionVendorPtr vendor_resp;
+
+ for (uint32_t vendor_id : result_vendor_ids) {
+ vendor_resp = boost::dynamic_pointer_cast<OptionVendor>(opt.second);
+ ASSERT_TRUE(vendor_resp);
+ if (vendor_resp->getVendorId() == vendor_id) {
+ break;
+ }
+ }
+ ASSERT_TRUE(vendor_resp);
+
+ for (uint16_t option : configured_options) {
+ if (add_vendor_option &&
+ requested_vendor_ids.find(vendor_resp->getVendorId()) == requested_vendor_ids.end()) {
+ // If explicitly sending OptionVendor and the vendor is not
+ // configured, options should not be present.
+ if (option == DOCSIS3_V4_TFTP_SERVERS) {
+ // Option 2 should not be present.
+ OptionPtr docsis2 = vendor_resp->getOption(DOCSIS3_V4_TFTP_SERVERS);
+ ASSERT_FALSE(docsis2);
+ }
+ if (option == 22) {
+ // Option 22 should not be present.
+ OptionPtr custom = vendor_resp->getOption(22);
+ ASSERT_FALSE(custom);
+ }
+ } else {
+ if (option == DOCSIS3_V4_TFTP_SERVERS) {
+ // Option 2 should be present.
+ OptionPtr docsis2 = vendor_resp->getOption(DOCSIS3_V4_TFTP_SERVERS);
+ ASSERT_TRUE(docsis2);
+
+ // It should be an Option4AddrLst.
+ Option4AddrLstPtr tftp_srvs;
+ if (vendor_resp->getVendorId() == VENDOR_ID_CABLE_LABS) {
+ tftp_srvs = boost::dynamic_pointer_cast<Option4AddrLst>(docsis2);
+ } else {
+ // The option is serialized as Option so it needs to be converted to
+ // Option4AddrLst.
+ auto const& buffer = docsis2->toBinary();
+ tftp_srvs.reset(new Option4AddrLst(DOCSIS3_V4_TFTP_SERVERS,
+ buffer.begin(), buffer.end()));
+ }
+ ASSERT_TRUE(tftp_srvs);
+
+ // Check that the provided addresses match the ones in configuration.
+ Option4AddrLst::AddressContainer addrs = tftp_srvs->getAddresses();
+ ASSERT_EQ(2, addrs.size());
+ if (vendor_resp->getVendorId() == VENDOR_ID_CABLE_LABS) {
+ EXPECT_EQ("192.0.2.1", addrs[0].toText());
+ EXPECT_EQ("192.0.2.2", addrs[1].toText());
+ } else {
+ EXPECT_EQ("10.0.2.1", addrs[0].toText());
+ EXPECT_EQ("10.0.2.2", addrs[1].toText());
+ }
+ }
+
+ if (option == 22) {
+ // Option 22 should be present.
+ OptionPtr custom = vendor_resp->getOption(22);
+ ASSERT_TRUE(custom);
+
+ // It should be an OptionString.
+ // The option is serialized as Option so it needs to be converted to
+ // OptionString.
+ auto const& buffer = custom->toBinary();
+ OptionStringPtr tag(new OptionString(Option::V4, 22,
+ buffer.begin(), buffer.end()));
+ ASSERT_TRUE(tag);
+
+ // Check that the provided value match the ones in configuration.
+ if (vendor_resp->getVendorId() == VENDOR_ID_CABLE_LABS) {
+ EXPECT_EQ(tag->getValue(), "first");
+ } else {
+ EXPECT_EQ(tag->getValue(), "last");
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /// @brief Checks if vendor options are parsed correctly and the persistent
+ /// options are actually assigned. Also covers negative tests that options
+ /// are not provided when a different vendor ID is given.
+ ///
+ /// @param configured_vendor_ids The vendor IDs that are configured in the
+ /// server: 4491 or both 4491 and 3561.
+ /// @param requested_vendor_ids Then vendor IDs that are present in ORO.
+ /// @param requested_options The requested options in ORO.
+ /// @param configured_options The configured options. The suboption 22 has
+ /// always send flag set to true so it will always be sent.
+ void testVendorOptionsOROAndPersistent(std::set<uint32_t> configured_vendor_ids,
+ std::set<uint32_t> requested_vendor_ids,
+ std::set<uint16_t> requested_options,
+ std::set<uint16_t> configured_options) {
+ std::set<uint32_t> result_vendor_ids;
+ ASSERT_FALSE(configured_vendor_ids.empty());
+ ASSERT_TRUE(configured_vendor_ids.find(VENDOR_ID_CABLE_LABS) != configured_vendor_ids.end());
+ result_vendor_ids = configured_vendor_ids;
+ ASSERT_FALSE(configured_options.empty());
+ ASSERT_TRUE(configured_options.find(DOCSIS3_V4_TFTP_SERVERS) != configured_options.end());
+ // Create a config with custom options.
+ string config = R"(
+ {
+ "interfaces-config": {
+ "interfaces": [ "*" ]
+ },
+ "option-data": [
+ {
+ "code": 2,
+ "csv-format": true,
+ "data": "192.0.2.1, 192.0.2.2",
+ "name": "tftp-servers",
+ "space": "vendor-4491"
+ )";
+ if (configured_options.size() > 1) {
+ config += R"(
+ },
+ {
+ "always-send": true,
+ "code": 22,
+ "csv-format": true,
+ "data": "first",
+ "name": "tag",
+ "space": "vendor-4491"
+ )";
+ }
+ config += R"(
+ },
+ {
+ "always-send": true,
+ "name": "vivso-suboptions",
+ "data": "4491",
+ "space": "dhcp4"
+ )";
+ if (configured_vendor_ids.size() > 1) {
+ config += R"(
+ },
+ {
+ "code": 2,
+ "csv-format": true,
+ "data": "10.0.2.1, 10.0.2.2",
+ "name": "custom",
+ "space": "vendor-3561"
+ )";
+ if (configured_options.size() > 1) {
+ config += R"(
+ },
+ {
+ "always-send": true,
+ "code": 22,
+ "csv-format": true,
+ "data": "last",
+ "name": "special",
+ "space": "vendor-3561"
+ )";
+ }
+ config += R"(
+ },
+ {
+ "always-send": true,
+ "name": "vivso-suboptions",
+ "data": "3561",
+ "space": "dhcp4"
+ )";
+ }
+ config += R"(
+ }
+ ],
+ "option-def": [
+ {
+ "code": 22,
+ "name": "tag",
+ "space": "vendor-4491",
+ "type": "string"
+ )";
+ if (configured_vendor_ids.size() > 1) {
+ config += R"(
+ },
+ {
+ "code": 2,
+ "name": "custom",
+ "space": "vendor-3561",
+ "type": "ipv4-address",
+ "array": true
+ },
+ {
+ "code": 22,
+ "name": "special",
+ "space": "vendor-3561",
+ "type": "string"
+ )";
+ }
+ config += R"(
+ }
+ ],
+ "subnet4": [
+ {
+ "interface": "eth0",
+ "pools": [
+ {
+ "pool": "192.0.2.0/25"
+ }
+ ],
+ "subnet": "192.0.2.0/24",
+ "id": 10
+ }
+ ]
+ }
+ )";
+
+ // Parse the configuration.
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+
+ // Configure a mocked server.
+ NakedDhcpv4Srv srv(0);
+ ConstElementPtr x;
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(srv, json));
+ ASSERT_TRUE(x);
+ comment_ = parseAnswer(rcode_, x);
+ ASSERT_EQ(0, rcode_);
+
+ CfgMgr::instance().commit();
+
+ // Set the giaddr and hops to non-zero address as if it was relayed.
+ boost::shared_ptr<Pkt4> dis(new Pkt4(DHCPDISCOVER, 1234));
+ dis->setGiaddr(IOAddress("192.0.2.1"));
+ dis->setHops(1);
+
+ // Set interface. It is required by the server to generate server id.
+ dis->setIface("eth0");
+ dis->setIndex(ETH0_INDEX);
+
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+
+ // Let's add a vendor-option (vendor-id=4491) with a single sub-option.
+ // That suboption has code 1 and is a docsis ORO option.
+ OptionUint8ArrayPtr vendor_oro(new OptionUint8Array(Option::V4, DOCSIS3_V4_ORO));
+ for (uint16_t option : requested_options) {
+ vendor_oro->addValue(option);
+ }
+
+ for (uint32_t vendor_id : requested_vendor_ids) {
+ OptionVendorPtr vendor(new OptionVendor(Option::V4, vendor_id));
+ vendor->addOption(vendor_oro);
+ dis->Pkt::addOption(vendor);
+ }
+
+ // Pass it to the server and get an offer
+ Pkt4Ptr offer = srv.processDiscover(dis);
+
+ // check if we get response at all
+ ASSERT_TRUE(offer);
+
+ // Check if there is a vendor option response
+ OptionCollection tmp = offer->getOptions(DHO_VIVSO_SUBOPTIONS);
+ ASSERT_EQ(tmp.size(), result_vendor_ids.size());
+
+ for (auto const& opt : tmp) {
+ // The response should be an OptionVendor.
+ OptionVendorPtr vendor_resp;
+
+ for (uint32_t vendor_id : result_vendor_ids) {
+ vendor_resp = boost::dynamic_pointer_cast<OptionVendor>(opt.second);
+ ASSERT_TRUE(vendor_resp);
+ if (vendor_resp->getVendorId() == vendor_id) {
+ break;
+ }
+ }
+ ASSERT_TRUE(vendor_resp);
+
+ for (uint16_t option : configured_options) {
+ if (option == DOCSIS3_V4_TFTP_SERVERS) {
+ if (vendor_resp->getVendorId() == VENDOR_ID_CABLE_LABS &&
+ requested_options.find(option) != requested_options.end() &&
+ requested_vendor_ids.find(vendor_resp->getVendorId()) != requested_vendor_ids.end()) {
+ // Option 2 should be present.
+ OptionPtr docsis2 = vendor_resp->getOption(DOCSIS3_V4_TFTP_SERVERS);
+ ASSERT_TRUE(docsis2);
+
+ // It should be an Option4AddrLst.
+ Option4AddrLstPtr tftp_srvs;
+ if (vendor_resp->getVendorId() == VENDOR_ID_CABLE_LABS) {
+ tftp_srvs = boost::dynamic_pointer_cast<Option4AddrLst>(docsis2);
+ } else {
+ // The option is serialized as Option so it needs to be converted to
+ // Option4AddrLst.
+ auto const& buffer = docsis2->toBinary();
+ tftp_srvs.reset(new Option4AddrLst(DOCSIS3_V4_TFTP_SERVERS,
+ buffer.begin(), buffer.end()));
+ }
+ ASSERT_TRUE(tftp_srvs);
+
+ // Check that the provided addresses match the ones in configuration.
+ Option4AddrLst::AddressContainer addrs = tftp_srvs->getAddresses();
+ ASSERT_EQ(2, addrs.size());
+ if (vendor_resp->getVendorId() == VENDOR_ID_CABLE_LABS) {
+ EXPECT_EQ("192.0.2.1", addrs[0].toText());
+ EXPECT_EQ("192.0.2.2", addrs[1].toText());
+ } else {
+ EXPECT_EQ("10.0.2.1", addrs[0].toText());
+ EXPECT_EQ("10.0.2.2", addrs[1].toText());
+ }
+ } else {
+ // If explicitly sending OptionVendor and the vendor is not
+ // VENDOR_ID_CABLE_LABS, or the option is not explicitly
+ // requested, options should not be present. Kea only knows
+ // how to process VENDOR_ID_CABLE_LABS DOCSIS3_V4_ORO
+ // (suboption 1).
+ // Option 2 should not be present.
+ OptionPtr docsis2 = vendor_resp->getOption(DOCSIS3_V4_TFTP_SERVERS);
+ ASSERT_FALSE(docsis2);
+ }
+ }
+
+ if (option == 22) {
+ // Option 22 should be present.
+ OptionPtr custom = vendor_resp->getOption(22);
+ ASSERT_TRUE(custom);
+
+ // It should be an OptionString.
+ // The option is serialized as Option so it needs to be converted to
+ // OptionString.
+ auto const& buffer = custom->toBinary();
+ OptionStringPtr tag(new OptionString(Option::V4, 22,
+ buffer.begin(), buffer.end()));
+ ASSERT_TRUE(tag);
+
+ // Check that the provided value match the ones in configuration.
+ if (vendor_resp->getVendorId() == VENDOR_ID_CABLE_LABS) {
+ EXPECT_EQ(tag->getValue(), "first");
+ } else {
+ EXPECT_EQ(tag->getValue(), "last");
+ }
+ }
+ }
+ }
+ }
+
+ // @brief Test configuration for IfaceMgr.
+ std::unique_ptr<IfaceMgrTestConfig> iface_mgr_test_config_;
+};
+
+/// @todo Add more extensive vendor options tests, including multiple
+/// vendor options
+
+// Checks if vendor options are parsed correctly and requested vendor options
+// are echoed back.
+TEST_F(VendorOptsTest, vendorOptionsDocsis) {
+ NakedDhcpv4Srv srv(0);
+
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ " \"option-data\": [ {"
+ " \"name\": \"tftp-servers\","
+ " \"space\": \"vendor-4491\","
+ " \"code\": 2,"
+ " \"data\": \"10.253.175.16\","
+ " \"csv-format\": true"
+ " }],"
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"10.254.226.0/25\" } ],"
+ " \"subnet\": \"10.254.226.0/24\", "
+ " \"interface\": \"eth0\", "
+ " \"id\": 10"
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(srv, json));
+ ASSERT_TRUE(status);
+ comment_ = parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ CfgMgr::instance().commit();
+
+ // Let's create a relayed DISCOVER. This particular relayed DISCOVER has
+ // added option 82 (relay agent info) with 3 suboptions. The server
+ // is supposed to echo it back in its response.
+ Pkt4Ptr dis;
+ ASSERT_NO_THROW(dis = PktCaptures::captureRelayedDiscover());
+
+ // Simulate that we have received that traffic
+ srv.fakeReceive(dis);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered buffer4_receive callback.
+ srv.run();
+
+ // Check that the server did send a response
+ ASSERT_EQ(1, srv.fake_sent_.size());
+
+ // Make sure that we received a response
+ Pkt4Ptr offer = srv.fake_sent_.front();
+ ASSERT_TRUE(offer);
+
+ // Get Relay Agent Info from query...
+ OptionPtr vendor_opt_response = offer->getOption(DHO_VIVSO_SUBOPTIONS);
+ ASSERT_TRUE(vendor_opt_response);
+
+ // Check if it's of a correct type
+ OptionVendorPtr vendor_opt =
+ boost::dynamic_pointer_cast<OptionVendor>(vendor_opt_response);
+ ASSERT_TRUE(vendor_opt);
+ ASSERT_EQ(vendor_opt->getVendorId(), VENDOR_ID_CABLE_LABS);
+
+ // Get Relay Agent Info from response...
+ OptionPtr tftp_servers_generic = vendor_opt->getOption(DOCSIS3_V4_TFTP_SERVERS);
+ ASSERT_TRUE(tftp_servers_generic);
+
+ Option4AddrLstPtr tftp_servers =
+ boost::dynamic_pointer_cast<Option4AddrLst>(tftp_servers_generic);
+
+ ASSERT_TRUE(tftp_servers);
+
+ Option4AddrLst::AddressContainer addrs = tftp_servers->getAddresses();
+ ASSERT_EQ(1, addrs.size());
+ EXPECT_EQ("10.253.175.16", addrs[0].toText());
+}
+
+// Checks if server is able to handle a relayed traffic from DOCSIS3.0 modems
+TEST_F(VendorOptsTest, docsisVendorOptionsParse) {
+
+ // Let's get a traffic capture from DOCSIS3.0 modem
+ Pkt4Ptr dis = PktCaptures::captureRelayedDiscover();
+ ASSERT_NO_THROW(dis->unpack());
+
+ // Check if the packet contain
+ OptionPtr opt = dis->getOption(DHO_VIVSO_SUBOPTIONS);
+ ASSERT_TRUE(opt);
+
+ OptionVendorPtr vendor = boost::dynamic_pointer_cast<OptionVendor>(opt);
+ ASSERT_TRUE(vendor);
+ ASSERT_EQ(vendor->getVendorId(), VENDOR_ID_CABLE_LABS);
+
+ // This particular capture that we have included options 1 and 5
+ EXPECT_TRUE(vendor->getOption(1));
+ EXPECT_TRUE(vendor->getOption(5));
+
+ // It did not include options any other options
+ EXPECT_FALSE(vendor->getOption(2));
+ EXPECT_FALSE(vendor->getOption(3));
+ EXPECT_FALSE(vendor->getOption(17));
+}
+
+// Checks if server is able to parse incoming docsis option and extract suboption 1 (docsis ORO)
+TEST_F(VendorOptsTest, docsisVendorORO) {
+
+ // Let's get a traffic capture from DOCSIS3.0 modem
+ Pkt4Ptr dis = PktCaptures::captureRelayedDiscover();
+ ASSERT_NO_THROW(dis->unpack());
+
+ // Check if the packet contains vendor specific information option
+ OptionPtr opt = dis->getOption(DHO_VIVSO_SUBOPTIONS);
+ ASSERT_TRUE(opt);
+
+ OptionVendorPtr vendor = boost::dynamic_pointer_cast<OptionVendor>(opt);
+ ASSERT_TRUE(vendor);
+ ASSERT_EQ(vendor->getVendorId(), VENDOR_ID_CABLE_LABS);
+
+ opt = vendor->getOption(DOCSIS3_V4_ORO);
+ ASSERT_TRUE(opt);
+
+ OptionUint8ArrayPtr oro = boost::dynamic_pointer_cast<OptionUint8Array>(opt);
+ EXPECT_TRUE(oro);
+}
+
+// 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(VendorOptsTest, vendorOptionsOROOneOption) {
+ testVendorOptionsORO({ VENDOR_ID_CABLE_LABS },
+ { VENDOR_ID_CABLE_LABS },
+ { DOCSIS3_V4_TFTP_SERVERS });
+}
+
+// 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(VendorOptsTest, vendorOptionsOROMultipleOptions) {
+ testVendorOptionsORO({ VENDOR_ID_CABLE_LABS },
+ { VENDOR_ID_CABLE_LABS },
+ { DOCSIS3_V4_TFTP_SERVERS, 22 });
+}
+
+// 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(VendorOptsTest, vendorOptionsOROOneOptionMultipleVendorsMatchOne) {
+ testVendorOptionsORO({ VENDOR_ID_CABLE_LABS, 3561 },
+ { VENDOR_ID_CABLE_LABS },
+ { DOCSIS3_V4_TFTP_SERVERS });
+}
+
+// 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(VendorOptsTest, vendorOptionsOROMultipleOptionsMultipleVendorsMatchOne) {
+ testVendorOptionsORO({ VENDOR_ID_CABLE_LABS, 3561 },
+ { VENDOR_ID_CABLE_LABS },
+ { DOCSIS3_V4_TFTP_SERVERS, 22 });
+}
+
+// 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(VendorOptsTest, vendorOptionsOROOneOptionMultipleVendorsMatchAll) {
+ testVendorOptionsORO({ VENDOR_ID_CABLE_LABS, 3561 },
+ { VENDOR_ID_CABLE_LABS, 3561 },
+ { DOCSIS3_V4_TFTP_SERVERS });
+}
+
+// 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(VendorOptsTest, vendorOptionsOROMultipleOptionsMultipleVendorsMatchAll) {
+ testVendorOptionsORO({ VENDOR_ID_CABLE_LABS, 3561 },
+ { VENDOR_ID_CABLE_LABS, 3561 },
+ { DOCSIS3_V4_TFTP_SERVERS, 22 });
+}
+
+// Same as vendorOptionsORO except a different vendor ID than Cable Labs is
+// provided and vendor options are expected to not be present in the response.
+TEST_F(VendorOptsTest, vendorOptionsOROOneOptionDifferentVendorID) {
+ testVendorOptionsORO({ VENDOR_ID_CABLE_LABS },
+ { 32768 },
+ { DOCSIS3_V4_TFTP_SERVERS });
+}
+
+// Same as vendorOptionsORO except a different vendor ID than Cable Labs is
+// provided and vendor options are expected to not be present in the response.
+TEST_F(VendorOptsTest, vendorOptionsOROMultipleOptionsDifferentVendorID) {
+ testVendorOptionsORO({ VENDOR_ID_CABLE_LABS },
+ { 32768 },
+ { DOCSIS3_V4_TFTP_SERVERS, 22 });
+}
+
+// Same as vendorOptionsORO except a different vendor ID than Cable Labs is
+// provided and vendor options are expected to not be present in the response.
+TEST_F(VendorOptsTest, vendorOptionsOROOneOptionDifferentVendorIDMultipleVendorsMatchNone) {
+ testVendorOptionsORO({ VENDOR_ID_CABLE_LABS, 3561 },
+ { 32768, 16384 },
+ { DOCSIS3_V4_TFTP_SERVERS });
+}
+
+// Same as vendorOptionsORO except a different vendor ID than Cable Labs is
+// provided and vendor options are expected to not be present in the response.
+TEST_F(VendorOptsTest, vendorOptionsOROMultipleOptionDifferentVendorIDMultipleVendorsMatchNone) {
+ testVendorOptionsORO({ VENDOR_ID_CABLE_LABS, 3561 },
+ { 32768, 16384 },
+ { DOCSIS3_V4_TFTP_SERVERS, 22 });
+}
+
+// This test checks vendor options are parsed correctly and the persistent
+// options are actually assigned.
+TEST_F(VendorOptsTest, vendorPersistentOptionsOneOption) {
+ testVendorOptionsPersistent({ VENDOR_ID_CABLE_LABS },
+ { VENDOR_ID_CABLE_LABS },
+ { DOCSIS3_V4_TFTP_SERVERS },
+ false);
+}
+
+// This test checks vendor options are parsed correctly and the persistent
+// options are actually assigned.
+TEST_F(VendorOptsTest, vendorPersistentOptionsMultipleOption) {
+ testVendorOptionsPersistent({ VENDOR_ID_CABLE_LABS },
+ { VENDOR_ID_CABLE_LABS },
+ { DOCSIS3_V4_TFTP_SERVERS, 22 },
+ false);
+}
+
+// This test checks vendor options are parsed correctly and the persistent
+// options are actually assigned.
+TEST_F(VendorOptsTest, vendorPersistentOptionsOneOptionMultipleVendorsMatchOne) {
+ testVendorOptionsPersistent({ VENDOR_ID_CABLE_LABS, 3561 },
+ { VENDOR_ID_CABLE_LABS },
+ { DOCSIS3_V4_TFTP_SERVERS },
+ false);
+}
+
+// This test checks vendor options are parsed correctly and the persistent
+// options are actually assigned.
+TEST_F(VendorOptsTest, vendorPersistentOptionsMultipleOptionMultipleVendorsMatchOne) {
+ testVendorOptionsPersistent({ VENDOR_ID_CABLE_LABS, 3561 },
+ { VENDOR_ID_CABLE_LABS },
+ { DOCSIS3_V4_TFTP_SERVERS, 22 },
+ false);
+}
+
+// This test checks vendor options are parsed correctly and the persistent
+// options are actually assigned.
+TEST_F(VendorOptsTest, vendorPersistentOptionsOneOptionMultipleVendorsMatchAll) {
+ testVendorOptionsPersistent({ VENDOR_ID_CABLE_LABS, 3561 },
+ { VENDOR_ID_CABLE_LABS, 3561 },
+ { DOCSIS3_V4_TFTP_SERVERS },
+ false);
+}
+
+// This test checks vendor options are parsed correctly and the persistent
+// options are actually assigned.
+TEST_F(VendorOptsTest, vendorPersistentOptionsMultipleOptionMultipleVendorsMatchAll) {
+ testVendorOptionsPersistent({ VENDOR_ID_CABLE_LABS, 3561 },
+ { VENDOR_ID_CABLE_LABS, 3561 },
+ { DOCSIS3_V4_TFTP_SERVERS, 22 },
+ false);
+}
+
+// This test checks vendor options are parsed correctly and the persistent
+// options are actually assigned.
+TEST_F(VendorOptsTest, vendorPersistentOptionsOneOptionAddVendorOption) {
+ testVendorOptionsPersistent({ VENDOR_ID_CABLE_LABS },
+ { VENDOR_ID_CABLE_LABS },
+ { DOCSIS3_V4_TFTP_SERVERS },
+ true);
+}
+
+// This test checks vendor options are parsed correctly and the persistent
+// options are actually assigned.
+TEST_F(VendorOptsTest, vendorPersistentOptionsMultipleOptionAddVendorOption) {
+ testVendorOptionsPersistent({ VENDOR_ID_CABLE_LABS },
+ { VENDOR_ID_CABLE_LABS },
+ { DOCSIS3_V4_TFTP_SERVERS, 22 },
+ true);
+}
+
+// This test checks vendor options are parsed correctly and the persistent
+// options are actually assigned.
+TEST_F(VendorOptsTest, vendorPersistentOptionsOneOptionMultipleVendorsMatchOneAddVendorOption) {
+ testVendorOptionsPersistent({ VENDOR_ID_CABLE_LABS, 3561 },
+ { VENDOR_ID_CABLE_LABS },
+ { DOCSIS3_V4_TFTP_SERVERS },
+ true);
+}
+
+// This test checks vendor options are parsed correctly and the persistent
+// options are actually assigned.
+TEST_F(VendorOptsTest, vendorPersistentOptionsMultipleOptionMultipleVendorsMatchOneAddVendorOption) {
+ testVendorOptionsPersistent({ VENDOR_ID_CABLE_LABS, 3561 },
+ { VENDOR_ID_CABLE_LABS },
+ { DOCSIS3_V4_TFTP_SERVERS, 22 },
+ true);
+}
+
+// This test checks vendor options are parsed correctly and the persistent
+// options are actually assigned.
+TEST_F(VendorOptsTest, vendorPersistentOptionsOneOptionMultipleVendorsMatchAllAddVendorOption) {
+ testVendorOptionsPersistent({ VENDOR_ID_CABLE_LABS, 3561 },
+ { VENDOR_ID_CABLE_LABS, 3561 },
+ { DOCSIS3_V4_TFTP_SERVERS },
+ true);
+}
+
+// This test checks vendor options are parsed correctly and the persistent
+// options are actually assigned.
+TEST_F(VendorOptsTest, vendorPersistentOptionsMultipleOptionMultipleVendorsMatchAllAddVendorOption) {
+ testVendorOptionsPersistent({ VENDOR_ID_CABLE_LABS, 3561 },
+ { VENDOR_ID_CABLE_LABS, 3561 },
+ { DOCSIS3_V4_TFTP_SERVERS, 22 },
+ true);
+}
+
+// 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(VendorOptsTest, vendorOptionsOROAndPersistentOneOption) {
+ testVendorOptionsOROAndPersistent({ VENDOR_ID_CABLE_LABS },
+ { VENDOR_ID_CABLE_LABS },
+ { DOCSIS3_V4_TFTP_SERVERS },
+ { DOCSIS3_V4_TFTP_SERVERS });
+}
+
+// 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(VendorOptsTest, vendorOptionsOROAndPersistentMultipleOptions) {
+ testVendorOptionsOROAndPersistent({ VENDOR_ID_CABLE_LABS },
+ { VENDOR_ID_CABLE_LABS },
+ { DOCSIS3_V4_TFTP_SERVERS },
+ { DOCSIS3_V4_TFTP_SERVERS, 22 });
+}
+
+// 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(VendorOptsTest, vendorOptionsOROAndPersistentOneOptionMultipleVendorsMatchOne) {
+ testVendorOptionsOROAndPersistent({ VENDOR_ID_CABLE_LABS, 3561 },
+ { VENDOR_ID_CABLE_LABS },
+ { DOCSIS3_V4_TFTP_SERVERS },
+ { DOCSIS3_V4_TFTP_SERVERS });
+}
+
+// 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(VendorOptsTest, vendorOptionsOROAndPersistentMultipleOptionsMultipleVendorsMatchOne) {
+ testVendorOptionsOROAndPersistent({ VENDOR_ID_CABLE_LABS, 3561 },
+ { VENDOR_ID_CABLE_LABS },
+ { DOCSIS3_V4_TFTP_SERVERS },
+ { DOCSIS3_V4_TFTP_SERVERS, 22 });
+}
+
+// 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(VendorOptsTest, vendorOptionsOROAndPersistentOneOptionMultipleVendorsMatchAll) {
+ testVendorOptionsOROAndPersistent({ VENDOR_ID_CABLE_LABS, 3561 },
+ { VENDOR_ID_CABLE_LABS, 3561 },
+ { DOCSIS3_V4_TFTP_SERVERS },
+ { DOCSIS3_V4_TFTP_SERVERS });
+}
+
+// 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(VendorOptsTest, vendorOptionsOROAndPersistentMultipleOptionsMultipleVendorsMatchAll) {
+ testVendorOptionsOROAndPersistent({ VENDOR_ID_CABLE_LABS, 3561 },
+ { VENDOR_ID_CABLE_LABS, 3561 },
+ { DOCSIS3_V4_TFTP_SERVERS },
+ { DOCSIS3_V4_TFTP_SERVERS, 22 });
+}
+
+// Same as vendorOptionsORO except a different vendor ID than Cable Labs is
+// provided and vendor options are expected to not be present in the response.
+TEST_F(VendorOptsTest, vendorOptionsOROAndPersistentOneOptionDifferentVendorID) {
+ testVendorOptionsOROAndPersistent({ VENDOR_ID_CABLE_LABS },
+ { 32768 },
+ { DOCSIS3_V4_TFTP_SERVERS },
+ { DOCSIS3_V4_TFTP_SERVERS });
+}
+
+// Same as vendorOptionsORO except a different vendor ID than Cable Labs is
+// provided and vendor options are expected to not be present in the response.
+TEST_F(VendorOptsTest, vendorOptionsOROAndPersistentMultipleOptionsDifferentVendorID) {
+ testVendorOptionsOROAndPersistent({ VENDOR_ID_CABLE_LABS },
+ { 32768 },
+ { DOCSIS3_V4_TFTP_SERVERS },
+ { DOCSIS3_V4_TFTP_SERVERS, 22 });
+}
+
+// Same as vendorOptionsORO except a different vendor ID than Cable Labs is
+// provided and vendor options are expected to not be present in the response.
+TEST_F(VendorOptsTest, vendorOptionsOROAndPersistentOneOptionDifferentVendorIDMultipleVendorsMatchNone) {
+ testVendorOptionsOROAndPersistent({ VENDOR_ID_CABLE_LABS, 3561 },
+ { 32768, 16384 },
+ { DOCSIS3_V4_TFTP_SERVERS },
+ { DOCSIS3_V4_TFTP_SERVERS });
+}
+
+// Same as vendorOptionsORO except a different vendor ID than Cable Labs is
+// provided and vendor options are expected to not be present in the response.
+TEST_F(VendorOptsTest, vendorOptionsOROAndPersistentMultipleOptionDifferentVendorIDMultipleVendorsMatchNone) {
+ testVendorOptionsOROAndPersistent({ VENDOR_ID_CABLE_LABS, 3561 },
+ { 32768, 16384 },
+ { DOCSIS3_V4_TFTP_SERVERS },
+ { DOCSIS3_V4_TFTP_SERVERS, 22 });
+}
+
+// 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(VendorOptsTest, vendorOptionsOROAndPersistentOneOptionNoneRequested) {
+ testVendorOptionsOROAndPersistent({ VENDOR_ID_CABLE_LABS },
+ { VENDOR_ID_CABLE_LABS },
+ {},
+ { DOCSIS3_V4_TFTP_SERVERS });
+}
+
+// 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(VendorOptsTest, vendorOptionsOROAndPersistentMultipleOptionsNoneRequested) {
+ testVendorOptionsOROAndPersistent({ VENDOR_ID_CABLE_LABS },
+ { VENDOR_ID_CABLE_LABS },
+ {},
+ { DOCSIS3_V4_TFTP_SERVERS, 22 });
+}
+
+// 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(VendorOptsTest, vendorOptionsOROAndPersistentOneOptionMultipleVendorsMatchOneNoneRequested) {
+ testVendorOptionsOROAndPersistent({ VENDOR_ID_CABLE_LABS, 3561 },
+ { VENDOR_ID_CABLE_LABS },
+ {},
+ { DOCSIS3_V4_TFTP_SERVERS });
+}
+
+// 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(VendorOptsTest, vendorOptionsOROAndPersistentMultipleOptionsMultipleVendorsMatchOneNoneRequested) {
+ testVendorOptionsOROAndPersistent({ VENDOR_ID_CABLE_LABS, 3561 },
+ { VENDOR_ID_CABLE_LABS },
+ {},
+ { DOCSIS3_V4_TFTP_SERVERS, 22 });
+}
+
+// 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(VendorOptsTest, vendorOptionsOROAndPersistentOneOptionMultipleVendorsMatchAllNoneRequested) {
+ testVendorOptionsOROAndPersistent({ VENDOR_ID_CABLE_LABS, 3561 },
+ { VENDOR_ID_CABLE_LABS, 3561 },
+ {},
+ { DOCSIS3_V4_TFTP_SERVERS });
+}
+
+// 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(VendorOptsTest, vendorOptionsOROAndPersistentMultipleOptionsMultipleVendorsMatchAllNoneRequested) {
+ testVendorOptionsOROAndPersistent({ VENDOR_ID_CABLE_LABS, 3561 },
+ { VENDOR_ID_CABLE_LABS, 3561 },
+ {},
+ { DOCSIS3_V4_TFTP_SERVERS, 22 });
+}
+
+// Same as vendorOptionsORO except a different vendor ID than Cable Labs is
+// provided and vendor options are expected to not be present in the response.
+TEST_F(VendorOptsTest, vendorOptionsOROAndPersistentOneOptionDifferentVendorIDNoneRequested) {
+ testVendorOptionsOROAndPersistent({ VENDOR_ID_CABLE_LABS },
+ { 32768 },
+ {},
+ { DOCSIS3_V4_TFTP_SERVERS });
+}
+
+// Same as vendorOptionsORO except a different vendor ID than Cable Labs is
+// provided and vendor options are expected to not be present in the response.
+TEST_F(VendorOptsTest, vendorOptionsOROAndPersistentMultipleOptionsDifferentVendorIDNoneRequested) {
+ testVendorOptionsOROAndPersistent({ VENDOR_ID_CABLE_LABS },
+ { 32768 },
+ {},
+ { DOCSIS3_V4_TFTP_SERVERS, 22 });
+}
+
+// Same as vendorOptionsORO except a different vendor ID than Cable Labs is
+// provided and vendor options are expected to not be present in the response.
+TEST_F(VendorOptsTest, vendorOptionsOROAndPersistentOneOptionDifferentVendorIDMultipleVendorsMatchNoneNoneRequested) {
+ testVendorOptionsOROAndPersistent({ VENDOR_ID_CABLE_LABS, 3561 },
+ { 32768, 16384 },
+ {},
+ { DOCSIS3_V4_TFTP_SERVERS });
+}
+
+// Same as vendorOptionsORO except a different vendor ID than Cable Labs is
+// provided and vendor options are expected to not be present in the response.
+TEST_F(VendorOptsTest, vendorOptionsOROAndPersistentMultipleOptionDifferentVendorIDMultipleVendorsMatchNoneNoneRequested) {
+ testVendorOptionsOROAndPersistent({ VENDOR_ID_CABLE_LABS, 3561 },
+ { 32768, 16384 },
+ {},
+ { DOCSIS3_V4_TFTP_SERVERS, 22 });
+}
+
+// This test checks if cancelled options are actually never assigned.
+TEST_F(VendorOptsTest, vendorCancelledOptions) {
+ NakedDhcpv4Srv srv(0);
+
+ ConstElementPtr x;
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ " \"option-def\": [ {"
+ " \"space\": \"vendor-4491\","
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"string\""
+ " } ],"
+ " \"option-data\": [ {"
+ " \"name\": \"tftp-servers\","
+ " \"space\": \"vendor-4491\","
+ " \"code\": 2,"
+ " \"data\": \"192.0.2.1, 192.0.2.2\","
+ " \"csv-format\": true,"
+ " \"always-send\": true"
+ " },{"
+ " \"space\": \"vendor-4491\","
+ " \"code\": 100,"
+ " \"csv-format\": true,"
+ " \"data\": \"bar\""
+ " } ],"
+ "\"subnet4\": [ { "
+ " \"id\": 10,"
+ " \"pools\": [ { \"pool\": \"192.0.2.0/25\" } ],"
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"interface\": \"eth0\", "
+ " \"option-data\": [ {"
+ " \"name\": \"tftp-servers\","
+ " \"space\": \"vendor-4491\","
+ " \"code\": 2,"
+ " \"never-send\": true"
+ " } ]"
+ " } ]"
+ "}";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+
+ EXPECT_NO_THROW(x = configureDhcp4Server(srv, json));
+ ASSERT_TRUE(x);
+ comment_ = parseAnswer(rcode_, x);
+ ASSERT_EQ(0, rcode_) << comment_->str();
+
+ CfgMgr::instance().commit();
+
+ boost::shared_ptr<Pkt4> dis(new Pkt4(DHCPDISCOVER, 1234));
+ // Set the giaddr and hops to non-zero address as if it was relayed.
+ dis->setGiaddr(IOAddress("192.0.2.1"));
+ dis->setHops(1);
+
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+ // Set interface. It is required by the server to generate server id.
+ dis->setIface("eth0");
+ dis->setIndex(ETH0_INDEX);
+
+ // Let's add a vendor-option (vendor-id=4491).
+ OptionPtr vendor(new OptionVendor(Option::V4, 4491));
+ dis->addOption(vendor);
+
+ // Pass it to the server and get an advertise
+ Pkt4Ptr offer = srv.processDiscover(dis);
+
+ // check if we get response at all
+ ASSERT_TRUE(offer);
+
+ // There should be no vendor option response.
+ EXPECT_FALSE(offer->getOption(DHO_VIVSO_SUBOPTIONS));
+
+ // Let's add a vendor-option (vendor-id=4491) with a single sub-option.
+ // That suboption has code 1 and is a docsis ORO option.
+ boost::shared_ptr<OptionUint8Array> vendor_oro(new OptionUint8Array(Option::V4,
+ DOCSIS3_V4_ORO));
+ vendor_oro->addValue(DOCSIS3_V4_TFTP_SERVERS); // Request option 2.
+ vendor->addOption(vendor_oro);
+
+ // Need to process DHCPDISCOVER again after requesting new option.
+ offer = srv.processDiscover(dis);
+ ASSERT_TRUE(offer);
+
+ // Again there should be no vendor option response.
+ EXPECT_FALSE(offer->getOption(DHO_VIVSO_SUBOPTIONS));
+
+ // Request option 100.
+ vendor_oro->addValue(100);
+
+ // Try again.
+ offer = srv.processDiscover(dis);
+ ASSERT_TRUE(offer);
+
+ // Check if there is a vendor option response
+ OptionPtr tmp = offer->getOption(DHO_VIVSO_SUBOPTIONS);
+ ASSERT_TRUE(tmp);
+
+ // The response should be OptionVendor object
+ boost::shared_ptr<OptionVendor> vendor_resp =
+ boost::dynamic_pointer_cast<OptionVendor>(tmp);
+ ASSERT_TRUE(vendor_resp);
+
+ // No tftp-servers.
+ EXPECT_FALSE(vendor_resp->getOption(DOCSIS3_V4_TFTP_SERVERS));
+
+ // But an option 100.
+ EXPECT_EQ(1, vendor_resp->getOptions().size());
+ EXPECT_TRUE(vendor_resp->getOption(100));
+}
+
+// Test checks whether it is possible to use option definitions defined in
+// src/lib/dhcp/docsis3_option_defs.h.
+TEST_F(VendorOptsTest, vendorOptionsDocsisDefinitions) {
+ ConstElementPtr x;
+ string config_prefix = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ ]"
+ "},"
+ " \"option-data\": [ {"
+ " \"name\": \"tftp-servers\","
+ " \"space\": \"vendor-4491\","
+ " \"code\": ";
+ string config_postfix = ","
+ " \"data\": \"192.0.2.1\","
+ " \"csv-format\": true"
+ " }],"
+ "\"subnet4\": [ { "
+ " \"id\": 10,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.50\" } ],"
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"interface\": \"\""
+ " } ]"
+ "}";
+
+ // There is docsis3 (vendor-id=4491) vendor option 2, which is a
+ // tftp-server. Its format is list of IPv4 addresses.
+ string config_valid = config_prefix + "2" + config_postfix;
+
+ // There is no option 99 defined in vendor-id=4491. As there is no
+ // definition, the config should fail.
+ string config_bogus = config_prefix + "99" + config_postfix;
+
+ ConstElementPtr json_bogus;
+ ASSERT_NO_THROW(json_bogus = parseDHCP4(config_bogus));
+ ConstElementPtr json_valid;
+ ASSERT_NO_THROW(json_valid = parseDHCP4(config_valid));
+
+ NakedDhcpv4Srv srv(0);
+
+ // This should fail (missing option definition)
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(srv, json_bogus));
+ ASSERT_TRUE(x);
+ comment_ = parseAnswer(rcode_, x);
+ ASSERT_EQ(1, rcode_);
+
+ // This should work (option definition present)
+ EXPECT_NO_THROW(x = Dhcpv4SrvTest::configure(srv, json_valid));
+ ASSERT_TRUE(x);
+ comment_ = parseAnswer(rcode_, x);
+ ASSERT_EQ(0, rcode_);
+}
+
+/// Checks if DOCSIS client packets are classified properly
+///
+/// The test has been updated to work with the updated generic
+/// vendor options handling code.
+TEST_F(VendorOptsTest, docsisClientClassification) {
+
+ NakedDhcpv4Srv srv(0);
+
+ // Let's create a relayed DISCOVER. This particular relayed DISCOVER has
+ // vendor-class set to docsis3.0
+ Pkt4Ptr dis1;
+ ASSERT_NO_THROW(dis1 = PktCaptures::captureRelayedDiscover());
+ ASSERT_NO_THROW(dis1->unpack());
+
+ srv.classifyPacket(dis1);
+
+ 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
+ Pkt4Ptr dis2;
+ ASSERT_NO_THROW(dis2 = PktCaptures::captureRelayedDiscover2());
+ ASSERT_NO_THROW(dis2->unpack());
+
+ srv.classifyPacket(dis2);
+
+ EXPECT_TRUE(dis2->inClass(srv.VENDOR_CLASS_PREFIX + "eRouter1.0"));
+ EXPECT_FALSE(dis2->inClass(srv.VENDOR_CLASS_PREFIX + "docsis3.0:"));
+}
+
+// Checks that it's possible to have a vivso (125) option in the response
+// only. Once specific client (Genexis) sends only vendor-class info and
+// expects the server to include vivso in the response.
+TEST_F(VendorOptsTest, vivsoInResponseOnly) {
+ Dhcp4Client client;
+
+ // The config defines custom vendor 125 suboption 2 that conveys a TFTP URL.
+ // The client doesn't send vendor 125 option, so normal vendor option
+ // processing is impossible. However, since there's a class defined that
+ // matches client's packets and that class inserts vivso in the response,
+ // Kea should be able to figure out the vendor-id and then also insert
+ // suboption 2 with the TFTP URL.
+ string config =
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"option-def\": ["
+ " {"
+ " \"name\": \"tftp\","
+ " \"code\": 2,"
+ " \"space\": \"vendor-25167\","
+ " \"type\": \"string\""
+ " }"
+ " ],"
+ " \"client-classes\": ["
+ " {"
+ " \"name\": \"cpe_genexis\","
+ " \"test\": \"substring(option[60].hex,0,7) == 'HMC1000'\","
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"vivso-suboptions\","
+ " \"data\": \"25167\""
+ " },"
+ " {"
+ " \"name\": \"tftp\","
+ " \"space\": \"vendor-25167\","
+ " \"data\": \"tftp://192.0.2.1/genexis/HMC1000.v1.3.0-R.img\","
+ " \"always-send\": true"
+ " } ]"
+ " } ],"
+ "\"subnet4\": [ { "
+ " \"id\": 10,"
+ " \"pools\": [ { \"pool\": \"192.0.2.0/25\" } ],"
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"interface\": \"eth0\" "
+ " } ]"
+ "}";
+
+ EXPECT_NO_THROW(configure(config, *client.getServer()));
+
+ // Add a vendor-class identifier (this matches what Genexis hardware sends)
+ OptionPtr vopt(new OptionString(Option::V4, DHO_VENDOR_CLASS_IDENTIFIER,
+ "HMC1000.v1.3.0-R,Element-P1090,genexis.eu"));
+ client.addExtraOption(vopt);
+ client.requestOptions(DHO_VIVSO_SUBOPTIONS);
+
+ // Let's check whether the server is not able to process this packet
+ // and include vivso with appropriate sub-options
+ EXPECT_NO_THROW(client.doDiscover());
+ ASSERT_TRUE(client.getContext().response_);
+
+ // Check there's a response.
+ OptionPtr rsp = client.getContext().response_->getOption(DHO_VIVSO_SUBOPTIONS);
+ ASSERT_TRUE(rsp);
+
+ // Check that it includes vivso with vendor-id = 25167
+ OptionVendorPtr rsp_vivso = boost::dynamic_pointer_cast<OptionVendor>(rsp);
+ ASSERT_TRUE(rsp_vivso);
+ EXPECT_EQ(rsp_vivso->getVendorId(), 25167);
+
+ // Now check that it contains suboption 2 with appropriate content.
+ OptionPtr subopt2 = rsp_vivso->getOption(2);
+ ASSERT_TRUE(subopt2);
+ vector<uint8_t> subopt2bin = subopt2->toBinary(false);
+ string txt(subopt2bin.begin(), subopt2bin.end());
+ EXPECT_EQ("tftp://192.0.2.1/genexis/HMC1000.v1.3.0-R.img", txt);
+
+ // Check the config was not altered by unwanted side effect
+ // on the vendor option.
+
+ // Get class config:
+ ClientClassDefPtr cdef = CfgMgr::instance().getCurrentCfg()->
+ getClientClassDictionary()->findClass("cpe_genexis");
+ ASSERT_TRUE(cdef);
+ OptionDescriptor cdesc = cdef->getCfgOption()->
+ get(DHCP4_OPTION_SPACE, DHO_VIVSO_SUBOPTIONS);
+ ASSERT_TRUE(cdesc.option_);
+ // If the config was altered these two EXPECT will fail.
+ EXPECT_TRUE(cdesc.option_->getOptions().empty());
+ EXPECT_FALSE(cdesc.option_->getOption(2));
+}
+
+// Verifies last resort option 43 is backward compatible
+TEST_F(VendorOptsTest, option43LastResort) {
+ NakedDhcpv4Srv srv(0);
+
+ // If there is no definition for option 43 a last resort
+ // one is applied. This definition was used by Kea <= 1.2
+ // so should be backward compatible.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ] }, "
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], "
+ " \"id\": 10,"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"option-def\": [ "
+ "{ \"code\": 1, "
+ " \"name\": \"foo\", "
+ " \"space\": \"vendor-encapsulated-options-space\", "
+ " \"type\": \"uint32\" } ],"
+ "\"option-data\": [ "
+ "{ \"name\": \"foo\", "
+ " \"space\": \"vendor-encapsulated-options-space\", "
+ " \"data\": \"12345678\" }, "
+ "{ \"name\": \"vendor-class-identifier\", "
+ " \"data\": \"bar\" }, "
+ "{ \"name\": \"vendor-encapsulated-options\" } ] }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(srv, json));
+ ASSERT_TRUE(status);
+ comment_ = parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ CfgMgr::instance().commit();
+
+ // Create a packet with enough to select the subnet and go through
+ // the DISCOVER processing
+ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
+ query->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ query->addOption(clientid);
+ query->setIface("eth1");
+ query->setIndex(ETH1_INDEX);
+
+ // Create and add a PRL option to the query
+ OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4,
+ DHO_DHCP_PARAMETER_REQUEST_LIST));
+ ASSERT_TRUE(prl);
+ prl->addValue(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ prl->addValue(DHO_VENDOR_CLASS_IDENTIFIER);
+ query->addOption(prl);
+
+ srv.classifyPacket(query);
+ ASSERT_NO_THROW(srv.deferredUnpack(query));
+
+ // Pass it to the server and get a DHCPOFFER.
+ Pkt4Ptr offer = srv.processDiscover(query);
+
+ // Check if we get response at all
+ checkResponse(offer, DHCPOFFER, 1234);
+
+ // Processing should add a vendor-class-identifier (code 60)
+ OptionPtr opt = offer->getOption(DHO_VENDOR_CLASS_IDENTIFIER);
+ EXPECT_TRUE(opt);
+
+ // And a vendor-encapsulated-options (code 43)
+ opt = offer->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ ASSERT_TRUE(opt);
+ const OptionCollection& opts = opt->getOptions();
+ ASSERT_EQ(1, opts.size());
+ OptionPtr sopt = opts.begin()->second;
+ ASSERT_TRUE(sopt);
+ EXPECT_EQ(1, sopt->getType());
+}
+
+// Checks effect of raw not compatible option 43 (no failure)
+TEST_F(VendorOptsTest, option43BadRaw) {
+ NakedDhcpv4Srv srv(0);
+
+ // The vendor-encapsulated-options has an incompatible data
+ // so won't have the expected content but processing of truncated
+ // (suboption length > available length) suboptions does not raise
+ // an exception.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ] }, "
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], "
+ " \"id\": 10,"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"option-data\": [ "
+ "{ \"name\": \"vendor-class-identifier\", "
+ " \"data\": \"bar\" }, "
+ "{ \"name\": \"vendor-encapsulated-options\", "
+ " \"csv-format\": false, "
+ " \"data\": \"0102\" } ] }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(srv, json));
+ ASSERT_TRUE(status);
+ comment_ = parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ CfgMgr::instance().commit();
+
+ // Create a packet with enough to select the subnet and go through
+ // the DISCOVER processing
+ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
+ query->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ query->addOption(clientid);
+ query->setIface("eth1");
+ query->setIndex(ETH1_INDEX);
+
+ // Create and add a vendor-encapsulated-options (code 43)
+ // with not compatible (not parsable as suboptions) content
+ OptionBuffer buf;
+ buf.push_back(0x01);
+ buf.push_back(0x02);
+ OptionPtr vopt(new Option(Option::V4, DHO_VENDOR_ENCAPSULATED_OPTIONS, buf));
+ query->addOption(vopt);
+ query->getDeferredOptions().push_back(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+
+ // Create and add a PRL option to the query
+ OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4,
+ DHO_DHCP_PARAMETER_REQUEST_LIST));
+ ASSERT_TRUE(prl);
+ prl->addValue(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ prl->addValue(DHO_VENDOR_CLASS_IDENTIFIER);
+ query->addOption(prl);
+
+ srv.classifyPacket(query);
+ srv.deferredUnpack(query);
+
+ // Check if the option was (uncorrectly) re-unpacked
+ vopt = query->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ OptionCustomPtr custom = boost::dynamic_pointer_cast<OptionCustom>(vopt);
+ EXPECT_TRUE(custom);
+
+ // Pass it to the server and get a DHCPOFFER.
+ Pkt4Ptr offer = srv.processDiscover(query);
+
+ // Check if we get response at all
+ checkResponse(offer, DHCPOFFER, 1234);
+
+ // Processing should add a vendor-class-identifier (code 60)
+ OptionPtr opt = offer->getOption(DHO_VENDOR_CLASS_IDENTIFIER);
+ EXPECT_TRUE(opt);
+
+ // And a vendor-encapsulated-options (code 43)
+ opt = offer->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ ASSERT_TRUE(opt);
+ // But truncated.
+ EXPECT_EQ(0, opt->len() - opt->getHeaderLen());
+}
+
+// Checks effect of raw not compatible option 43 (failure)
+TEST_F(VendorOptsTest, option43FailRaw) {
+ NakedDhcpv4Srv srv(0);
+
+ // The vendor-encapsulated-options has an incompatible data
+ // so won't have the expected content. Here the processing
+ // of suboptions tries to unpack the uitn32 foo suboption and
+ // raises an exception which is caught so the option stays unpacked.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ] }, "
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], "
+ " \"id\": 10,"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"option-def\": [ "
+ "{ \"code\": 1, "
+ " \"name\": \"foo\", "
+ " \"space\": \"vendor-encapsulated-options-space\", "
+ " \"type\": \"uint32\" } ],"
+ "\"option-data\": [ "
+ "{ \"name\": \"vendor-class-identifier\", "
+ " \"data\": \"bar\" }, "
+ "{ \"name\": \"vendor-encapsulated-options\", "
+ " \"csv-format\": false, "
+ " \"data\": \"0102\" } ] }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(srv, json));
+ ASSERT_TRUE(status);
+ comment_ = parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ CfgMgr::instance().commit();
+
+ // Create a packet with enough to select the subnet and go through
+ // the DISCOVER processing
+ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
+ query->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ query->addOption(clientid);
+ query->setIface("eth1");
+ query->setIndex(ETH1_INDEX);
+
+ // Create and add a vendor-encapsulated-options (code 43)
+ // with not compatible (not parsable as suboptions) content
+ // which will raise an exception
+ OptionBuffer buf;
+ buf.push_back(0x01);
+ buf.push_back(0x01);
+ buf.push_back(0x01);
+ OptionPtr vopt(new Option(Option::V4, DHO_VENDOR_ENCAPSULATED_OPTIONS, buf));
+ query->addOption(vopt);
+ query->getDeferredOptions().push_back(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+
+ // Create and add a PRL option to the query
+ OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4,
+ DHO_DHCP_PARAMETER_REQUEST_LIST));
+ ASSERT_TRUE(prl);
+ prl->addValue(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ prl->addValue(DHO_VENDOR_CLASS_IDENTIFIER);
+ query->addOption(prl);
+
+ srv.classifyPacket(query);
+ EXPECT_NO_THROW(srv.deferredUnpack(query));
+ ASSERT_TRUE(query->getOption(vopt->getType()));
+ EXPECT_EQ(vopt, query->getOption(vopt->getType()));
+}
+
+// Verifies raw option 43 can be handled (global)
+TEST_F(VendorOptsTest, option43RawGlobal) {
+ NakedDhcpv4Srv srv(0);
+
+ // The vendor-encapsulated-options is redefined as raw binary
+ // in a global definition.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ] }, "
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], "
+ " \"id\": 10,"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"option-def\": [ "
+ "{ \"code\": 43, "
+ " \"name\": \"vendor-encapsulated-options\", "
+ " \"type\": \"binary\" } ],"
+ "\"option-data\": [ "
+ "{ \"name\": \"vendor-class-identifier\", "
+ " \"data\": \"bar\" }, "
+ "{ \"name\": \"vendor-encapsulated-options\", "
+ " \"csv-format\": false, "
+ " \"data\": \"0102\" } ] }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(srv, json));
+ ASSERT_TRUE(status);
+ comment_ = parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ CfgMgr::instance().commit();
+
+ // Create a packet with enough to select the subnet and go through
+ // the DISCOVER processing
+ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
+ query->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ query->addOption(clientid);
+ query->setIface("eth1");
+ query->setIndex(ETH1_INDEX);
+
+ // Create and add a vendor-encapsulated-options (code 43)
+ // with not compatible (not parsable as suboptions) content
+ OptionBuffer buf;
+ buf.push_back(0x02);
+ buf.push_back(0x03);
+ OptionPtr vopt(new Option(Option::V4, DHO_VENDOR_ENCAPSULATED_OPTIONS, buf));
+ query->addOption(vopt);
+ query->getDeferredOptions().push_back(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+
+ // Create and add a PRL option to the query
+ OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4,
+ DHO_DHCP_PARAMETER_REQUEST_LIST));
+ ASSERT_TRUE(prl);
+ prl->addValue(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ prl->addValue(DHO_VENDOR_CLASS_IDENTIFIER);
+ query->addOption(prl);
+
+ srv.classifyPacket(query);
+ ASSERT_NO_THROW(srv.deferredUnpack(query));
+
+ // Check if the option was (correctly) re-unpacked
+ vopt = query->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ OptionCustomPtr custom = boost::dynamic_pointer_cast<OptionCustom>(vopt);
+ EXPECT_FALSE(custom);
+
+ // Pass it to the server and get a DHCPOFFER.
+ Pkt4Ptr offer = srv.processDiscover(query);
+
+ // Check if we get response at all
+ checkResponse(offer, DHCPOFFER, 1234);
+
+ // Processing should add a vendor-class-identifier (code 60)
+ OptionPtr opt = offer->getOption(DHO_VENDOR_CLASS_IDENTIFIER);
+ EXPECT_TRUE(opt);
+
+ // And a vendor-encapsulated-options (code 43)
+ opt = offer->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ ASSERT_TRUE(opt);
+ // Verifies the content
+ ASSERT_EQ(2, opt->len() - opt->getHeaderLen());
+ EXPECT_EQ(0x01, opt->getData()[0]);
+ EXPECT_EQ(0x02, opt->getData()[1]);
+}
+
+// Verifies raw option 43 can be handled (catch-all class)
+TEST_F(VendorOptsTest, option43RawClass) {
+ NakedDhcpv4Srv srv(0);
+
+ // The vendor-encapsulated-options is redefined as raw binary
+ // in a class definition.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ] }, "
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], "
+ " \"id\": 10,"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"client-classes\": [ "
+ "{ \"name\": \"vendor\", "
+ " \"test\": \"option[vendor-encapsulated-options].exists\", "
+ " \"option-def\": [ "
+ " { \"code\": 43, "
+ " \"name\": \"vendor-encapsulated-options\", "
+ " \"type\": \"binary\" } ],"
+ " \"option-data\": [ "
+ " { \"name\": \"vendor-class-identifier\", "
+ " \"data\": \"bar\" }, "
+ " { \"name\": \"vendor-encapsulated-options\", "
+ " \"csv-format\": false, "
+ " \"data\": \"0102\" } ] } ] }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(srv, json));
+ ASSERT_TRUE(status);
+ comment_ = parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ CfgMgr::instance().commit();
+
+ // Create a packet with enough to select the subnet and go through
+ // the DISCOVER processing
+ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
+ query->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ query->addOption(clientid);
+ query->setIface("eth1");
+ query->setIndex(ETH1_INDEX);
+
+ // Create and add a vendor-encapsulated-options (code 43)
+ // with not compatible (not parsable as suboptions) content
+ OptionBuffer buf;
+ buf.push_back(0x02);
+ buf.push_back(0x03);
+ OptionPtr vopt(new Option(Option::V4, DHO_VENDOR_ENCAPSULATED_OPTIONS, buf));
+ query->addOption(vopt);
+ query->getDeferredOptions().push_back(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+
+ // Create and add a PRL option to the query
+ OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4,
+ DHO_DHCP_PARAMETER_REQUEST_LIST));
+ ASSERT_TRUE(prl);
+ prl->addValue(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ prl->addValue(DHO_VENDOR_CLASS_IDENTIFIER);
+ query->addOption(prl);
+
+ srv.classifyPacket(query);
+ ASSERT_NO_THROW(srv.deferredUnpack(query));
+
+ // Check if the option was (correctly) re-unpacked
+ vopt = query->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ OptionCustomPtr custom = boost::dynamic_pointer_cast<OptionCustom>(vopt);
+ EXPECT_FALSE(custom);
+
+ // Pass it to the server and get a DHCPOFFER.
+ Pkt4Ptr offer = srv.processDiscover(query);
+
+ // Check if we get response at all
+ checkResponse(offer, DHCPOFFER, 1234);
+
+ // Processing should add a vendor-class-identifier (code 60)
+ OptionPtr opt = offer->getOption(DHO_VENDOR_CLASS_IDENTIFIER);
+ EXPECT_TRUE(opt);
+
+ // And a vendor-encapsulated-options (code 43)
+ opt = offer->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ ASSERT_TRUE(opt);
+ // Verifies the content
+ ASSERT_EQ(2, opt->len() - opt->getHeaderLen());
+ EXPECT_EQ(0x01, opt->getData()[0]);
+ EXPECT_EQ(0x02, opt->getData()[1]);
+}
+
+// Verifies option 43 deferred processing (one class)
+TEST_F(VendorOptsTest, option43Class) {
+ NakedDhcpv4Srv srv(0);
+
+ // A client class defines vendor-encapsulated-options (code 43)
+ // and data for it and its sub-option.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ] }, "
+ "\"option-def\": [ "
+ "{ \"code\": 1, "
+ " \"name\": \"foo\", "
+ " \"space\": \"alpha\", "
+ " \"type\": \"uint32\" } ],"
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], "
+ " \"id\": 10,"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"client-classes\": [ "
+ "{ \"name\": \"alpha\", "
+ " \"test\": \"option[vendor-class-identifier].text == 'alpha'\", "
+ " \"option-def\": [ "
+ " { \"code\": 43, "
+ " \"name\": \"vendor-encapsulated-options\", "
+ " \"type\": \"empty\", "
+ " \"encapsulate\": \"alpha\" } ],"
+ " \"option-data\": [ "
+ " { \"name\": \"vendor-class-identifier\", "
+ " \"data\": \"alpha\" }, "
+ " { \"name\": \"vendor-encapsulated-options\" }, "
+ " { \"name\": \"foo\", "
+ " \"space\": \"alpha\", "
+ " \"data\": \"12345678\" } ] } ] }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(srv, json));
+ ASSERT_TRUE(status);
+ comment_ = parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ CfgMgr::instance().commit();
+
+ // Create a packet with enough to select the subnet and go through
+ // the DISCOVER processing
+ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
+ query->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ query->addOption(clientid);
+ query->setIface("eth1");
+ query->setIndex(ETH1_INDEX);
+
+ // Create and add a vendor-encapsulated-options (code 43)
+ OptionBuffer buf;
+ buf.push_back(0x01);
+ buf.push_back(0x04);
+ buf.push_back(0x87);
+ buf.push_back(0x65);
+ buf.push_back(0x43);
+ buf.push_back(0x21);
+ OptionPtr vopt(new Option(Option::V4, DHO_VENDOR_ENCAPSULATED_OPTIONS, buf));
+ query->addOption(vopt);
+ query->getDeferredOptions().push_back(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+
+ // Create and add a vendor-class-identifier (code 60)
+ OptionStringPtr iopt(new OptionString(Option::V4,
+ DHO_VENDOR_CLASS_IDENTIFIER,
+ "alpha"));
+ query->addOption(iopt);
+
+ // Create and add a PRL option to the query
+ OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4,
+ DHO_DHCP_PARAMETER_REQUEST_LIST));
+ ASSERT_TRUE(prl);
+ prl->addValue(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ prl->addValue(DHO_VENDOR_CLASS_IDENTIFIER);
+ query->addOption(prl);
+
+ srv.classifyPacket(query);
+ ASSERT_NO_THROW(srv.deferredUnpack(query));
+
+ // Check if the option was (correctly) re-unpacked
+ vopt = query->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ OptionCustomPtr custom = boost::dynamic_pointer_cast<OptionCustom>(vopt);
+ EXPECT_TRUE(custom);
+ EXPECT_EQ(1, vopt->getOptions().size());
+
+ // Pass it to the server and get a DHCPOFFER.
+ Pkt4Ptr offer = srv.processDiscover(query);
+
+ // Check if we get response at all
+ checkResponse(offer, DHCPOFFER, 1234);
+
+ // Processing should add a vendor-class-identifier (code 60)
+ OptionPtr opt = offer->getOption(DHO_VENDOR_CLASS_IDENTIFIER);
+ EXPECT_TRUE(opt);
+
+ // And a vendor-encapsulated-options (code 43)
+ opt = offer->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ ASSERT_TRUE(opt);
+ // Verifies the content
+ const OptionCollection& opts = opt->getOptions();
+ ASSERT_EQ(1, opts.size());
+ OptionPtr sopt = opts.begin()->second;
+ ASSERT_TRUE(sopt);
+ EXPECT_EQ(1, sopt->getType());
+ OptionUint32Ptr sopt32 = boost::dynamic_pointer_cast<OptionUint32>(sopt);
+ ASSERT_TRUE(sopt32);
+ EXPECT_EQ(12345678, sopt32->getValue());
+}
+
+// Verifies option 43 priority
+TEST_F(VendorOptsTest, option43ClassPriority) {
+ NakedDhcpv4Srv srv(0);
+
+ // Both global and client-class scopes get vendor-encapsulated-options
+ // (code 43) definition and data. The client-class has precedence.
+ // Note it does not work without the vendor-encapsulated-options
+ // option-data in the client-class.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ] }, "
+ "\"option-def\": [ "
+ "{ \"code\": 1, "
+ " \"name\": \"foo\", "
+ " \"space\": \"alpha\", "
+ " \"type\": \"uint32\" },"
+ "{ \"code\": 1, "
+ " \"name\": \"bar\", "
+ " \"space\": \"beta\", "
+ " \"type\": \"uint8\" }, "
+ "{ \"code\": 43, "
+ " \"name\": \"vendor-encapsulated-options\", "
+ " \"type\": \"empty\", "
+ " \"encapsulate\": \"beta\" } ],"
+ "\"option-data\": [ "
+ "{ \"name\": \"vendor-encapsulated-options\" }, "
+ "{ \"name\": \"vendor-class-identifier\", "
+ " \"data\": \"beta\" }, "
+ "{ \"name\": \"bar\", "
+ " \"space\": \"beta\", "
+ " \"data\": \"33\" } ],"
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], "
+ " \"id\": 10,"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"client-classes\": [ "
+ "{ \"name\": \"alpha\", "
+ " \"test\": \"option[vendor-class-identifier].text == 'alpha'\", "
+ " \"option-def\": [ "
+ " { \"code\": 43, "
+ " \"name\": \"vendor-encapsulated-options\", "
+ " \"type\": \"empty\", "
+ " \"encapsulate\": \"alpha\" } ],"
+ " \"option-data\": [ "
+ "{ \"name\": \"vendor-encapsulated-options\" }, "
+ " { \"name\": \"vendor-class-identifier\", "
+ " \"data\": \"alpha\" }, "
+ " { \"name\": \"foo\", "
+ " \"space\": \"alpha\", "
+ " \"data\": \"12345678\" } ] } ] }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(srv, json));
+ ASSERT_TRUE(status);
+ comment_ = parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ CfgMgr::instance().commit();
+
+ // Create a packet with enough to select the subnet and go through
+ // the DISCOVER processing
+ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
+ query->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ query->addOption(clientid);
+ query->setIface("eth1");
+ query->setIndex(ETH1_INDEX);
+
+ // Create and add a vendor-encapsulated-options (code 43)
+ OptionBuffer buf;
+ buf.push_back(0x01);
+ buf.push_back(0x04);
+ buf.push_back(0x87);
+ buf.push_back(0x65);
+ buf.push_back(0x43);
+ buf.push_back(0x21);
+ OptionPtr vopt(new Option(Option::V4, DHO_VENDOR_ENCAPSULATED_OPTIONS, buf));
+ query->addOption(vopt);
+ query->getDeferredOptions().push_back(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+
+ // Create and add a vendor-class-identifier (code 60)
+ OptionStringPtr iopt(new OptionString(Option::V4,
+ DHO_VENDOR_CLASS_IDENTIFIER,
+ "alpha"));
+ query->addOption(iopt);
+
+ // Create and add a PRL option to the query
+ OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4,
+ DHO_DHCP_PARAMETER_REQUEST_LIST));
+ ASSERT_TRUE(prl);
+ prl->addValue(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ prl->addValue(DHO_VENDOR_CLASS_IDENTIFIER);
+ query->addOption(prl);
+
+ srv.classifyPacket(query);
+ ASSERT_NO_THROW(srv.deferredUnpack(query));
+
+ // Check if the option was (correctly) re-unpacked
+ vopt = query->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ OptionCustomPtr custom = boost::dynamic_pointer_cast<OptionCustom>(vopt);
+ EXPECT_TRUE(custom);
+ EXPECT_EQ(1, vopt->getOptions().size());
+
+ // Pass it to the server and get a DHCPOFFER.
+ Pkt4Ptr offer = srv.processDiscover(query);
+
+ // Check if we get response at all
+ checkResponse(offer, DHCPOFFER, 1234);
+
+ // Processing should add a vendor-class-identifier (code 60)
+ OptionPtr opt = offer->getOption(DHO_VENDOR_CLASS_IDENTIFIER);
+ EXPECT_TRUE(opt);
+ OptionStringPtr id = boost::dynamic_pointer_cast<OptionString>(opt);
+ ASSERT_TRUE(id);
+ EXPECT_EQ("alpha", id->getValue());
+
+ // And a vendor-encapsulated-options (code 43)
+ opt = offer->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ ASSERT_TRUE(opt);
+ // Verifies the content
+ const OptionCollection& opts = opt->getOptions();
+ ASSERT_EQ(1, opts.size());
+ OptionPtr sopt = opts.begin()->second;
+ ASSERT_TRUE(sopt);
+ EXPECT_EQ(1, sopt->getType());
+ EXPECT_EQ(2 + 4, sopt->len());
+ OptionUint32Ptr sopt32 = boost::dynamic_pointer_cast<OptionUint32>(sopt);
+ ASSERT_TRUE(sopt32);
+ EXPECT_EQ(12345678, sopt32->getValue());
+}
+
+// Verifies option 43 deferred processing (two classes)
+TEST_F(VendorOptsTest, option43Classes) {
+ NakedDhcpv4Srv srv(0);
+
+ // Two client-class scopes get vendor-encapsulated-options
+ // (code 43) definition and data. The first matching client-class
+ // (from a set?) applies.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ] }, "
+ "\"option-def\": [ "
+ "{ \"code\": 1, "
+ " \"name\": \"foo\", "
+ " \"space\": \"alpha\", "
+ " \"type\": \"uint32\" },"
+ "{ \"code\": 1, "
+ " \"name\": \"bar\", "
+ " \"space\": \"beta\", "
+ " \"type\": \"uint8\" } ],"
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], "
+ " \"id\": 10,"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"client-classes\": [ "
+ "{ \"name\": \"alpha\", "
+ " \"test\": \"option[vendor-class-identifier].text == 'alpha'\", "
+ " \"option-def\": [ "
+ " { \"code\": 43, "
+ " \"name\": \"vendor-encapsulated-options\", "
+ " \"type\": \"empty\", "
+ " \"encapsulate\": \"alpha\" } ],"
+ " \"option-data\": [ "
+ "{ \"name\": \"vendor-encapsulated-options\" }, "
+ " { \"name\": \"vendor-class-identifier\", "
+ " \"data\": \"alpha\" }, "
+ " { \"name\": \"foo\", "
+ " \"space\": \"alpha\", "
+ " \"data\": \"12345678\" } ] },"
+ "{ \"name\": \"beta\", "
+ " \"test\": \"option[vendor-class-identifier].text == 'beta'\", "
+ " \"option-def\": [ "
+ " { \"code\": 43, "
+ " \"name\": \"vendor-encapsulated-options\", "
+ " \"type\": \"empty\", "
+ " \"encapsulate\": \"beta\" } ],"
+ " \"option-data\": [ "
+ "{ \"name\": \"vendor-encapsulated-options\" }, "
+ " { \"name\": \"vendor-class-identifier\", "
+ " \"data\": \"beta\" }, "
+ " { \"name\": \"bar\", "
+ " \"space\": \"beta\", "
+ " \"data\": \"33\" } ] } ] }";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(srv, json));
+ ASSERT_TRUE(status);
+ comment_ = parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ CfgMgr::instance().commit();
+
+ // Create a packet with enough to select the subnet and go through
+ // the DISCOVER processing
+ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
+ query->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ query->addOption(clientid);
+ query->setIface("eth1");
+ query->setIndex(ETH1_INDEX);
+
+ // Create and add a vendor-encapsulated-options (code 43)
+ OptionBuffer buf;
+ buf.push_back(0x01);
+ buf.push_back(0x04);
+ buf.push_back(0x87);
+ buf.push_back(0x65);
+ buf.push_back(0x43);
+ buf.push_back(0x21);
+ OptionPtr vopt(new Option(Option::V4, DHO_VENDOR_ENCAPSULATED_OPTIONS, buf));
+ query->addOption(vopt);
+ query->getDeferredOptions().push_back(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+
+ // Create and add a vendor-class-identifier (code 60)
+ OptionStringPtr iopt(new OptionString(Option::V4,
+ DHO_VENDOR_CLASS_IDENTIFIER,
+ "alpha"));
+ query->addOption(iopt);
+
+ // Create and add a PRL option to the query
+ OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4,
+ DHO_DHCP_PARAMETER_REQUEST_LIST));
+ ASSERT_TRUE(prl);
+ prl->addValue(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ prl->addValue(DHO_VENDOR_CLASS_IDENTIFIER);
+ query->addOption(prl);
+
+ srv.classifyPacket(query);
+ ASSERT_NO_THROW(srv.deferredUnpack(query));
+
+ // Check if the option was (correctly) re-unpacked
+ vopt = query->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ OptionCustomPtr custom = boost::dynamic_pointer_cast<OptionCustom>(vopt);
+ EXPECT_TRUE(custom);
+ EXPECT_EQ(1, vopt->getOptions().size());
+
+ // Pass it to the server and get a DHCPOFFER.
+ Pkt4Ptr offer = srv.processDiscover(query);
+
+ // Check if we get response at all
+ checkResponse(offer, DHCPOFFER, 1234);
+
+ // Processing should add a vendor-class-identifier (code 60)
+ OptionPtr opt = offer->getOption(DHO_VENDOR_CLASS_IDENTIFIER);
+ EXPECT_TRUE(opt);
+ OptionStringPtr id = boost::dynamic_pointer_cast<OptionString>(opt);
+ ASSERT_TRUE(id);
+ EXPECT_EQ("alpha", id->getValue());
+
+ // And a vendor-encapsulated-options (code 43)
+ opt = offer->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ ASSERT_TRUE(opt);
+ // Verifies the content
+ const OptionCollection& opts = opt->getOptions();
+ ASSERT_EQ(1, opts.size());
+ OptionPtr sopt = opts.begin()->second;
+ ASSERT_TRUE(sopt);
+ EXPECT_EQ(1, sopt->getType());
+ EXPECT_EQ(2 + 4, sopt->len());
+ OptionUint32Ptr sopt32 = boost::dynamic_pointer_cast<OptionUint32>(sopt);
+ ASSERT_TRUE(sopt32);
+ EXPECT_EQ(12345678, sopt32->getValue());
+}
+
+// Checks effect of raw not compatible option 43 sent by a client (failure)
+TEST_F(VendorOptsTest, clientOption43FailRaw) {
+ Dhcp4Client client;
+
+ // The vendor-encapsulated-options has an incompatible data
+ // so won't have the expected content. Here the processing
+ // of suboptions tries to unpack the uint32 foo suboption and
+ // raises an exception which is caught.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ] }, "
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { \"pool\": \"10.0.0.10 - 10.0.0.100\" } ], "
+ " \"id\": 10,"
+ " \"subnet\": \"10.0.0.0/24\" } ],"
+ "\"option-def\": [ "
+ "{ \"code\": 1, "
+ " \"name\": \"foo\", "
+ " \"space\": \"vendor-encapsulated-options-space\", "
+ " \"type\": \"uint32\" } ] }";
+
+ EXPECT_NO_THROW(configure(config, *client.getServer()));
+
+ // Create and add a vendor-encapsulated-options (code 43)
+ // with not compatible (not parsable as suboptions) content
+ // which will raise an exception
+ OptionBuffer buf;
+ buf.push_back(0x01);
+ buf.push_back(0x01);
+ buf.push_back(0x01);
+ OptionPtr vopt(new Option(Option::V4, DHO_VENDOR_ENCAPSULATED_OPTIONS, buf));
+ client.addExtraOption(vopt);
+
+ // Let's check whether the server is not able to process this packet
+ // and raises an exception which is caught so the response is not empty.
+ EXPECT_NO_THROW(client.doDiscover());
+ EXPECT_TRUE(client.getContext().response_);
+}
+
+// Verifies raw option 43 sent by a client can be handled (global)
+TEST_F(VendorOptsTest, clientOption43RawGlobal) {
+ Dhcp4Client client;
+
+ // The vendor-encapsulated-options is redefined as raw binary
+ // in a global definition.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ] }, "
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { \"pool\": \"10.0.0.10 - 10.0.0.100\" } ], "
+ " \"id\": 10,"
+ " \"subnet\": \"10.0.0.0/24\" } ],"
+ "\"option-def\": [ "
+ "{ \"code\": 1, "
+ " \"name\": \"foo\", "
+ " \"space\": \"vendor-encapsulated-options-space\", "
+ " \"type\": \"uint32\" },"
+ "{ \"code\": 43, "
+ " \"name\": \"vendor-encapsulated-options\", "
+ " \"type\": \"binary\" } ],"
+ "\"option-data\": [ "
+ "{ \"name\": \"vendor-class-identifier\", "
+ " \"data\": \"bar\" }, "
+ "{ \"name\": \"vendor-encapsulated-options\", "
+ " \"csv-format\": false, "
+ " \"data\": \"0102\" } ] }";
+
+ EXPECT_NO_THROW(configure(config, *client.getServer()));
+
+ // Create and add a vendor-encapsulated-options (code 43)
+ // with not compatible (not parsable as suboptions) content
+ OptionBuffer buf;
+ buf.push_back(0x01);
+ buf.push_back(0x01);
+ buf.push_back(0x01);
+ OptionPtr vopt(new Option(Option::V4, DHO_VENDOR_ENCAPSULATED_OPTIONS, buf));
+ client.addExtraOption(vopt);
+
+ // Let's check whether the server is able to process this packet without
+ // throwing any exceptions so the response is not empty.
+ EXPECT_NO_THROW(client.doDiscover());
+ EXPECT_TRUE(client.getContext().response_);
+}
+
+// Verifies raw option 43 sent by a client can be handled (catch-all class)
+TEST_F(VendorOptsTest, clientOption43RawClass) {
+ Dhcp4Client client;
+
+ // The vendor-encapsulated-options is redefined as raw binary
+ // in a class definition.
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ] }, "
+ "\"subnet4\": [ "
+ "{ \"pools\": [ { \"pool\": \"10.0.0.10 - 10.0.0.100\" } ], "
+ " \"id\": 10,"
+ " \"subnet\": \"10.0.0.0/24\" } ],"
+ "\"option-def\": [ "
+ "{ \"code\": 1, "
+ " \"name\": \"foo\", "
+ " \"space\": \"vendor-encapsulated-options-space\", "
+ " \"type\": \"uint32\" } ],"
+ "\"client-classes\": [ "
+ "{ \"name\": \"vendor\", "
+ " \"test\": \"option[vendor-encapsulated-options].exists\", "
+ " \"option-def\": [ "
+ " { \"code\": 43, "
+ " \"name\": \"vendor-encapsulated-options\", "
+ " \"type\": \"binary\" } ],"
+ " \"option-data\": [ "
+ " { \"name\": \"vendor-class-identifier\", "
+ " \"data\": \"bar\" }, "
+ " { \"name\": \"vendor-encapsulated-options\", "
+ " \"csv-format\": false, "
+ " \"data\": \"0102\" } ] } ] }";
+
+ EXPECT_NO_THROW(configure(config, *client.getServer()));
+
+ // Create and add a vendor-encapsulated-options (code 43)
+ // with not compatible (not parsable as suboptions) content
+ OptionBuffer buf;
+ buf.push_back(0x01);
+ buf.push_back(0x01);
+ buf.push_back(0x01);
+ OptionPtr vopt(new Option(Option::V4, DHO_VENDOR_ENCAPSULATED_OPTIONS, buf));
+ client.addExtraOption(vopt);
+
+ // Let's check whether the server is able to process this packet without
+ // throwing any exceptions so the response is not empty.
+ EXPECT_NO_THROW(client.doDiscover());
+ EXPECT_TRUE(client.getContext().response_);
+}
+
+// Verifies that a client query with a truncated length in
+// vendor option (125) will still be processed by the server.
+TEST_F(Dhcpv4SrvTest, truncatedVIVSOOption) {
+ NakedDhcpv4Srv srv(0);
+
+ string config = "{ \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"10.206.80.0/25\" } ],"
+ " \"id\": 10,"
+ " \"subnet\": \"10.206.80.0/24\", "
+ " \"rebind-timer\": 2000, "
+ " \"renew-timer\": 1000, "
+ " \"valid-lifetime\": 4000"
+ " } ]"
+ "}";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config));
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(srv, json));
+ ASSERT_TRUE(status);
+ comment_ = parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_) << isc::data::prettyPrint(status);
+
+ CfgMgr::instance().commit();
+
+ // Create a DISCOVER with a VIVSO option whose length is
+ // too short.
+ Pkt4Ptr dis;
+ ASSERT_NO_THROW(dis = PktCaptures::discoverWithTruncatedVIVSO());
+
+ // Simulate that we have received that traffic
+ srv.fakeReceive(dis);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered buffer4_receive callback.
+ srv.run();
+
+ // Check that the server did send a response
+ ASSERT_EQ(1, srv.fake_sent_.size());
+
+ // Make sure that we received an response and it was a DHCPOFFER.
+ Pkt4Ptr offer = srv.fake_sent_.front();
+ ASSERT_TRUE(offer);
+}
+
+/// Checks that it's possible to define and use a suboption 0.
+TEST_F(VendorOptsTest, vendorOpsSubOption0) {
+ NakedDhcpv4Srv srv(0);
+
+ // Zero Touch provisioning
+ string config =
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ " },"
+ " \"option-def\": ["
+ " {"
+ " \"name\": \"vendor-encapsulated-options\","
+ " \"code\": 43,"
+ " \"type\": \"empty\","
+ " \"encapsulate\": \"ZTP\""
+ " },"
+ " {"
+ " \"name\": \"config-file-name\","
+ " \"code\": 1,"
+ " \"space\": \"ZTP\","
+ " \"type\": \"string\""
+ " },"
+ " {"
+ " \"name\": \"image-file-name\","
+ " \"code\": 0,"
+ " \"space\": \"ZTP\","
+ " \"type\": \"string\""
+ " },"
+ " {"
+ " \"name\": \"image-file-type\","
+ " \"code\": 2,"
+ " \"space\": \"ZTP\","
+ " \"type\": \"string\""
+ " },"
+ " {"
+ " \"name\": \"transfer-mode\","
+ " \"code\": 3,"
+ " \"space\": \"ZTP\","
+ " \"type\": \"string\""
+ " },"
+ " {"
+ " \"name\": \"all-image-file-name\","
+ " \"code\": 4,"
+ " \"space\": \"ZTP\","
+ " \"type\": \"string\""
+ " },"
+ " {"
+ " \"name\": \"http-port\","
+ " \"code\": 5,"
+ " \"space\": \"ZTP\","
+ " \"type\": \"string\""
+ " }"
+ " ],"
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"vendor-encapsulated-options\""
+ " },"
+ " {"
+ " \"name\": \"image-file-name\","
+ " \"data\": \"/dist/images/jinstall-ex.tgz\","
+ " \"space\": \"ZTP\""
+ " }"
+ " ],"
+ "\"subnet4\": [ { "
+ " \"id\": 10,"
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"subnet\": \"192.0.2.0/24\""
+ " } ]"
+ "}";
+
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = parseDHCP4(config, true));
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = Dhcpv4SrvTest::configure(srv, json));
+ ASSERT_TRUE(status);
+ comment_ = parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ CfgMgr::instance().commit();
+
+ // Create a packet with enough to select the subnet and go through
+ // the DISCOVER processing
+ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
+ query->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ query->addOption(clientid);
+ query->setIface("eth1");
+ query->setIndex(ETH1_INDEX);
+
+ // Create and add a PRL option to the query
+ OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4,
+ DHO_DHCP_PARAMETER_REQUEST_LIST));
+ ASSERT_TRUE(prl);
+ prl->addValue(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ prl->addValue(DHO_VENDOR_CLASS_IDENTIFIER);
+ query->addOption(prl);
+
+ srv.classifyPacket(query);
+ ASSERT_NO_THROW(srv.deferredUnpack(query));
+
+ // Pass it to the server and get a DHCPOFFER.
+ Pkt4Ptr offer = srv.processDiscover(query);
+
+ // Check if we get response at all
+ checkResponse(offer, DHCPOFFER, 1234);
+
+ // Processing should add a vendor-encapsulated-options (code 43)
+ OptionPtr opt = offer->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ ASSERT_TRUE(opt);
+ const OptionCollection& opts = opt->getOptions();
+ ASSERT_EQ(1, opts.size());
+ OptionPtr sopt = opts.begin()->second;
+ ASSERT_TRUE(sopt);
+ EXPECT_EQ(0, sopt->getType());
+
+ // Check suboption 0 content.
+ OptionStringPtr sopt0 =
+ boost::dynamic_pointer_cast<OptionString>(sopt);
+ ASSERT_TRUE(sopt0);
+ EXPECT_EQ("/dist/images/jinstall-ex.tgz", sopt0->getValue());
+}
+
+// Checks if it's possible to have 2 vivco options with different vendor IDs.
+TEST_F(VendorOptsTest, twoVivcos) {
+ Dhcp4Client client;
+
+ // The config defines 2 vendors with for each a vivco option,
+ // having the always send flag set to true.
+ // The encoding for the option-class option is a bit hairy: first is
+ // the vendor id (uint32) and the remaining is a binary which stands
+ // for a tuple so length (uint8) x value.
+ string config =
+ "{"
+ " \"interfaces-config\": {"
+ " \"interfaces\": [ ]"
+ " },"
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"vivco-suboptions\","
+ " \"always-send\": true,"
+ " \"data\": \"1234, 03666f6f\""
+ " },"
+ " {"
+ " \"name\": \"vivco-suboptions\","
+ " \"always-send\": true,"
+ " \"data\": \"5678, 03626172\""
+ " }"
+ " ],"
+ "\"subnet4\": [ { "
+ " \"id\": 10,"
+ " \"pools\": [ { \"pool\": \"10.0.0.10 - 10.0.0.100\" } ],"
+ " \"subnet\": \"10.0.0.0/24\", "
+ " \"interface\": \"eth0\" "
+ " } ]"
+ "}";
+
+ EXPECT_NO_THROW(configure(config, *client.getServer()));
+
+ // Let's check whether the server is able to process this packet.
+ EXPECT_NO_THROW(client.doDiscover());
+ ASSERT_TRUE(client.getContext().response_);
+
+ // Check whether there are vivco options.
+ const OptionCollection& classes =
+ client.getContext().response_->getOptions(DHO_VIVCO_SUBOPTIONS);
+ ASSERT_EQ(2, classes.size());
+ OptionVendorClassPtr opt_class1234;
+ OptionVendorClassPtr opt_class5678;
+ for (auto opt : classes) {
+ ASSERT_EQ(DHO_VIVCO_SUBOPTIONS, opt.first);
+ OptionVendorClassPtr opt_class =
+ boost::dynamic_pointer_cast<OptionVendorClass>(opt.second);
+ ASSERT_TRUE(opt_class);
+ uint32_t vendor_id = opt_class->getVendorId();
+ if (vendor_id == 1234) {
+ ASSERT_FALSE(opt_class1234);
+ opt_class1234 = opt_class;
+ continue;
+ }
+ ASSERT_EQ(5678, vendor_id);
+ ASSERT_FALSE(opt_class5678);
+ opt_class5678 = opt_class;
+ }
+
+ // Verify first vivco option.
+ ASSERT_TRUE(opt_class1234);
+ ASSERT_EQ(1, opt_class1234->getTuplesNum());
+ EXPECT_EQ("foo", opt_class1234->getTuple(0).getText());
+
+ // Verify second vivco option.
+ ASSERT_TRUE(opt_class5678);
+ ASSERT_EQ(1, opt_class5678->getTuplesNum());
+ EXPECT_EQ("bar", opt_class5678->getTuple(0).getText());
+}