diff options
Diffstat (limited to 'src/bin/dhcp4/tests')
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='[0;31m'; \ + grn='[0;32m'; \ + lgn='[1;32m'; \ + blu='[1;34m'; \ + mgn='[0;35m'; \ + brg='[1m'; \ + std='[m'; \ + 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()); +} |