diff options
4 files changed, 13415 insertions, 1 deletions
diff --git a/data/live-build-config/hooks/live/18-enable-disable_services.chroot b/data/live-build-config/hooks/live/18-enable-disable_services.chroot index 3b4efe38..21b41b23 100755 --- a/data/live-build-config/hooks/live/18-enable-disable_services.chroot +++ b/data/live-build-config/hooks/live/18-enable-disable_services.chroot @@ -72,6 +72,8 @@ systemctl disable suricata.service systemctl disable vyconfd.service systemctl disable vpp.service systemctl disable vyos-commitd.service +systemctl disable netplug.service + echo I: Enabling services systemctl enable vyos-hostsd.service diff --git a/scripts/package-build/kea/package.toml b/scripts/package-build/kea/package.toml index 0bfce21e..872be441 100644 --- a/scripts/package-build/kea/package.toml +++ b/scripts/package-build/kea/package.toml @@ -1,4 +1,4 @@ [[packages]] name = "isc-kea" -commit_id = "debian/2.4.1-3" +commit_id = "debian/2.6.1-2" scm_url = "https://salsa.debian.org/debian/isc-kea" diff --git a/scripts/package-build/kea/patches/isc-kea/0001-Add-multithreading-test-mode.patch b/scripts/package-build/kea/patches/isc-kea/0001-Add-multithreading-test-mode.patch new file mode 100644 index 00000000..981e6f1d --- /dev/null +++ b/scripts/package-build/kea/patches/isc-kea/0001-Add-multithreading-test-mode.patch @@ -0,0 +1,135 @@ +From cb2b064162e2d5bf09331c619abf76a40130ade1 Mon Sep 17 00:00:00 2001 +From: sarthurdev <s.arthur@vyos.io> +Date: Wed, 2 Apr 2025 08:48:48 +0000 +Subject: [PATCH 1/2] Add multithreading test mode + +--- + src/bin/dhcp4/json_config_parser.cc | 4 ++++ + src/bin/dhcp6/json_config_parser.cc | 6 +++++- + src/lib/config/cmd_http_listener.cc | 3 +++ + src/lib/tcp/mt_tcp_listener_mgr.cc | 3 +++ + src/lib/util/multi_threading_mgr.cc | 3 ++- + src/lib/util/multi_threading_mgr.h | 19 +++++++++++++++++++ + 6 files changed, 36 insertions(+), 2 deletions(-) + +diff --git a/src/bin/dhcp4/json_config_parser.cc b/src/bin/dhcp4/json_config_parser.cc +index c2e34c5..1350816 100644 +--- a/src/bin/dhcp4/json_config_parser.cc ++++ b/src/bin/dhcp4/json_config_parser.cc +@@ -718,6 +718,10 @@ configureDhcp4Server(Dhcpv4Srv& server, isc::data::ConstElementPtr config_set, + LOG_DEBUG(dhcp4_logger, DBG_DHCP4_COMMAND, DHCP4_CONFIG_START) + .arg(server.redactConfig(config_set)->str()); + ++ if (check_only) { ++ MultiThreadingMgr::instance().setTestMode(true); ++ } ++ + auto answer = processDhcp4Config(config_set); + + int status_code = CONTROL_RESULT_SUCCESS; +diff --git a/src/bin/dhcp6/json_config_parser.cc b/src/bin/dhcp6/json_config_parser.cc +index 671d69a..a74a568 100644 +--- a/src/bin/dhcp6/json_config_parser.cc ++++ b/src/bin/dhcp6/json_config_parser.cc +@@ -850,6 +850,10 @@ configureDhcp6Server(Dhcpv6Srv& server, isc::data::ConstElementPtr config_set, + LOG_DEBUG(dhcp6_logger, DBG_DHCP6_COMMAND, DHCP6_CONFIG_START) + .arg(server.redactConfig(config_set)->str()); + ++ if (check_only) { ++ MultiThreadingMgr::instance().setTestMode(true); ++ } ++ + auto answer = processDhcp6Config(config_set); + + int status_code = CONTROL_RESULT_SUCCESS; +@@ -953,7 +957,7 @@ configureDhcp6Server(Dhcpv6Srv& server, isc::data::ConstElementPtr config_set, + // configuration. This will add created subnets and option values into + // the server's configuration. + // This operation should be exception safe but let's make sure. +- if (status_code == CONTROL_RESULT_SUCCESS && (!check_only || extra_checks)) { ++ if (status_code == CONTROL_RESULT_SUCCESS && !check_only) { + try { + + // Setup the command channel. +diff --git a/src/lib/config/cmd_http_listener.cc b/src/lib/config/cmd_http_listener.cc +index 9dfea59..394806e 100644 +--- a/src/lib/config/cmd_http_listener.cc ++++ b/src/lib/config/cmd_http_listener.cc +@@ -40,6 +40,9 @@ CmdHttpListener::~CmdHttpListener() { + + void + CmdHttpListener::start() { ++ if (MultiThreadingMgr::instance().isTestMode()) { ++ return; ++ } + // We must be in multi-threading mode. + if (!MultiThreadingMgr::instance().getMode()) { + isc_throw(InvalidOperation, "CmdHttpListener cannot be started" +diff --git a/src/lib/tcp/mt_tcp_listener_mgr.cc b/src/lib/tcp/mt_tcp_listener_mgr.cc +index e880284..4680717 100644 +--- a/src/lib/tcp/mt_tcp_listener_mgr.cc ++++ b/src/lib/tcp/mt_tcp_listener_mgr.cc +@@ -40,6 +40,9 @@ MtTcpListenerMgr::~MtTcpListenerMgr() { + + void + MtTcpListenerMgr::start() { ++ if (MultiThreadingMgr::instance().isTestMode()) { ++ return; ++ } + // We must be in multi-threading mode. + if (!MultiThreadingMgr::instance().getMode()) { + isc_throw(InvalidOperation, "MtTcpListenerMgr cannot be started" +diff --git a/src/lib/util/multi_threading_mgr.cc b/src/lib/util/multi_threading_mgr.cc +index d1526b9..cab284d 100644 +--- a/src/lib/util/multi_threading_mgr.cc ++++ b/src/lib/util/multi_threading_mgr.cc +@@ -14,7 +14,8 @@ namespace isc { + namespace util { + + MultiThreadingMgr::MultiThreadingMgr() +- : enabled_(false), critical_section_count_(0), thread_pool_size_(0) { ++ : enabled_(false), test_mode_(false), critical_section_count_(0), ++ thread_pool_size_(0) { + } + + MultiThreadingMgr::~MultiThreadingMgr() { +diff --git a/src/lib/util/multi_threading_mgr.h b/src/lib/util/multi_threading_mgr.h +index e86c488..f3da67b 100644 +--- a/src/lib/util/multi_threading_mgr.h ++++ b/src/lib/util/multi_threading_mgr.h +@@ -154,6 +154,22 @@ public: + /// @param enabled The new mode. + void setMode(bool enabled); + ++ /// @brief Sets or clears the test mode for @c MultiThreadingMgr. ++ /// ++ /// @param test_mode A flag which indicates that the @c MultiThreadingMgr is ++ /// in the test mode (if true), or not (if false). ++ void setTestMode(const bool test_mode) { ++ test_mode_ = test_mode; ++ } ++ ++ /// @brief Checks if the @c MultiThreadingMgr is in the test mode. ++ /// ++ /// @return true if the @c MultiThreadingMgr is in the test mode, false ++ /// otherwise. ++ bool isTestMode() const { ++ return (test_mode_); ++ } ++ + /// @brief Enter critical section. + /// + /// When entering @ref MultiThreadingCriticalSection, increment internal +@@ -308,6 +324,9 @@ private: + /// otherwise. + bool enabled_; + ++ /// @brief Indicates if the @c MultiThreadingMgr is in the test mode. ++ bool test_mode_; ++ + /// @brief The critical section count. + /// + /// In case the configuration is applied within a +-- +2.39.5 + diff --git a/scripts/package-build/kea/patches/isc-kea/0002-Add-ping_check-hook-library.patch b/scripts/package-build/kea/patches/isc-kea/0002-Add-ping_check-hook-library.patch new file mode 100644 index 00000000..c2f172ca --- /dev/null +++ b/scripts/package-build/kea/patches/isc-kea/0002-Add-ping_check-hook-library.patch @@ -0,0 +1,13277 @@ +From 6f198a187195a7fa4ad2cf9d147532bd64724f65 Mon Sep 17 00:00:00 2001 +From: sarthurdev <965089+sarthurdev@users.noreply.github.com> +Date: Mon, 24 Mar 2025 19:38:34 +0100 +Subject: [PATCH] Add ping_check hook library + +--- + configure.ac | 3 + + src/hooks/dhcp/Makefile.am | 2 +- + src/hooks/dhcp/ping_check/Doxyfile | 2568 +++++++++++++++++ + src/hooks/dhcp/ping_check/Makefile.am | 104 + + src/hooks/dhcp/ping_check/config_cache.cc | 107 + + src/hooks/dhcp/ping_check/config_cache.h | 146 + + src/hooks/dhcp/ping_check/icmp_endpoint.h | 134 + + src/hooks/dhcp/ping_check/icmp_msg.cc | 112 + + src/hooks/dhcp/ping_check/icmp_msg.h | 223 ++ + src/hooks/dhcp/ping_check/icmp_socket.h | 359 +++ + .../dhcp/ping_check/libloadtests/.gitignore | 1 + + .../dhcp/ping_check/libloadtests/Makefile.am | 60 + + .../libloadtests/load_unload_unittests.cc | 107 + + .../dhcp/ping_check/libloadtests/meson.build | 21 + + .../ping_check/libloadtests/run_unittests.cc | 19 + + src/hooks/dhcp/ping_check/meson.build | 41 + + src/hooks/dhcp/ping_check/ping_channel.cc | 466 +++ + src/hooks/dhcp/ping_check/ping_channel.h | 371 +++ + src/hooks/dhcp/ping_check/ping_check.dox | 44 + + .../dhcp/ping_check/ping_check_callouts.cc | 240 ++ + .../dhcp/ping_check/ping_check_config.cc | 98 + + src/hooks/dhcp/ping_check/ping_check_config.h | 134 + + src/hooks/dhcp/ping_check/ping_check_log.cc | 17 + + src/hooks/dhcp/ping_check/ping_check_log.h | 23 + + .../dhcp/ping_check/ping_check_messages.cc | 99 + + .../dhcp/ping_check/ping_check_messages.h | 50 + + .../dhcp/ping_check/ping_check_messages.mes | 229 ++ + src/hooks/dhcp/ping_check/ping_check_mgr.cc | 798 +++++ + src/hooks/dhcp/ping_check/ping_check_mgr.h | 436 +++ + src/hooks/dhcp/ping_check/ping_context.cc | 237 ++ + src/hooks/dhcp/ping_check/ping_context.h | 280 ++ + .../dhcp/ping_check/ping_context_store.cc | 144 + + .../dhcp/ping_check/ping_context_store.h | 240 ++ + src/hooks/dhcp/ping_check/tests/.gitignore | 1 + + src/hooks/dhcp/ping_check/tests/Makefile.am | 70 + + .../tests/config_cache_unittests.cc | 245 ++ + .../tests/icmp_endpoint_unittests.cc | 44 + + .../ping_check/tests/icmp_msg_unittests.cc | 172 ++ + .../ping_check/tests/icmp_socket_unittests.cc | 380 +++ + src/hooks/dhcp/ping_check/tests/meson.build | 21 + + .../tests/ping_channel_unittests.cc | 821 ++++++ + .../tests/ping_check_config_unittests.cc | 287 ++ + .../tests/ping_check_mgr_unittests.cc | 1878 ++++++++++++ + .../tests/ping_context_store_unittests.cc | 467 +++ + .../tests/ping_context_unittests.cc | 146 + + .../dhcp/ping_check/tests/ping_test_utils.h | 396 +++ + .../dhcp/ping_check/tests/run_unittests.cc | 19 + + src/hooks/dhcp/ping_check/version.cc | 17 + + 48 files changed, 12876 insertions(+), 1 deletion(-) + create mode 100644 src/hooks/dhcp/ping_check/Doxyfile + create mode 100644 src/hooks/dhcp/ping_check/Makefile.am + create mode 100644 src/hooks/dhcp/ping_check/config_cache.cc + create mode 100644 src/hooks/dhcp/ping_check/config_cache.h + create mode 100644 src/hooks/dhcp/ping_check/icmp_endpoint.h + create mode 100644 src/hooks/dhcp/ping_check/icmp_msg.cc + create mode 100644 src/hooks/dhcp/ping_check/icmp_msg.h + create mode 100644 src/hooks/dhcp/ping_check/icmp_socket.h + create mode 100644 src/hooks/dhcp/ping_check/libloadtests/.gitignore + create mode 100644 src/hooks/dhcp/ping_check/libloadtests/Makefile.am + create mode 100644 src/hooks/dhcp/ping_check/libloadtests/load_unload_unittests.cc + create mode 100644 src/hooks/dhcp/ping_check/libloadtests/meson.build + create mode 100644 src/hooks/dhcp/ping_check/libloadtests/run_unittests.cc + create mode 100644 src/hooks/dhcp/ping_check/meson.build + create mode 100644 src/hooks/dhcp/ping_check/ping_channel.cc + create mode 100644 src/hooks/dhcp/ping_check/ping_channel.h + create mode 100644 src/hooks/dhcp/ping_check/ping_check.dox + create mode 100644 src/hooks/dhcp/ping_check/ping_check_callouts.cc + create mode 100644 src/hooks/dhcp/ping_check/ping_check_config.cc + create mode 100644 src/hooks/dhcp/ping_check/ping_check_config.h + create mode 100644 src/hooks/dhcp/ping_check/ping_check_log.cc + create mode 100644 src/hooks/dhcp/ping_check/ping_check_log.h + create mode 100644 src/hooks/dhcp/ping_check/ping_check_messages.cc + create mode 100644 src/hooks/dhcp/ping_check/ping_check_messages.h + create mode 100644 src/hooks/dhcp/ping_check/ping_check_messages.mes + create mode 100644 src/hooks/dhcp/ping_check/ping_check_mgr.cc + create mode 100644 src/hooks/dhcp/ping_check/ping_check_mgr.h + create mode 100644 src/hooks/dhcp/ping_check/ping_context.cc + create mode 100644 src/hooks/dhcp/ping_check/ping_context.h + create mode 100644 src/hooks/dhcp/ping_check/ping_context_store.cc + create mode 100644 src/hooks/dhcp/ping_check/ping_context_store.h + create mode 100644 src/hooks/dhcp/ping_check/tests/.gitignore + create mode 100644 src/hooks/dhcp/ping_check/tests/Makefile.am + create mode 100644 src/hooks/dhcp/ping_check/tests/config_cache_unittests.cc + create mode 100644 src/hooks/dhcp/ping_check/tests/icmp_endpoint_unittests.cc + create mode 100644 src/hooks/dhcp/ping_check/tests/icmp_msg_unittests.cc + create mode 100644 src/hooks/dhcp/ping_check/tests/icmp_socket_unittests.cc + create mode 100644 src/hooks/dhcp/ping_check/tests/meson.build + create mode 100644 src/hooks/dhcp/ping_check/tests/ping_channel_unittests.cc + create mode 100644 src/hooks/dhcp/ping_check/tests/ping_check_config_unittests.cc + create mode 100644 src/hooks/dhcp/ping_check/tests/ping_check_mgr_unittests.cc + create mode 100644 src/hooks/dhcp/ping_check/tests/ping_context_store_unittests.cc + create mode 100644 src/hooks/dhcp/ping_check/tests/ping_context_unittests.cc + create mode 100644 src/hooks/dhcp/ping_check/tests/ping_test_utils.h + create mode 100644 src/hooks/dhcp/ping_check/tests/run_unittests.cc + create mode 100644 src/hooks/dhcp/ping_check/version.cc + +diff --git a/configure.ac b/configure.ac +index cc1b31af71..23c8eefb81 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -1582,6 +1582,9 @@ AC_CONFIG_FILES([src/hooks/dhcp/lease_cmds/tests/Makefile]) + AC_CONFIG_FILES([src/hooks/dhcp/mysql_cb/Makefile]) + AC_CONFIG_FILES([src/hooks/dhcp/mysql_cb/libloadtests/Makefile]) + AC_CONFIG_FILES([src/hooks/dhcp/mysql_cb/tests/Makefile]) ++AC_CONFIG_FILES([src/hooks/dhcp/ping_check/Makefile]) ++AC_CONFIG_FILES([src/hooks/dhcp/ping_check/libloadtests/Makefile]) ++AC_CONFIG_FILES([src/hooks/dhcp/ping_check/tests/Makefile]) + AC_CONFIG_FILES([src/hooks/dhcp/pgsql_cb/Makefile]) + AC_CONFIG_FILES([src/hooks/dhcp/pgsql_cb/libloadtests/Makefile]) + AC_CONFIG_FILES([src/hooks/dhcp/pgsql_cb/tests/Makefile]) +diff --git a/src/hooks/dhcp/Makefile.am b/src/hooks/dhcp/Makefile.am +index 1b77976424..806e310a17 100644 +--- a/src/hooks/dhcp/Makefile.am ++++ b/src/hooks/dhcp/Makefile.am +@@ -8,4 +8,4 @@ if HAVE_PGSQL + SUBDIRS += pgsql_cb + endif + +-SUBDIRS += run_script stat_cmds user_chk ++SUBDIRS += run_script stat_cmds user_chk ping_check +diff --git a/src/hooks/dhcp/ping_check/Doxyfile b/src/hooks/dhcp/ping_check/Doxyfile +new file mode 100644 +index 0000000000..7c8554b557 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/Doxyfile +@@ -0,0 +1,2568 @@ ++# Doxyfile 1.9.1 ++ ++# This file describes the settings to be used by the documentation system ++# doxygen (www.doxygen.org) for a project. ++# ++# All text after a double hash (##) is considered a comment and is placed in ++# front of the TAG it is preceding. ++# ++# All text after a single hash (#) is considered a comment and will be ignored. ++# The format is: ++# TAG = value [value, ...] ++# For lists, items can also be appended using: ++# TAG += value [value, ...] ++# Values that contain spaces should be placed between quotes (\" \"). ++ ++#--------------------------------------------------------------------------- ++# Project related configuration options ++#--------------------------------------------------------------------------- ++ ++# This tag specifies the encoding used for all characters in the configuration ++# file that follow. The default is UTF-8 which is also the encoding used for all ++# text before the first occurrence of this tag. Doxygen uses libiconv (or the ++# iconv built into libc) for the transcoding. See ++# https://www.gnu.org/software/libiconv/ for the list of possible encodings. ++# The default value is: UTF-8. ++ ++DOXYFILE_ENCODING = UTF-8 ++ ++# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by ++# double-quotes, unless you are using Doxywizard) that should identify the ++# project for which the documentation is generated. This name is used in the ++# title of most generated pages and in a few other places. ++# The default value is: My Project. ++ ++PROJECT_NAME = "Kea Ping Check Hooks Library" ++ ++# The PROJECT_NUMBER tag can be used to enter a project or revision number. This ++# could be handy for archiving the generated documentation or if some version ++# control system is used. ++ ++PROJECT_NUMBER = ++ ++# Using the PROJECT_BRIEF tag one can provide an optional one line description ++# for a project that appears at the top of each page and should give viewer a ++# quick idea about the purpose of the project. Keep the description short. ++ ++PROJECT_BRIEF = ++ ++# With the PROJECT_LOGO tag one can specify a logo or an icon that is included ++# in the documentation. The maximum height of the logo should not exceed 55 ++# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy ++# the logo to the output directory. ++ ++PROJECT_LOGO = ../../../../../doc/images/kea-logo-100x70.png ++ ++# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path ++# into which the generated documentation will be written. If a relative path is ++# entered, it will be relative to the location where doxygen was started. If ++# left blank the current directory will be used. ++ ++OUTPUT_DIRECTORY = html ++ ++# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- ++# directories (in 2 levels) under the output directory of each output format and ++# will distribute the generated files over these directories. Enabling this ++# option can be useful when feeding doxygen a huge amount of source files, where ++# putting all generated files in the same directory would otherwise causes ++# performance problems for the file system. ++# The default value is: NO. ++ ++CREATE_SUBDIRS = YES ++ ++# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII ++# characters to appear in the names of generated files. If set to NO, non-ASCII ++# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode ++# U+3044. ++# The default value is: NO. ++ ++ALLOW_UNICODE_NAMES = NO ++ ++# The OUTPUT_LANGUAGE tag is used to specify the language in which all ++# documentation generated by doxygen is written. Doxygen will use this ++# information to generate all constant output in the proper language. ++# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, ++# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), ++# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, ++# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), ++# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, ++# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, ++# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, ++# Ukrainian and Vietnamese. ++# The default value is: English. ++ ++OUTPUT_LANGUAGE = English ++ ++# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all ++# documentation generated by doxygen is written. Doxygen will use this ++# information to generate all generated output in the proper direction. ++# Possible values are: None, LTR, RTL and Context. ++# The default value is: None. ++ ++OUTPUT_TEXT_DIRECTION = None ++ ++# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member ++# descriptions after the members that are listed in the file and class ++# documentation (similar to Javadoc). Set to NO to disable this. ++# The default value is: YES. ++ ++BRIEF_MEMBER_DESC = YES ++ ++# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief ++# description of a member or function before the detailed description ++# ++# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the ++# brief descriptions will be completely suppressed. ++# The default value is: YES. ++ ++REPEAT_BRIEF = YES ++ ++# This tag implements a quasi-intelligent brief description abbreviator that is ++# used to form the text in various listings. Each string in this list, if found ++# as the leading text of the brief description, will be stripped from the text ++# and the result, after processing the whole list, is used as the annotated ++# text. Otherwise, the brief description is used as-is. If left blank, the ++# following values are used ($name is automatically replaced with the name of ++# the entity):The $name class, The $name widget, The $name file, is, provides, ++# specifies, contains, represents, a, an and the. ++ ++ABBREVIATE_BRIEF = ++ ++# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then ++# doxygen will generate a detailed section even if there is only a brief ++# description. ++# The default value is: NO. ++ ++ALWAYS_DETAILED_SEC = NO ++ ++# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all ++# inherited members of a class in the documentation of that class as if those ++# members were ordinary class members. Constructors, destructors and assignment ++# operators of the base classes will not be shown. ++# The default value is: NO. ++ ++INLINE_INHERITED_MEMB = NO ++ ++# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path ++# before files name in the file list and in the header files. If set to NO the ++# shortest path that makes the file name unique will be used ++# The default value is: YES. ++ ++FULL_PATH_NAMES = NO ++ ++# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. ++# Stripping is only done if one of the specified strings matches the left-hand ++# part of the path. The tag can be used to show relative paths in the file list. ++# If left blank the directory from which doxygen is run is used as the path to ++# strip. ++# ++# Note that you can specify absolute paths here, but also relative paths, which ++# will be relative from the directory where doxygen is started. ++# This tag requires that the tag FULL_PATH_NAMES is set to YES. ++ ++STRIP_FROM_PATH = ++ ++# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the ++# path mentioned in the documentation of a class, which tells the reader which ++# header file to include in order to use a class. If left blank only the name of ++# the header file containing the class definition is used. Otherwise one should ++# specify the list of include paths that are normally passed to the compiler ++# using the -I flag. ++ ++STRIP_FROM_INC_PATH = ++ ++# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but ++# less readable) file names. This can be useful is your file systems doesn't ++# support long names like on DOS, Mac, or CD-ROM. ++# The default value is: NO. ++ ++SHORT_NAMES = NO ++ ++# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the ++# first line (until the first dot) of a Javadoc-style comment as the brief ++# description. If set to NO, the Javadoc-style will behave just like regular Qt- ++# style comments (thus requiring an explicit @brief command for a brief ++# description.) ++# The default value is: NO. ++ ++JAVADOC_AUTOBRIEF = YES ++ ++# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line ++# such as ++# /*************** ++# as being the beginning of a Javadoc-style comment "banner". If set to NO, the ++# Javadoc-style will behave just like regular comments and it will not be ++# interpreted by doxygen. ++# The default value is: NO. ++ ++JAVADOC_BANNER = NO ++ ++# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first ++# line (until the first dot) of a Qt-style comment as the brief description. If ++# set to NO, the Qt-style will behave just like regular Qt-style comments (thus ++# requiring an explicit \brief command for a brief description.) ++# The default value is: NO. ++ ++QT_AUTOBRIEF = NO ++ ++# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a ++# multi-line C++ special comment block (i.e. a block of //! or /// comments) as ++# a brief description. This used to be the default behavior. The new default is ++# to treat a multi-line C++ comment block as a detailed description. Set this ++# tag to YES if you prefer the old behavior instead. ++# ++# Note that setting this tag to YES also means that rational rose comments are ++# not recognized any more. ++# The default value is: NO. ++ ++MULTILINE_CPP_IS_BRIEF = NO ++ ++# By default Python docstrings are displayed as preformatted text and doxygen's ++# special commands cannot be used. By setting PYTHON_DOCSTRING to NO the ++# doxygen's special commands can be used and the contents of the docstring ++# documentation blocks is shown as doxygen documentation. ++# The default value is: YES. ++ ++PYTHON_DOCSTRING = YES ++ ++# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the ++# documentation from any documented member that it re-implements. ++# The default value is: YES. ++ ++INHERIT_DOCS = YES ++ ++# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new ++# page for each member. If set to NO, the documentation of a member will be part ++# of the file/class/namespace that contains it. ++# The default value is: NO. ++ ++SEPARATE_MEMBER_PAGES = NO ++ ++# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen ++# uses this value to replace tabs by spaces in code fragments. ++# Minimum value: 1, maximum value: 16, default value: 4. ++ ++TAB_SIZE = 4 ++ ++# This tag can be used to specify a number of aliases that act as commands in ++# the documentation. An alias has the form: ++# name=value ++# For example adding ++# "sideeffect=@par Side Effects:\n" ++# will allow you to put the command \sideeffect (or @sideeffect) in the ++# documentation, which will result in a user-defined paragraph with heading ++# "Side Effects:". You can put \n's in the value part of an alias to insert ++# newlines (in the resulting output). You can put ^^ in the value part of an ++# alias to insert a newline as if a physical newline was in the original file. ++# When you need a literal { or } or , in the value part of an alias you have to ++# escape them by means of a backslash (\), this can lead to conflicts with the ++# commands \{ and \} for these it is advised to use the version @{ and @} or use ++# a double escape (\\{ and \\}) ++ ++ALIASES = ++ ++# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources ++# only. Doxygen will then generate output that is more tailored for C. For ++# instance, some of the names that are used will be different. The list of all ++# members will be omitted, etc. ++# The default value is: NO. ++ ++OPTIMIZE_OUTPUT_FOR_C = NO ++ ++# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or ++# Python sources only. Doxygen will then generate output that is more tailored ++# for that language. For instance, namespaces will be presented as packages, ++# qualified scopes will look different, etc. ++# The default value is: NO. ++ ++OPTIMIZE_OUTPUT_JAVA = NO ++ ++# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran ++# sources. Doxygen will then generate output that is tailored for Fortran. ++# The default value is: NO. ++ ++OPTIMIZE_FOR_FORTRAN = NO ++ ++# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL ++# sources. Doxygen will then generate output that is tailored for VHDL. ++# The default value is: NO. ++ ++OPTIMIZE_OUTPUT_VHDL = NO ++ ++# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice ++# sources only. Doxygen will then generate output that is more tailored for that ++# language. For instance, namespaces will be presented as modules, types will be ++# separated into more groups, etc. ++# The default value is: NO. ++ ++OPTIMIZE_OUTPUT_SLICE = NO ++ ++# Doxygen selects the parser to use depending on the extension of the files it ++# parses. With this tag you can assign which parser to use for a given ++# extension. Doxygen has a built-in mapping, but you can override or extend it ++# using this tag. The format is ext=language, where ext is a file extension, and ++# language is one of the parsers supported by doxygen: IDL, Java, JavaScript, ++# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, VHDL, ++# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: ++# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser ++# tries to guess whether the code is fixed or free formatted code, this is the ++# default for Fortran type files). For instance to make doxygen treat .inc files ++# as Fortran files (default is PHP), and .f files as C (default is Fortran), ++# use: inc=Fortran f=C. ++# ++# Note: For files without extension you can use no_extension as a placeholder. ++# ++# Note that for custom extensions you also need to set FILE_PATTERNS otherwise ++# the files are not read by doxygen. When specifying no_extension you should add ++# * to the FILE_PATTERNS. ++# ++# Note see also the list of default file extension mappings. ++ ++EXTENSION_MAPPING = ++ ++# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments ++# according to the Markdown format, which allows for more readable ++# documentation. See https://daringfireball.net/projects/markdown/ for details. ++# The output of markdown processing is further processed by doxygen, so you can ++# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in ++# case of backward compatibilities issues. ++# The default value is: YES. ++ ++MARKDOWN_SUPPORT = YES ++ ++# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up ++# to that level are automatically included in the table of contents, even if ++# they do not have an id attribute. ++# Note: This feature currently applies only to Markdown headings. ++# Minimum value: 0, maximum value: 99, default value: 5. ++# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. ++ ++TOC_INCLUDE_HEADINGS = 5 ++ ++# When enabled doxygen tries to link words that correspond to documented ++# classes, or namespaces to their corresponding documentation. Such a link can ++# be prevented in individual cases by putting a % sign in front of the word or ++# globally by setting AUTOLINK_SUPPORT to NO. ++# The default value is: YES. ++ ++AUTOLINK_SUPPORT = YES ++ ++# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want ++# to include (a tag file for) the STL sources as input, then you should set this ++# tag to YES in order to let doxygen match functions declarations and ++# definitions whose arguments contain STL classes (e.g. func(std::string); ++# versus func(std::string) {}). This also make the inheritance and collaboration ++# diagrams that involve STL classes more complete and accurate. ++# The default value is: NO. ++ ++BUILTIN_STL_SUPPORT = YES ++ ++# If you use Microsoft's C++/CLI language, you should set this option to YES to ++# enable parsing support. ++# The default value is: NO. ++ ++CPP_CLI_SUPPORT = NO ++ ++# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: ++# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen ++# will parse them like normal C++ but will assume all classes use public instead ++# of private inheritance when no explicit protection keyword is present. ++# The default value is: NO. ++ ++SIP_SUPPORT = NO ++ ++# For Microsoft's IDL there are propget and propput attributes to indicate ++# getter and setter methods for a property. Setting this option to YES will make ++# doxygen to replace the get and set methods by a property in the documentation. ++# This will only work if the methods are indeed getting or setting a simple ++# type. If this is not the case, or you want to show the methods anyway, you ++# should set this option to NO. ++# The default value is: YES. ++ ++IDL_PROPERTY_SUPPORT = YES ++ ++# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC ++# tag is set to YES then doxygen will reuse the documentation of the first ++# member in the group (if any) for the other members of the group. By default ++# all members of a group must be documented explicitly. ++# The default value is: NO. ++ ++DISTRIBUTE_GROUP_DOC = NO ++ ++# If one adds a struct or class to a group and this option is enabled, then also ++# any nested class or struct is added to the same group. By default this option ++# is disabled and one has to add nested compounds explicitly via \ingroup. ++# The default value is: NO. ++ ++GROUP_NESTED_COMPOUNDS = NO ++ ++# Set the SUBGROUPING tag to YES to allow class member groups of the same type ++# (for instance a group of public functions) to be put as a subgroup of that ++# type (e.g. under the Public Functions section). Set it to NO to prevent ++# subgrouping. Alternatively, this can be done per class using the ++# \nosubgrouping command. ++# The default value is: YES. ++ ++SUBGROUPING = YES ++ ++# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions ++# are shown inside the group in which they are included (e.g. using \ingroup) ++# instead of on a separate page (for HTML and Man pages) or section (for LaTeX ++# and RTF). ++# ++# Note that this feature does not work in combination with ++# SEPARATE_MEMBER_PAGES. ++# The default value is: NO. ++ ++INLINE_GROUPED_CLASSES = NO ++ ++# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions ++# with only public data fields or simple typedef fields will be shown inline in ++# the documentation of the scope in which they are defined (i.e. file, ++# namespace, or group documentation), provided this scope is documented. If set ++# to NO, structs, classes, and unions are shown on a separate page (for HTML and ++# Man pages) or section (for LaTeX and RTF). ++# The default value is: NO. ++ ++INLINE_SIMPLE_STRUCTS = NO ++ ++# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or ++# enum is documented as struct, union, or enum with the name of the typedef. So ++# typedef struct TypeS {} TypeT, will appear in the documentation as a struct ++# with name TypeT. When disabled the typedef will appear as a member of a file, ++# namespace, or class. And the struct will be named TypeS. This can typically be ++# useful for C code in case the coding convention dictates that all compound ++# types are typedef'ed and only the typedef is referenced, never the tag name. ++# The default value is: NO. ++ ++TYPEDEF_HIDES_STRUCT = NO ++ ++# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This ++# cache is used to resolve symbols given their name and scope. Since this can be ++# an expensive process and often the same symbol appears multiple times in the ++# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small ++# doxygen will become slower. If the cache is too large, memory is wasted. The ++# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range ++# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 ++# symbols. At the end of a run doxygen will report the cache usage and suggest ++# the optimal cache size from a speed point of view. ++# Minimum value: 0, maximum value: 9, default value: 0. ++ ++LOOKUP_CACHE_SIZE = 0 ++ ++# The NUM_PROC_THREADS specifies the number threads doxygen is allowed to use ++# during processing. When set to 0 doxygen will based this on the number of ++# cores available in the system. You can set it explicitly to a value larger ++# than 0 to get more control over the balance between CPU load and processing ++# speed. At this moment only the input processing can be done using multiple ++# threads. Since this is still an experimental feature the default is set to 1, ++# which effectively disables parallel processing. Please report any issues you ++# encounter. Generating dot graphs in parallel is controlled by the ++# DOT_NUM_THREADS setting. ++# Minimum value: 0, maximum value: 32, default value: 1. ++ ++NUM_PROC_THREADS = 1 ++ ++#--------------------------------------------------------------------------- ++# Build related configuration options ++#--------------------------------------------------------------------------- ++ ++# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in ++# documentation are documented, even if no documentation was available. Private ++# class members and static file members will be hidden unless the ++# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. ++# Note: This will also disable the warnings about undocumented members that are ++# normally produced when WARNINGS is set to YES. ++# The default value is: NO. ++ ++EXTRACT_ALL = YES ++ ++# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will ++# be included in the documentation. ++# The default value is: NO. ++ ++EXTRACT_PRIVATE = NO ++ ++# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual ++# methods of a class will be included in the documentation. ++# The default value is: NO. ++ ++EXTRACT_PRIV_VIRTUAL = NO ++ ++# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal ++# scope will be included in the documentation. ++# The default value is: NO. ++ ++EXTRACT_PACKAGE = NO ++ ++# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be ++# included in the documentation. ++# The default value is: NO. ++ ++EXTRACT_STATIC = NO ++ ++# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined ++# locally in source files will be included in the documentation. If set to NO, ++# only classes defined in header files are included. Does not have any effect ++# for Java sources. ++# The default value is: YES. ++ ++EXTRACT_LOCAL_CLASSES = YES ++ ++# This flag is only useful for Objective-C code. If set to YES, local methods, ++# which are defined in the implementation section but not in the interface are ++# included in the documentation. If set to NO, only methods in the interface are ++# included. ++# The default value is: NO. ++ ++EXTRACT_LOCAL_METHODS = NO ++ ++# If this flag is set to YES, the members of anonymous namespaces will be ++# extracted and appear in the documentation as a namespace called ++# 'anonymous_namespace{file}', where file will be replaced with the base name of ++# the file that contains the anonymous namespace. By default anonymous namespace ++# are hidden. ++# The default value is: NO. ++ ++EXTRACT_ANON_NSPACES = NO ++ ++# If this flag is set to YES, the name of an unnamed parameter in a declaration ++# will be determined by the corresponding definition. By default unnamed ++# parameters remain unnamed in the output. ++# The default value is: YES. ++ ++RESOLVE_UNNAMED_PARAMS = YES ++ ++# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all ++# undocumented members inside documented classes or files. If set to NO these ++# members will be included in the various overviews, but no documentation ++# section is generated. This option has no effect if EXTRACT_ALL is enabled. ++# The default value is: NO. ++ ++HIDE_UNDOC_MEMBERS = NO ++ ++# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all ++# undocumented classes that are normally visible in the class hierarchy. If set ++# to NO, these classes will be included in the various overviews. This option ++# has no effect if EXTRACT_ALL is enabled. ++# The default value is: NO. ++ ++HIDE_UNDOC_CLASSES = NO ++ ++# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend ++# declarations. If set to NO, these declarations will be included in the ++# documentation. ++# The default value is: NO. ++ ++HIDE_FRIEND_COMPOUNDS = NO ++ ++# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any ++# documentation blocks found inside the body of a function. If set to NO, these ++# blocks will be appended to the function's detailed documentation block. ++# The default value is: NO. ++ ++HIDE_IN_BODY_DOCS = NO ++ ++# The INTERNAL_DOCS tag determines if documentation that is typed after a ++# \internal command is included. If the tag is set to NO then the documentation ++# will be excluded. Set it to YES to include the internal documentation. ++# The default value is: NO. ++ ++INTERNAL_DOCS = NO ++ ++# With the correct setting of option CASE_SENSE_NAMES doxygen will better be ++# able to match the capabilities of the underlying filesystem. In case the ++# filesystem is case sensitive (i.e. it supports files in the same directory ++# whose names only differ in casing), the option must be set to YES to properly ++# deal with such files in case they appear in the input. For filesystems that ++# are not case sensitive the option should be be set to NO to properly deal with ++# output files written for symbols that only differ in casing, such as for two ++# classes, one named CLASS and the other named Class, and to also support ++# references to files without having to specify the exact matching casing. On ++# Windows (including Cygwin) and MacOS, users should typically set this option ++# to NO, whereas on Linux or other Unix flavors it should typically be set to ++# YES. ++# The default value is: system dependent. ++ ++CASE_SENSE_NAMES = YES ++ ++# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with ++# their full class and namespace scopes in the documentation. If set to YES, the ++# scope will be hidden. ++# The default value is: NO. ++ ++HIDE_SCOPE_NAMES = NO ++ ++# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will ++# append additional text to a page's title, such as Class Reference. If set to ++# YES the compound reference will be hidden. ++# The default value is: NO. ++ ++HIDE_COMPOUND_REFERENCE= NO ++ ++# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of ++# the files that are included by a file in the documentation of that file. ++# The default value is: YES. ++ ++SHOW_INCLUDE_FILES = YES ++ ++# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each ++# grouped member an include statement to the documentation, telling the reader ++# which file to include in order to use the member. ++# The default value is: NO. ++ ++SHOW_GROUPED_MEMB_INC = NO ++ ++# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include ++# files with double quotes in the documentation rather than with sharp brackets. ++# The default value is: NO. ++ ++FORCE_LOCAL_INCLUDES = NO ++ ++# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the ++# documentation for inline members. ++# The default value is: YES. ++ ++INLINE_INFO = YES ++ ++# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the ++# (detailed) documentation of file and class members alphabetically by member ++# name. If set to NO, the members will appear in declaration order. ++# The default value is: YES. ++ ++SORT_MEMBER_DOCS = YES ++ ++# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief ++# descriptions of file, namespace and class members alphabetically by member ++# name. If set to NO, the members will appear in declaration order. Note that ++# this will also influence the order of the classes in the class list. ++# The default value is: NO. ++ ++SORT_BRIEF_DOCS = YES ++ ++# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the ++# (brief and detailed) documentation of class members so that constructors and ++# destructors are listed first. If set to NO the constructors will appear in the ++# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. ++# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief ++# member documentation. ++# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting ++# detailed member documentation. ++# The default value is: NO. ++ ++SORT_MEMBERS_CTORS_1ST = YES ++ ++# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy ++# of group names into alphabetical order. If set to NO the group names will ++# appear in their defined order. ++# The default value is: NO. ++ ++SORT_GROUP_NAMES = YES ++ ++# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by ++# fully-qualified names, including namespaces. If set to NO, the class list will ++# be sorted only by class name, not including the namespace part. ++# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. ++# Note: This option applies only to the class list, not to the alphabetical ++# list. ++# The default value is: NO. ++ ++SORT_BY_SCOPE_NAME = NO ++ ++# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper ++# type resolution of all parameters of a function it will reject a match between ++# the prototype and the implementation of a member function even if there is ++# only one candidate or it is obvious which candidate to choose by doing a ++# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still ++# accept a match between prototype and implementation in such cases. ++# The default value is: NO. ++ ++STRICT_PROTO_MATCHING = NO ++ ++# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo ++# list. This list is created by putting \todo commands in the documentation. ++# The default value is: YES. ++ ++GENERATE_TODOLIST = YES ++ ++# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test ++# list. This list is created by putting \test commands in the documentation. ++# The default value is: YES. ++ ++GENERATE_TESTLIST = YES ++ ++# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug ++# list. This list is created by putting \bug commands in the documentation. ++# The default value is: YES. ++ ++GENERATE_BUGLIST = YES ++ ++# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) ++# the deprecated list. This list is created by putting \deprecated commands in ++# the documentation. ++# The default value is: YES. ++ ++GENERATE_DEPRECATEDLIST= YES ++ ++# The ENABLED_SECTIONS tag can be used to enable conditional documentation ++# sections, marked by \if <section_label> ... \endif and \cond <section_label> ++# ... \endcond blocks. ++ ++ENABLED_SECTIONS = ++ ++# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the ++# initial value of a variable or macro / define can have for it to appear in the ++# documentation. If the initializer consists of more lines than specified here ++# it will be hidden. Use a value of 0 to hide initializers completely. The ++# appearance of the value of individual variables and macros / defines can be ++# controlled using \showinitializer or \hideinitializer command in the ++# documentation regardless of this setting. ++# Minimum value: 0, maximum value: 10000, default value: 30. ++ ++MAX_INITIALIZER_LINES = 30 ++ ++# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at ++# the bottom of the documentation of classes and structs. If set to YES, the ++# list will mention the files that were used to generate the documentation. ++# The default value is: YES. ++ ++SHOW_USED_FILES = YES ++ ++# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This ++# will remove the Files entry from the Quick Index and from the Folder Tree View ++# (if specified). ++# The default value is: YES. ++ ++SHOW_FILES = YES ++ ++# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces ++# page. This will remove the Namespaces entry from the Quick Index and from the ++# Folder Tree View (if specified). ++# The default value is: YES. ++ ++SHOW_NAMESPACES = YES ++ ++# The FILE_VERSION_FILTER tag can be used to specify a program or script that ++# doxygen should invoke to get the current version for each file (typically from ++# the version control system). Doxygen will invoke the program by executing (via ++# popen()) the command command input-file, where command is the value of the ++# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided ++# by doxygen. Whatever the program writes to standard output is used as the file ++# version. For an example see the documentation. ++ ++FILE_VERSION_FILTER = ++ ++# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed ++# by doxygen. The layout file controls the global structure of the generated ++# output files in an output format independent way. To create the layout file ++# that represents doxygen's defaults, run doxygen with the -l option. You can ++# optionally specify a file name after the option, if omitted DoxygenLayout.xml ++# will be used as the name of the layout file. ++# ++# Note that if you run doxygen from a directory containing a file called ++# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE ++# tag is left empty. ++ ++LAYOUT_FILE = ++ ++# The CITE_BIB_FILES tag can be used to specify one or more bib files containing ++# the reference definitions. This must be a list of .bib files. The .bib ++# extension is automatically appended if omitted. This requires the bibtex tool ++# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info. ++# For LaTeX the style of the bibliography can be controlled using ++# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the ++# search path. See also \cite for info how to create references. ++ ++CITE_BIB_FILES = ++ ++#--------------------------------------------------------------------------- ++# Configuration options related to warning and progress messages ++#--------------------------------------------------------------------------- ++ ++# The QUIET tag can be used to turn on/off the messages that are generated to ++# standard output by doxygen. If QUIET is set to YES this implies that the ++# messages are off. ++# The default value is: NO. ++ ++QUIET = YES ++ ++# The WARNINGS tag can be used to turn on/off the warning messages that are ++# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES ++# this implies that the warnings are on. ++# ++# Tip: Turn warnings on while writing the documentation. ++# The default value is: YES. ++ ++WARNINGS = YES ++ ++# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate ++# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag ++# will automatically be disabled. ++# The default value is: YES. ++ ++WARN_IF_UNDOCUMENTED = YES ++ ++# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for ++# potential errors in the documentation, such as not documenting some parameters ++# in a documented function, or documenting parameters that don't exist or using ++# markup commands wrongly. ++# The default value is: YES. ++ ++WARN_IF_DOC_ERROR = YES ++ ++# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that ++# are documented, but have no documentation for their parameters or return ++# value. If set to NO, doxygen will only warn about wrong or incomplete ++# parameter documentation, but not about the absence of documentation. If ++# EXTRACT_ALL is set to YES then this flag will automatically be disabled. ++# The default value is: NO. ++ ++WARN_NO_PARAMDOC = NO ++ ++# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when ++# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS ++# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but ++# at the end of the doxygen process doxygen will return with a non-zero status. ++# Possible values are: NO, YES and FAIL_ON_WARNINGS. ++# The default value is: NO. ++ ++WARN_AS_ERROR = NO ++ ++# The WARN_FORMAT tag determines the format of the warning messages that doxygen ++# can produce. The string should contain the $file, $line, and $text tags, which ++# will be replaced by the file and line number from which the warning originated ++# and the warning text. Optionally the format may contain $version, which will ++# be replaced by the version of the file (if it could be obtained via ++# FILE_VERSION_FILTER) ++# The default value is: $file:$line: $text. ++ ++WARN_FORMAT = "$file:$line: $text" ++ ++# The WARN_LOGFILE tag can be used to specify a file to which warning and error ++# messages should be written. If left blank the output is written to standard ++# error (stderr). ++ ++WARN_LOGFILE = ++ ++#--------------------------------------------------------------------------- ++# Configuration options related to the input files ++#--------------------------------------------------------------------------- ++ ++# The INPUT tag is used to specify the files and/or directories that contain ++# documented source files. You may enter file names like myfile.cpp or ++# directories like /usr/src/myproject. Separate the files or directories with ++# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING ++# Note: If this tag is empty the current directory is searched. ++ ++INPUT = ++ ++# This tag can be used to specify the character encoding of the source files ++# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses ++# libiconv (or the iconv built into libc) for the transcoding. See the libiconv ++# documentation (see: ++# https://www.gnu.org/software/libiconv/) for the list of possible encodings. ++# The default value is: UTF-8. ++ ++INPUT_ENCODING = UTF-8 ++ ++# If the value of the INPUT tag contains directories, you can use the ++# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and ++# *.h) to filter out the source-files in the directories. ++# ++# Note that for custom extensions or not directly supported extensions you also ++# need to set EXTENSION_MAPPING for the extension otherwise the files are not ++# read by doxygen. ++# ++# Note the list of default checked file patterns might differ from the list of ++# default file extension mappings. ++# ++# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, ++# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, ++# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, ++# *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C comment), ++# *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, *.vhdl, ++# *.ucf, *.qsf and *.ice. ++ ++FILE_PATTERNS = *.c \ ++ *.cc \ ++ *.h \ ++ *.hpp \ ++ *.dox ++ ++# The RECURSIVE tag can be used to specify whether or not subdirectories should ++# be searched for input files as well. ++# The default value is: NO. ++ ++RECURSIVE = NO ++ ++# The EXCLUDE tag can be used to specify files and/or directories that should be ++# excluded from the INPUT source files. This way you can easily exclude a ++# subdirectory from a directory tree whose root is specified with the INPUT tag. ++# ++# Note that relative paths are relative to the directory from which doxygen is ++# run. ++ ++EXCLUDE = ++ ++# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or ++# directories that are symbolic links (a Unix file system feature) are excluded ++# from the input. ++# The default value is: NO. ++ ++EXCLUDE_SYMLINKS = NO ++ ++# If the value of the INPUT tag contains directories, you can use the ++# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude ++# certain files from those directories. ++# ++# Note that the wildcards are matched against the file with absolute path, so to ++# exclude all test directories for example use the pattern */test/* ++ ++EXCLUDE_PATTERNS = ++ ++# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names ++# (namespaces, classes, functions, etc.) that should be excluded from the ++# output. The symbol name can be a fully qualified name, a word, or if the ++# wildcard * is used, a substring. Examples: ANamespace, AClass, ++# AClass::ANamespace, ANamespace::*Test ++# ++# Note that the wildcards are matched against the file with absolute path, so to ++# exclude all test directories use the pattern */test/* ++ ++EXCLUDE_SYMBOLS = ++ ++# The EXAMPLE_PATH tag can be used to specify one or more files or directories ++# that contain example code fragments that are included (see the \include ++# command). ++ ++EXAMPLE_PATH = ++ ++# If the value of the EXAMPLE_PATH tag contains directories, you can use the ++# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and ++# *.h) to filter out the source-files in the directories. If left blank all ++# files are included. ++ ++EXAMPLE_PATTERNS = ++ ++# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be ++# searched for input files to be used with the \include or \dontinclude commands ++# irrespective of the value of the RECURSIVE tag. ++# The default value is: NO. ++ ++EXAMPLE_RECURSIVE = NO ++ ++# The IMAGE_PATH tag can be used to specify one or more files or directories ++# that contain images that are to be included in the documentation (see the ++# \image command). ++ ++IMAGE_PATH = ../../../../../doc/images ++ ++# The INPUT_FILTER tag can be used to specify a program that doxygen should ++# invoke to filter for each input file. Doxygen will invoke the filter program ++# by executing (via popen()) the command: ++# ++# <filter> <input-file> ++# ++# where <filter> is the value of the INPUT_FILTER tag, and <input-file> is the ++# name of an input file. Doxygen will then use the output that the filter ++# program writes to standard output. If FILTER_PATTERNS is specified, this tag ++# will be ignored. ++# ++# Note that the filter must not add or remove lines; it is applied before the ++# code is scanned, but not when the output code is generated. If lines are added ++# or removed, the anchors will not be placed correctly. ++# ++# Note that for custom extensions or not directly supported extensions you also ++# need to set EXTENSION_MAPPING for the extension otherwise the files are not ++# properly processed by doxygen. ++ ++INPUT_FILTER = ++ ++# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern ++# basis. Doxygen will compare the file name with each pattern and apply the ++# filter if there is a match. The filters are a list of the form: pattern=filter ++# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how ++# filters are used. If the FILTER_PATTERNS tag is empty or if none of the ++# patterns match the file name, INPUT_FILTER is applied. ++# ++# Note that for custom extensions or not directly supported extensions you also ++# need to set EXTENSION_MAPPING for the extension otherwise the files are not ++# properly processed by doxygen. ++ ++FILTER_PATTERNS = ++ ++# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using ++# INPUT_FILTER) will also be used to filter the input files that are used for ++# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). ++# The default value is: NO. ++ ++FILTER_SOURCE_FILES = NO ++ ++# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file ++# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and ++# it is also possible to disable source filtering for a specific pattern using ++# *.ext= (so without naming a filter). ++# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. ++ ++FILTER_SOURCE_PATTERNS = ++ ++# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that ++# is part of the input, its contents will be placed on the main page ++# (index.html). This can be useful if you have a project on for instance GitHub ++# and want to reuse the introduction page also for the doxygen output. ++ ++USE_MDFILE_AS_MAINPAGE = ++ ++#--------------------------------------------------------------------------- ++# Configuration options related to source browsing ++#--------------------------------------------------------------------------- ++ ++# If the SOURCE_BROWSER tag is set to YES then a list of source files will be ++# generated. Documented entities will be cross-referenced with these sources. ++# ++# Note: To get rid of all source code in the generated output, make sure that ++# also VERBATIM_HEADERS is set to NO. ++# The default value is: NO. ++ ++SOURCE_BROWSER = YES ++ ++# Setting the INLINE_SOURCES tag to YES will include the body of functions, ++# classes and enums directly into the documentation. ++# The default value is: NO. ++ ++INLINE_SOURCES = NO ++ ++# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any ++# special comment blocks from generated source code fragments. Normal C, C++ and ++# Fortran comments will always remain visible. ++# The default value is: YES. ++ ++STRIP_CODE_COMMENTS = YES ++ ++# If the REFERENCED_BY_RELATION tag is set to YES then for each documented ++# entity all documented functions referencing it will be listed. ++# The default value is: NO. ++ ++REFERENCED_BY_RELATION = YES ++ ++# If the REFERENCES_RELATION tag is set to YES then for each documented function ++# all documented entities called/used by that function will be listed. ++# The default value is: NO. ++ ++REFERENCES_RELATION = YES ++ ++# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set ++# to YES then the hyperlinks from functions in REFERENCES_RELATION and ++# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will ++# link to the documentation. ++# The default value is: YES. ++ ++REFERENCES_LINK_SOURCE = YES ++ ++# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the ++# source code will show a tooltip with additional information such as prototype, ++# brief description and links to the definition and documentation. Since this ++# will make the HTML file larger and loading of large files a bit slower, you ++# can opt to disable this feature. ++# The default value is: YES. ++# This tag requires that the tag SOURCE_BROWSER is set to YES. ++ ++SOURCE_TOOLTIPS = YES ++ ++# If the USE_HTAGS tag is set to YES then the references to source code will ++# point to the HTML generated by the htags(1) tool instead of doxygen built-in ++# source browser. The htags tool is part of GNU's global source tagging system ++# (see https://www.gnu.org/software/global/global.html). You will need version ++# 4.8.6 or higher. ++# ++# To use it do the following: ++# - Install the latest version of global ++# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file ++# - Make sure the INPUT points to the root of the source tree ++# - Run doxygen as normal ++# ++# Doxygen will invoke htags (and that will in turn invoke gtags), so these ++# tools must be available from the command line (i.e. in the search path). ++# ++# The result: instead of the source browser generated by doxygen, the links to ++# source code will now point to the output of htags. ++# The default value is: NO. ++# This tag requires that the tag SOURCE_BROWSER is set to YES. ++ ++USE_HTAGS = NO ++ ++# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a ++# verbatim copy of the header file for each class for which an include is ++# specified. Set to NO to disable this. ++# See also: Section \class. ++# The default value is: YES. ++ ++VERBATIM_HEADERS = YES ++ ++#--------------------------------------------------------------------------- ++# Configuration options related to the alphabetical class index ++#--------------------------------------------------------------------------- ++ ++# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all ++# compounds will be generated. Enable this if the project contains a lot of ++# classes, structs, unions or interfaces. ++# The default value is: YES. ++ ++ALPHABETICAL_INDEX = YES ++ ++# In case all classes in a project start with a common prefix, all classes will ++# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag ++# can be used to specify a prefix (or a list of prefixes) that should be ignored ++# while generating the index headers. ++# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. ++ ++IGNORE_PREFIX = ++ ++#--------------------------------------------------------------------------- ++# Configuration options related to the HTML output ++#--------------------------------------------------------------------------- ++ ++# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output ++# The default value is: YES. ++ ++GENERATE_HTML = YES ++ ++# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a ++# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of ++# it. ++# The default directory is: html. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++HTML_OUTPUT = ../html ++ ++# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each ++# generated HTML page (for example: .htm, .php, .asp). ++# The default value is: .html. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++HTML_FILE_EXTENSION = .html ++ ++# The HTML_HEADER tag can be used to specify a user-defined HTML header file for ++# each generated HTML page. If the tag is left blank doxygen will generate a ++# standard header. ++# ++# To get valid HTML the header file that includes any scripts and style sheets ++# that doxygen needs, which is dependent on the configuration options used (e.g. ++# the setting GENERATE_TREEVIEW). It is highly recommended to start with a ++# default header using ++# doxygen -w html new_header.html new_footer.html new_stylesheet.css ++# YourConfigFile ++# and then modify the file new_header.html. See also section "Doxygen usage" ++# for information on how to generate the default header that doxygen normally ++# uses. ++# Note: The header is subject to change so you typically have to regenerate the ++# default header when upgrading to a newer version of doxygen. For a description ++# of the possible markers and block names see the documentation. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++HTML_HEADER = ++ ++# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each ++# generated HTML page. If the tag is left blank doxygen will generate a standard ++# footer. See HTML_HEADER for more information on how to generate a default ++# footer and what special commands can be used inside the footer. See also ++# section "Doxygen usage" for information on how to generate the default footer ++# that doxygen normally uses. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++HTML_FOOTER = ++ ++# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style ++# sheet that is used by each HTML page. It can be used to fine-tune the look of ++# the HTML output. If left blank doxygen will generate a default style sheet. ++# See also section "Doxygen usage" for information on how to generate the style ++# sheet that doxygen normally uses. ++# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as ++# it is more robust and this tag (HTML_STYLESHEET) will in the future become ++# obsolete. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++HTML_STYLESHEET = ++ ++# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined ++# cascading style sheets that are included after the standard style sheets ++# created by doxygen. Using this option one can overrule certain style aspects. ++# This is preferred over using HTML_STYLESHEET since it does not replace the ++# standard style sheet and is therefore more robust against future updates. ++# Doxygen will copy the style sheet files to the output directory. ++# Note: The order of the extra style sheet files is of importance (e.g. the last ++# style sheet in the list overrules the setting of the previous ones in the ++# list). For an example see the documentation. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++HTML_EXTRA_STYLESHEET = ++ ++# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or ++# other source files which should be copied to the HTML output directory. Note ++# that these files will be copied to the base HTML output directory. Use the ++# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these ++# files. In the HTML_STYLESHEET file, use the file name only. Also note that the ++# files will be copied as-is; there are no commands or markers available. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++HTML_EXTRA_FILES = ++ ++# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen ++# will adjust the colors in the style sheet and background images according to ++# this color. Hue is specified as an angle on a colorwheel, see ++# https://en.wikipedia.org/wiki/Hue for more information. For instance the value ++# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 ++# purple, and 360 is red again. ++# Minimum value: 0, maximum value: 359, default value: 220. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++HTML_COLORSTYLE_HUE = 148 ++ ++# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors ++# in the HTML output. For a value of 0 the output will use grayscales only. A ++# value of 255 will produce the most vivid colors. ++# Minimum value: 0, maximum value: 255, default value: 100. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++HTML_COLORSTYLE_SAT = 93 ++ ++# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the ++# luminance component of the colors in the HTML output. Values below 100 ++# gradually make the output lighter, whereas values above 100 make the output ++# darker. The value divided by 100 is the actual gamma applied, so 80 represents ++# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not ++# change the gamma. ++# Minimum value: 40, maximum value: 240, default value: 80. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++HTML_COLORSTYLE_GAMMA = 80 ++ ++# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML ++# page will contain the date and time when the page was generated. Setting this ++# to YES can help to show when doxygen was last run and thus if the ++# documentation is up to date. ++# The default value is: NO. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++HTML_TIMESTAMP = YES ++ ++# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML ++# documentation will contain a main index with vertical navigation menus that ++# are dynamically created via JavaScript. If disabled, the navigation index will ++# consists of multiple levels of tabs that are statically embedded in every HTML ++# page. Disable this option to support browsers that do not have JavaScript, ++# like the Qt help browser. ++# The default value is: YES. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++HTML_DYNAMIC_MENUS = YES ++ ++# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML ++# documentation will contain sections that can be hidden and shown after the ++# page has loaded. ++# The default value is: NO. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++HTML_DYNAMIC_SECTIONS = YES ++ ++# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries ++# shown in the various tree structured indices initially; the user can expand ++# and collapse entries dynamically later on. Doxygen will expand the tree to ++# such a level that at most the specified number of entries are visible (unless ++# a fully collapsed tree already exceeds this amount). So setting the number of ++# entries 1 will produce a full collapsed tree by default. 0 is a special value ++# representing an infinite number of entries and will result in a full expanded ++# tree by default. ++# Minimum value: 0, maximum value: 9999, default value: 100. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++HTML_INDEX_NUM_ENTRIES = 100 ++ ++# If the GENERATE_DOCSET tag is set to YES, additional index files will be ++# generated that can be used as input for Apple's Xcode 3 integrated development ++# environment (see: ++# https://developer.apple.com/xcode/), introduced with OSX 10.5 (Leopard). To ++# create a documentation set, doxygen will generate a Makefile in the HTML ++# output directory. Running make will produce the docset in that directory and ++# running make install will install the docset in ++# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at ++# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy ++# genXcode/_index.html for more information. ++# The default value is: NO. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++GENERATE_DOCSET = NO ++ ++# This tag determines the name of the docset feed. A documentation feed provides ++# an umbrella under which multiple documentation sets from a single provider ++# (such as a company or product suite) can be grouped. ++# The default value is: Doxygen generated docs. ++# This tag requires that the tag GENERATE_DOCSET is set to YES. ++ ++DOCSET_FEEDNAME = "Doxygen generated docs" ++ ++# This tag specifies a string that should uniquely identify the documentation ++# set bundle. This should be a reverse domain-name style string, e.g. ++# com.mycompany.MyDocSet. Doxygen will append .docset to the name. ++# The default value is: org.doxygen.Project. ++# This tag requires that the tag GENERATE_DOCSET is set to YES. ++ ++DOCSET_BUNDLE_ID = org.doxygen.Project ++ ++# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify ++# the documentation publisher. This should be a reverse domain-name style ++# string, e.g. com.mycompany.MyDocSet.documentation. ++# The default value is: org.doxygen.Publisher. ++# This tag requires that the tag GENERATE_DOCSET is set to YES. ++ ++DOCSET_PUBLISHER_ID = org.doxygen.Publisher ++ ++# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. ++# The default value is: Publisher. ++# This tag requires that the tag GENERATE_DOCSET is set to YES. ++ ++DOCSET_PUBLISHER_NAME = Publisher ++ ++# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three ++# additional HTML index files: index.hhp, index.hhc, and index.hhk. The ++# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop ++# (see: ++# https://www.microsoft.com/en-us/download/details.aspx?id=21138) on Windows. ++# ++# The HTML Help Workshop contains a compiler that can convert all HTML output ++# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML ++# files are now used as the Windows 98 help format, and will replace the old ++# Windows help format (.hlp) on all Windows platforms in the future. Compressed ++# HTML files also contain an index, a table of contents, and you can search for ++# words in the documentation. The HTML workshop also contains a viewer for ++# compressed HTML files. ++# The default value is: NO. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++GENERATE_HTMLHELP = NO ++ ++# The CHM_FILE tag can be used to specify the file name of the resulting .chm ++# file. You can add a path in front of the file if the result should not be ++# written to the html output directory. ++# This tag requires that the tag GENERATE_HTMLHELP is set to YES. ++ ++CHM_FILE = ++ ++# The HHC_LOCATION tag can be used to specify the location (absolute path ++# including file name) of the HTML help compiler (hhc.exe). If non-empty, ++# doxygen will try to run the HTML help compiler on the generated index.hhp. ++# The file has to be specified with full path. ++# This tag requires that the tag GENERATE_HTMLHELP is set to YES. ++ ++HHC_LOCATION = ++ ++# The GENERATE_CHI flag controls if a separate .chi index file is generated ++# (YES) or that it should be included in the main .chm file (NO). ++# The default value is: NO. ++# This tag requires that the tag GENERATE_HTMLHELP is set to YES. ++ ++GENERATE_CHI = NO ++ ++# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) ++# and project file content. ++# This tag requires that the tag GENERATE_HTMLHELP is set to YES. ++ ++CHM_INDEX_ENCODING = ++ ++# The BINARY_TOC flag controls whether a binary table of contents is generated ++# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it ++# enables the Previous and Next buttons. ++# The default value is: NO. ++# This tag requires that the tag GENERATE_HTMLHELP is set to YES. ++ ++BINARY_TOC = NO ++ ++# The TOC_EXPAND flag can be set to YES to add extra items for group members to ++# the table of contents of the HTML help documentation and to the tree view. ++# The default value is: NO. ++# This tag requires that the tag GENERATE_HTMLHELP is set to YES. ++ ++TOC_EXPAND = NO ++ ++# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and ++# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that ++# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help ++# (.qch) of the generated HTML documentation. ++# The default value is: NO. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++GENERATE_QHP = NO ++ ++# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify ++# the file name of the resulting .qch file. The path specified is relative to ++# the HTML output folder. ++# This tag requires that the tag GENERATE_QHP is set to YES. ++ ++QCH_FILE = ++ ++# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help ++# Project output. For more information please see Qt Help Project / Namespace ++# (see: ++# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace). ++# The default value is: org.doxygen.Project. ++# This tag requires that the tag GENERATE_QHP is set to YES. ++ ++QHP_NAMESPACE = ++ ++# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt ++# Help Project output. For more information please see Qt Help Project / Virtual ++# Folders (see: ++# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-folders). ++# The default value is: doc. ++# This tag requires that the tag GENERATE_QHP is set to YES. ++ ++QHP_VIRTUAL_FOLDER = doc ++ ++# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom ++# filter to add. For more information please see Qt Help Project / Custom ++# Filters (see: ++# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). ++# This tag requires that the tag GENERATE_QHP is set to YES. ++ ++QHP_CUST_FILTER_NAME = ++ ++# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the ++# custom filter to add. For more information please see Qt Help Project / Custom ++# Filters (see: ++# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). ++# This tag requires that the tag GENERATE_QHP is set to YES. ++ ++QHP_CUST_FILTER_ATTRS = ++ ++# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this ++# project's filter section matches. Qt Help Project / Filter Attributes (see: ++# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes). ++# This tag requires that the tag GENERATE_QHP is set to YES. ++ ++QHP_SECT_FILTER_ATTRS = ++ ++# The QHG_LOCATION tag can be used to specify the location (absolute path ++# including file name) of Qt's qhelpgenerator. If non-empty doxygen will try to ++# run qhelpgenerator on the generated .qhp file. ++# This tag requires that the tag GENERATE_QHP is set to YES. ++ ++QHG_LOCATION = ++ ++# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be ++# generated, together with the HTML files, they form an Eclipse help plugin. To ++# install this plugin and make it available under the help contents menu in ++# Eclipse, the contents of the directory containing the HTML and XML files needs ++# to be copied into the plugins directory of eclipse. The name of the directory ++# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. ++# After copying Eclipse needs to be restarted before the help appears. ++# The default value is: NO. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++GENERATE_ECLIPSEHELP = NO ++ ++# A unique identifier for the Eclipse help plugin. When installing the plugin ++# the directory name containing the HTML and XML files should also have this ++# name. Each documentation set should have its own identifier. ++# The default value is: org.doxygen.Project. ++# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. ++ ++ECLIPSE_DOC_ID = org.doxygen.Project ++ ++# If you want full control over the layout of the generated HTML pages it might ++# be necessary to disable the index and replace it with your own. The ++# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top ++# of each HTML page. A value of NO enables the index and the value YES disables ++# it. Since the tabs in the index contain the same information as the navigation ++# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. ++# The default value is: NO. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++DISABLE_INDEX = NO ++ ++# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index ++# structure should be generated to display hierarchical information. If the tag ++# value is set to YES, a side panel will be generated containing a tree-like ++# index structure (just like the one that is generated for HTML Help). For this ++# to work a browser that supports JavaScript, DHTML, CSS and frames is required ++# (i.e. any modern browser). Windows users are probably better off using the ++# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can ++# further fine-tune the look of the index. As an example, the default style ++# sheet generated by doxygen has an example that shows how to put an image at ++# the root of the tree instead of the PROJECT_NAME. Since the tree basically has ++# the same information as the tab index, you could consider setting ++# DISABLE_INDEX to YES when enabling this option. ++# The default value is: NO. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++GENERATE_TREEVIEW = YES ++ ++# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that ++# doxygen will group on one line in the generated HTML documentation. ++# ++# Note that a value of 0 will completely suppress the enum values from appearing ++# in the overview section. ++# Minimum value: 0, maximum value: 20, default value: 4. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++ENUM_VALUES_PER_LINE = 4 ++ ++# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used ++# to set the initial width (in pixels) of the frame in which the tree is shown. ++# Minimum value: 0, maximum value: 1500, default value: 250. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++TREEVIEW_WIDTH = 180 ++ ++# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to ++# external symbols imported via tag files in a separate window. ++# The default value is: NO. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++EXT_LINKS_IN_WINDOW = NO ++ ++# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg ++# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see ++# https://inkscape.org) to generate formulas as SVG images instead of PNGs for ++# the HTML output. These images will generally look nicer at scaled resolutions. ++# Possible values are: png (the default) and svg (looks nicer but requires the ++# pdf2svg or inkscape tool). ++# The default value is: png. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++HTML_FORMULA_FORMAT = png ++ ++# Use this tag to change the font size of LaTeX formulas included as images in ++# the HTML documentation. When you change the font size after a successful ++# doxygen run you need to manually remove any form_*.png images from the HTML ++# output directory to force them to be regenerated. ++# Minimum value: 8, maximum value: 50, default value: 10. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++FORMULA_FONTSIZE = 10 ++ ++# Use the FORMULA_TRANSPARENT tag to determine whether or not the images ++# generated for formulas are transparent PNGs. Transparent PNGs are not ++# supported properly for IE 6.0, but are supported on all modern browsers. ++# ++# Note that when changing this option you need to delete any form_*.png files in ++# the HTML output directory before the changes have effect. ++# The default value is: YES. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++FORMULA_TRANSPARENT = YES ++ ++# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands ++# to create new LaTeX commands to be used in formulas as building blocks. See ++# the section "Including formulas" for details. ++ ++FORMULA_MACROFILE = ++ ++# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see ++# https://www.mathjax.org) which uses client side JavaScript for the rendering ++# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX ++# installed or if you want to formulas look prettier in the HTML output. When ++# enabled you may also need to install MathJax separately and configure the path ++# to it using the MATHJAX_RELPATH option. ++# The default value is: NO. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++USE_MATHJAX = NO ++ ++# When MathJax is enabled you can set the default output format to be used for ++# the MathJax output. See the MathJax site (see: ++# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. ++# Possible values are: HTML-CSS (which is slower, but has the best ++# compatibility), NativeMML (i.e. MathML) and SVG. ++# The default value is: HTML-CSS. ++# This tag requires that the tag USE_MATHJAX is set to YES. ++ ++MATHJAX_FORMAT = HTML-CSS ++ ++# When MathJax is enabled you need to specify the location relative to the HTML ++# output directory using the MATHJAX_RELPATH option. The destination directory ++# should contain the MathJax.js script. For instance, if the mathjax directory ++# is located at the same level as the HTML output directory, then ++# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax ++# Content Delivery Network so you can quickly see the result without installing ++# MathJax. However, it is strongly recommended to install a local copy of ++# MathJax from https://www.mathjax.org before deployment. ++# The default value is: https://cdn.jsdelivr.net/npm/mathjax@2. ++# This tag requires that the tag USE_MATHJAX is set to YES. ++ ++MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest ++ ++# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax ++# extension names that should be enabled during MathJax rendering. For example ++# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols ++# This tag requires that the tag USE_MATHJAX is set to YES. ++ ++MATHJAX_EXTENSIONS = ++ ++# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces ++# of code that will be used on startup of the MathJax code. See the MathJax site ++# (see: ++# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an ++# example see the documentation. ++# This tag requires that the tag USE_MATHJAX is set to YES. ++ ++MATHJAX_CODEFILE = ++ ++# When the SEARCHENGINE tag is enabled doxygen will generate a search box for ++# the HTML output. The underlying search engine uses javascript and DHTML and ++# should work on any modern browser. Note that when using HTML help ++# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) ++# there is already a search function so this one should typically be disabled. ++# For large projects the javascript based search engine can be slow, then ++# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to ++# search using the keyboard; to jump to the search box use <access key> + S ++# (what the <access key> is depends on the OS and browser, but it is typically ++# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down ++# key> to jump into the search results window, the results can be navigated ++# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel ++# the search. The filter options can be selected when the cursor is inside the ++# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys> ++# to select a filter and <Enter> or <escape> to activate or cancel the filter ++# option. ++# The default value is: YES. ++# This tag requires that the tag GENERATE_HTML is set to YES. ++ ++SEARCHENGINE = NO ++ ++# When the SERVER_BASED_SEARCH tag is enabled the search engine will be ++# implemented using a web server instead of a web client using JavaScript. There ++# are two flavors of web server based searching depending on the EXTERNAL_SEARCH ++# setting. When disabled, doxygen will generate a PHP script for searching and ++# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing ++# and searching needs to be provided by external tools. See the section ++# "External Indexing and Searching" for details. ++# The default value is: NO. ++# This tag requires that the tag SEARCHENGINE is set to YES. ++ ++SERVER_BASED_SEARCH = NO ++ ++# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP ++# script for searching. Instead the search results are written to an XML file ++# which needs to be processed by an external indexer. Doxygen will invoke an ++# external search engine pointed to by the SEARCHENGINE_URL option to obtain the ++# search results. ++# ++# Doxygen ships with an example indexer (doxyindexer) and search engine ++# (doxysearch.cgi) which are based on the open source search engine library ++# Xapian (see: ++# https://xapian.org/). ++# ++# See the section "External Indexing and Searching" for details. ++# The default value is: NO. ++# This tag requires that the tag SEARCHENGINE is set to YES. ++ ++EXTERNAL_SEARCH = NO ++ ++# The SEARCHENGINE_URL should point to a search engine hosted by a web server ++# which will return the search results when EXTERNAL_SEARCH is enabled. ++# ++# Doxygen ships with an example indexer (doxyindexer) and search engine ++# (doxysearch.cgi) which are based on the open source search engine library ++# Xapian (see: ++# https://xapian.org/). See the section "External Indexing and Searching" for ++# details. ++# This tag requires that the tag SEARCHENGINE is set to YES. ++ ++SEARCHENGINE_URL = ++ ++# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed ++# search data is written to a file for indexing by an external tool. With the ++# SEARCHDATA_FILE tag the name of this file can be specified. ++# The default file is: searchdata.xml. ++# This tag requires that the tag SEARCHENGINE is set to YES. ++ ++SEARCHDATA_FILE = searchdata.xml ++ ++# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the ++# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is ++# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple ++# projects and redirect the results back to the right project. ++# This tag requires that the tag SEARCHENGINE is set to YES. ++ ++EXTERNAL_SEARCH_ID = ++ ++# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen ++# projects other than the one defined by this configuration file, but that are ++# all added to the same external search index. Each project needs to have a ++# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of ++# to a relative location where the documentation can be found. The format is: ++# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ... ++# This tag requires that the tag SEARCHENGINE is set to YES. ++ ++EXTRA_SEARCH_MAPPINGS = ++ ++#--------------------------------------------------------------------------- ++# Configuration options related to the LaTeX output ++#--------------------------------------------------------------------------- ++ ++# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output. ++# The default value is: YES. ++ ++GENERATE_LATEX = NO ++ ++# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a ++# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of ++# it. ++# The default directory is: latex. ++# This tag requires that the tag GENERATE_LATEX is set to YES. ++ ++LATEX_OUTPUT = latex ++ ++# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be ++# invoked. ++# ++# Note that when not enabling USE_PDFLATEX the default is latex when enabling ++# USE_PDFLATEX the default is pdflatex and when in the later case latex is ++# chosen this is overwritten by pdflatex. For specific output languages the ++# default can have been set differently, this depends on the implementation of ++# the output language. ++# This tag requires that the tag GENERATE_LATEX is set to YES. ++ ++LATEX_CMD_NAME = latex ++ ++# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate ++# index for LaTeX. ++# Note: This tag is used in the Makefile / make.bat. ++# See also: LATEX_MAKEINDEX_CMD for the part in the generated output file ++# (.tex). ++# The default file is: makeindex. ++# This tag requires that the tag GENERATE_LATEX is set to YES. ++ ++MAKEINDEX_CMD_NAME = makeindex ++ ++# The LATEX_MAKEINDEX_CMD tag can be used to specify the command name to ++# generate index for LaTeX. In case there is no backslash (\) as first character ++# it will be automatically added in the LaTeX code. ++# Note: This tag is used in the generated output file (.tex). ++# See also: MAKEINDEX_CMD_NAME for the part in the Makefile / make.bat. ++# The default value is: makeindex. ++# This tag requires that the tag GENERATE_LATEX is set to YES. ++ ++LATEX_MAKEINDEX_CMD = makeindex ++ ++# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX ++# documents. This may be useful for small projects and may help to save some ++# trees in general. ++# The default value is: NO. ++# This tag requires that the tag GENERATE_LATEX is set to YES. ++ ++COMPACT_LATEX = NO ++ ++# The PAPER_TYPE tag can be used to set the paper type that is used by the ++# printer. ++# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x ++# 14 inches) and executive (7.25 x 10.5 inches). ++# The default value is: a4. ++# This tag requires that the tag GENERATE_LATEX is set to YES. ++ ++PAPER_TYPE = a4 ++ ++# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names ++# that should be included in the LaTeX output. The package can be specified just ++# by its name or with the correct syntax as to be used with the LaTeX ++# \usepackage command. To get the times font for instance you can specify : ++# EXTRA_PACKAGES=times or EXTRA_PACKAGES={times} ++# To use the option intlimits with the amsmath package you can specify: ++# EXTRA_PACKAGES=[intlimits]{amsmath} ++# If left blank no extra packages will be included. ++# This tag requires that the tag GENERATE_LATEX is set to YES. ++ ++EXTRA_PACKAGES = ++ ++# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the ++# generated LaTeX document. The header should contain everything until the first ++# chapter. If it is left blank doxygen will generate a standard header. See ++# section "Doxygen usage" for information on how to let doxygen write the ++# default header to a separate file. ++# ++# Note: Only use a user-defined header if you know what you are doing! The ++# following commands have a special meaning inside the header: $title, ++# $datetime, $date, $doxygenversion, $projectname, $projectnumber, ++# $projectbrief, $projectlogo. Doxygen will replace $title with the empty ++# string, for the replacement values of the other commands the user is referred ++# to HTML_HEADER. ++# This tag requires that the tag GENERATE_LATEX is set to YES. ++ ++LATEX_HEADER = ++ ++# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the ++# generated LaTeX document. The footer should contain everything after the last ++# chapter. If it is left blank doxygen will generate a standard footer. See ++# LATEX_HEADER for more information on how to generate a default footer and what ++# special commands can be used inside the footer. ++# ++# Note: Only use a user-defined footer if you know what you are doing! ++# This tag requires that the tag GENERATE_LATEX is set to YES. ++ ++LATEX_FOOTER = ++ ++# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined ++# LaTeX style sheets that are included after the standard style sheets created ++# by doxygen. Using this option one can overrule certain style aspects. Doxygen ++# will copy the style sheet files to the output directory. ++# Note: The order of the extra style sheet files is of importance (e.g. the last ++# style sheet in the list overrules the setting of the previous ones in the ++# list). ++# This tag requires that the tag GENERATE_LATEX is set to YES. ++ ++LATEX_EXTRA_STYLESHEET = ++ ++# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or ++# other source files which should be copied to the LATEX_OUTPUT output ++# directory. Note that the files will be copied as-is; there are no commands or ++# markers available. ++# This tag requires that the tag GENERATE_LATEX is set to YES. ++ ++LATEX_EXTRA_FILES = ++ ++# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is ++# prepared for conversion to PDF (using ps2pdf or pdflatex). The PDF file will ++# contain links (just like the HTML output) instead of page references. This ++# makes the output suitable for online browsing using a PDF viewer. ++# The default value is: YES. ++# This tag requires that the tag GENERATE_LATEX is set to YES. ++ ++PDF_HYPERLINKS = NO ++ ++# If the USE_PDFLATEX tag is set to YES, doxygen will use the engine as ++# specified with LATEX_CMD_NAME to generate the PDF file directly from the LaTeX ++# files. Set this option to YES, to get a higher quality PDF documentation. ++# ++# See also section LATEX_CMD_NAME for selecting the engine. ++# The default value is: YES. ++# This tag requires that the tag GENERATE_LATEX is set to YES. ++ ++USE_PDFLATEX = NO ++ ++# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode ++# command to the generated LaTeX files. This will instruct LaTeX to keep running ++# if errors occur, instead of asking the user for help. This option is also used ++# when generating formulas in HTML. ++# The default value is: NO. ++# This tag requires that the tag GENERATE_LATEX is set to YES. ++ ++LATEX_BATCHMODE = NO ++ ++# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the ++# index chapters (such as File Index, Compound Index, etc.) in the output. ++# The default value is: NO. ++# This tag requires that the tag GENERATE_LATEX is set to YES. ++ ++LATEX_HIDE_INDICES = NO ++ ++# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source ++# code with syntax highlighting in the LaTeX output. ++# ++# Note that which sources are shown also depends on other settings such as ++# SOURCE_BROWSER. ++# The default value is: NO. ++# This tag requires that the tag GENERATE_LATEX is set to YES. ++ ++LATEX_SOURCE_CODE = NO ++ ++# The LATEX_BIB_STYLE tag can be used to specify the style to use for the ++# bibliography, e.g. plainnat, or ieeetr. See ++# https://en.wikipedia.org/wiki/BibTeX and \cite for more info. ++# The default value is: plain. ++# This tag requires that the tag GENERATE_LATEX is set to YES. ++ ++LATEX_BIB_STYLE = plain ++ ++# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated ++# page will contain the date and time when the page was generated. Setting this ++# to NO can help when comparing the output of multiple runs. ++# The default value is: NO. ++# This tag requires that the tag GENERATE_LATEX is set to YES. ++ ++LATEX_TIMESTAMP = NO ++ ++# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute) ++# path from which the emoji images will be read. If a relative path is entered, ++# it will be relative to the LATEX_OUTPUT directory. If left blank the ++# LATEX_OUTPUT directory will be used. ++# This tag requires that the tag GENERATE_LATEX is set to YES. ++ ++LATEX_EMOJI_DIRECTORY = ++ ++#--------------------------------------------------------------------------- ++# Configuration options related to the RTF output ++#--------------------------------------------------------------------------- ++ ++# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The ++# RTF output is optimized for Word 97 and may not look too pretty with other RTF ++# readers/editors. ++# The default value is: NO. ++ ++GENERATE_RTF = NO ++ ++# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. If a ++# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of ++# it. ++# The default directory is: rtf. ++# This tag requires that the tag GENERATE_RTF is set to YES. ++ ++RTF_OUTPUT = rtf ++ ++# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF ++# documents. This may be useful for small projects and may help to save some ++# trees in general. ++# The default value is: NO. ++# This tag requires that the tag GENERATE_RTF is set to YES. ++ ++COMPACT_RTF = NO ++ ++# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated will ++# contain hyperlink fields. The RTF file will contain links (just like the HTML ++# output) instead of page references. This makes the output suitable for online ++# browsing using Word or some other Word compatible readers that support those ++# fields. ++# ++# Note: WordPad (write) and others do not support links. ++# The default value is: NO. ++# This tag requires that the tag GENERATE_RTF is set to YES. ++ ++RTF_HYPERLINKS = NO ++ ++# Load stylesheet definitions from file. Syntax is similar to doxygen's ++# configuration file, i.e. a series of assignments. You only have to provide ++# replacements, missing definitions are set to their default value. ++# ++# See also section "Doxygen usage" for information on how to generate the ++# default style sheet that doxygen normally uses. ++# This tag requires that the tag GENERATE_RTF is set to YES. ++ ++RTF_STYLESHEET_FILE = ++ ++# Set optional variables used in the generation of an RTF document. Syntax is ++# similar to doxygen's configuration file. A template extensions file can be ++# generated using doxygen -e rtf extensionFile. ++# This tag requires that the tag GENERATE_RTF is set to YES. ++ ++RTF_EXTENSIONS_FILE = ++ ++# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code ++# with syntax highlighting in the RTF output. ++# ++# Note that which sources are shown also depends on other settings such as ++# SOURCE_BROWSER. ++# The default value is: NO. ++# This tag requires that the tag GENERATE_RTF is set to YES. ++ ++RTF_SOURCE_CODE = NO ++ ++#--------------------------------------------------------------------------- ++# Configuration options related to the man page output ++#--------------------------------------------------------------------------- ++ ++# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for ++# classes and files. ++# The default value is: NO. ++ ++GENERATE_MAN = NO ++ ++# The MAN_OUTPUT tag is used to specify where the man pages will be put. If a ++# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of ++# it. A directory man3 will be created inside the directory specified by ++# MAN_OUTPUT. ++# The default directory is: man. ++# This tag requires that the tag GENERATE_MAN is set to YES. ++ ++MAN_OUTPUT = man ++ ++# The MAN_EXTENSION tag determines the extension that is added to the generated ++# man pages. In case the manual section does not start with a number, the number ++# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is ++# optional. ++# The default value is: .3. ++# This tag requires that the tag GENERATE_MAN is set to YES. ++ ++MAN_EXTENSION = .3 ++ ++# The MAN_SUBDIR tag determines the name of the directory created within ++# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by ++# MAN_EXTENSION with the initial . removed. ++# This tag requires that the tag GENERATE_MAN is set to YES. ++ ++MAN_SUBDIR = ++ ++# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it ++# will generate one additional man file for each entity documented in the real ++# man page(s). These additional files only source the real man page, but without ++# them the man command would be unable to find the correct page. ++# The default value is: NO. ++# This tag requires that the tag GENERATE_MAN is set to YES. ++ ++MAN_LINKS = NO ++ ++#--------------------------------------------------------------------------- ++# Configuration options related to the XML output ++#--------------------------------------------------------------------------- ++ ++# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that ++# captures the structure of the code including all documentation. ++# The default value is: NO. ++ ++GENERATE_XML = NO ++ ++# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a ++# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of ++# it. ++# The default directory is: xml. ++# This tag requires that the tag GENERATE_XML is set to YES. ++ ++XML_OUTPUT = xml ++ ++# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program ++# listings (including syntax highlighting and cross-referencing information) to ++# the XML output. Note that enabling this will significantly increase the size ++# of the XML output. ++# The default value is: YES. ++# This tag requires that the tag GENERATE_XML is set to YES. ++ ++XML_PROGRAMLISTING = NO ++ ++# If the XML_NS_MEMB_FILE_SCOPE tag is set to YES, doxygen will include ++# namespace members in file scope as well, matching the HTML output. ++# The default value is: NO. ++# This tag requires that the tag GENERATE_XML is set to YES. ++ ++XML_NS_MEMB_FILE_SCOPE = NO ++ ++#--------------------------------------------------------------------------- ++# Configuration options related to the DOCBOOK output ++#--------------------------------------------------------------------------- ++ ++# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files ++# that can be used to generate PDF. ++# The default value is: NO. ++ ++GENERATE_DOCBOOK = NO ++ ++# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put. ++# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in ++# front of it. ++# The default directory is: docbook. ++# This tag requires that the tag GENERATE_DOCBOOK is set to YES. ++ ++DOCBOOK_OUTPUT = docbook ++ ++# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the ++# program listings (including syntax highlighting and cross-referencing ++# information) to the DOCBOOK output. Note that enabling this will significantly ++# increase the size of the DOCBOOK output. ++# The default value is: NO. ++# This tag requires that the tag GENERATE_DOCBOOK is set to YES. ++ ++DOCBOOK_PROGRAMLISTING = NO ++ ++#--------------------------------------------------------------------------- ++# Configuration options for the AutoGen Definitions output ++#--------------------------------------------------------------------------- ++ ++# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an ++# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures ++# the structure of the code including all documentation. Note that this feature ++# is still experimental and incomplete at the moment. ++# The default value is: NO. ++ ++GENERATE_AUTOGEN_DEF = NO ++ ++#--------------------------------------------------------------------------- ++# Configuration options related to the Perl module output ++#--------------------------------------------------------------------------- ++ ++# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module ++# file that captures the structure of the code including all documentation. ++# ++# Note that this feature is still experimental and incomplete at the moment. ++# The default value is: NO. ++ ++GENERATE_PERLMOD = NO ++ ++# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary ++# Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI ++# output from the Perl module output. ++# The default value is: NO. ++# This tag requires that the tag GENERATE_PERLMOD is set to YES. ++ ++PERLMOD_LATEX = NO ++ ++# If the PERLMOD_PRETTY tag is set to YES, the Perl module output will be nicely ++# formatted so it can be parsed by a human reader. This is useful if you want to ++# understand what is going on. On the other hand, if this tag is set to NO, the ++# size of the Perl module output will be much smaller and Perl will parse it ++# just the same. ++# The default value is: YES. ++# This tag requires that the tag GENERATE_PERLMOD is set to YES. ++ ++PERLMOD_PRETTY = YES ++ ++# The names of the make variables in the generated doxyrules.make file are ++# prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. This is useful ++# so different doxyrules.make files included by the same Makefile don't ++# overwrite each other's variables. ++# This tag requires that the tag GENERATE_PERLMOD is set to YES. ++ ++PERLMOD_MAKEVAR_PREFIX = ++ ++#--------------------------------------------------------------------------- ++# Configuration options related to the preprocessor ++#--------------------------------------------------------------------------- ++ ++# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all ++# C-preprocessor directives found in the sources and include files. ++# The default value is: YES. ++ ++ENABLE_PREPROCESSING = YES ++ ++# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names ++# in the source code. If set to NO, only conditional compilation will be ++# performed. Macro expansion can be done in a controlled way by setting ++# EXPAND_ONLY_PREDEF to YES. ++# The default value is: NO. ++# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. ++ ++MACRO_EXPANSION = YES ++ ++# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then ++# the macro expansion is limited to the macros specified with the PREDEFINED and ++# EXPAND_AS_DEFINED tags. ++# The default value is: NO. ++# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. ++ ++EXPAND_ONLY_PREDEF = NO ++ ++# If the SEARCH_INCLUDES tag is set to YES, the include files in the ++# INCLUDE_PATH will be searched if a #include is found. ++# The default value is: YES. ++# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. ++ ++SEARCH_INCLUDES = YES ++ ++# The INCLUDE_PATH tag can be used to specify one or more directories that ++# contain include files that are not input files but should be processed by the ++# preprocessor. ++# This tag requires that the tag SEARCH_INCLUDES is set to YES. ++ ++INCLUDE_PATH = ++ ++# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard ++# patterns (like *.h and *.hpp) to filter out the header-files in the ++# directories. If left blank, the patterns specified with FILE_PATTERNS will be ++# used. ++# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. ++ ++INCLUDE_FILE_PATTERNS = ++ ++# The PREDEFINED tag can be used to specify one or more macro names that are ++# defined before the preprocessor is started (similar to the -D option of e.g. ++# gcc). The argument of the tag is a list of macros of the form: name or ++# name=definition (no spaces). If the definition and the "=" are omitted, "=1" ++# is assumed. To prevent a macro definition from being undefined via #undef or ++# recursively expanded use the := operator instead of the = operator. ++# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. ++ ++PREDEFINED = ++ ++# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this ++# tag can be used to specify a list of macro names that should be expanded. The ++# macro definition that is found in the sources will be used. Use the PREDEFINED ++# tag if you want to use a different macro definition that overrules the ++# definition found in the source code. ++# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. ++ ++EXPAND_AS_DEFINED = ++ ++# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will ++# remove all references to function-like macros that are alone on a line, have ++# an all uppercase name, and do not end with a semicolon. Such function macros ++# are typically used for boiler-plate code, and will confuse the parser if not ++# removed. ++# The default value is: YES. ++# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. ++ ++SKIP_FUNCTION_MACROS = YES ++ ++#--------------------------------------------------------------------------- ++# Configuration options related to external references ++#--------------------------------------------------------------------------- ++ ++# The TAGFILES tag can be used to specify one or more tag files. For each tag ++# file the location of the external documentation should be added. The format of ++# a tag file without this location is as follows: ++# TAGFILES = file1 file2 ... ++# Adding location for the tag files is done as follows: ++# TAGFILES = file1=loc1 "file2 = loc2" ... ++# where loc1 and loc2 can be relative or absolute paths or URLs. See the ++# section "Linking to external documentation" for more information about the use ++# of tag files. ++# Note: Each tag file must have a unique name (where the name does NOT include ++# the path). If a tag file is not located in the directory in which doxygen is ++# run, you must also specify the path to the tagfile here. ++ ++TAGFILES = ++ ++# When a file name is specified after GENERATE_TAGFILE, doxygen will create a ++# tag file that is based on the input files it reads. See section "Linking to ++# external documentation" for more information about the usage of tag files. ++ ++GENERATE_TAGFILE = ++ ++# If the ALLEXTERNALS tag is set to YES, all external class will be listed in ++# the class index. If set to NO, only the inherited external classes will be ++# listed. ++# The default value is: NO. ++ ++ALLEXTERNALS = NO ++ ++# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed ++# in the modules index. If set to NO, only the current project's groups will be ++# listed. ++# The default value is: YES. ++ ++EXTERNAL_GROUPS = YES ++ ++# If the EXTERNAL_PAGES tag is set to YES, all external pages will be listed in ++# the related pages index. If set to NO, only the current project's pages will ++# be listed. ++# The default value is: YES. ++ ++EXTERNAL_PAGES = YES ++ ++#--------------------------------------------------------------------------- ++# Configuration options related to the dot tool ++#--------------------------------------------------------------------------- ++ ++# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram ++# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to ++# NO turns the diagrams off. Note that this option also works with HAVE_DOT ++# disabled, but it is recommended to install and use dot, since it yields more ++# powerful graphs. ++# The default value is: YES. ++ ++CLASS_DIAGRAMS = YES ++ ++# You can include diagrams made with dia in doxygen documentation. Doxygen will ++# then run dia to produce the diagram and insert it in the documentation. The ++# DIA_PATH tag allows you to specify the directory where the dia binary resides. ++# If left empty dia is assumed to be found in the default search path. ++ ++DIA_PATH = ++ ++# If set to YES the inheritance and collaboration graphs will hide inheritance ++# and usage relations if the target is undocumented or is not a class. ++# The default value is: YES. ++ ++HIDE_UNDOC_RELATIONS = YES ++ ++# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is ++# available from the path. This tool is part of Graphviz (see: ++# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent ++# Bell Labs. The other options in this section have no effect if this option is ++# set to NO ++# The default value is: NO. ++ ++HAVE_DOT = YES ++ ++# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed ++# to run in parallel. When set to 0 doxygen will base this on the number of ++# processors available in the system. You can set it explicitly to a value ++# larger than 0 to get control over the balance between CPU load and processing ++# speed. ++# Minimum value: 0, maximum value: 32, default value: 0. ++# This tag requires that the tag HAVE_DOT is set to YES. ++ ++DOT_NUM_THREADS = 0 ++ ++# When you want a differently looking font in the dot files that doxygen ++# generates you can specify the font name using DOT_FONTNAME. You need to make ++# sure dot is able to find the font, which can be done by putting it in a ++# standard location or by setting the DOTFONTPATH environment variable or by ++# setting DOT_FONTPATH to the directory containing the font. ++# The default value is: Helvetica. ++# This tag requires that the tag HAVE_DOT is set to YES. ++ ++DOT_FONTNAME = Helvetica ++ ++# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of ++# dot graphs. ++# Minimum value: 4, maximum value: 24, default value: 10. ++# This tag requires that the tag HAVE_DOT is set to YES. ++ ++DOT_FONTSIZE = 10 ++ ++# By default doxygen will tell dot to use the default font as specified with ++# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set ++# the path where dot can find it using this tag. ++# This tag requires that the tag HAVE_DOT is set to YES. ++ ++DOT_FONTPATH = ++ ++# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for ++# each documented class showing the direct and indirect inheritance relations. ++# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO. ++# The default value is: YES. ++# This tag requires that the tag HAVE_DOT is set to YES. ++ ++CLASS_GRAPH = YES ++ ++# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a ++# graph for each documented class showing the direct and indirect implementation ++# dependencies (inheritance, containment, and class references variables) of the ++# class with other documented classes. ++# The default value is: YES. ++# This tag requires that the tag HAVE_DOT is set to YES. ++ ++COLLABORATION_GRAPH = NO ++ ++# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for ++# groups, showing the direct groups dependencies. ++# The default value is: YES. ++# This tag requires that the tag HAVE_DOT is set to YES. ++ ++GROUP_GRAPHS = YES ++ ++# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and ++# collaboration diagrams in a style similar to the OMG's Unified Modeling ++# Language. ++# The default value is: NO. ++# This tag requires that the tag HAVE_DOT is set to YES. ++ ++UML_LOOK = NO ++ ++# If the UML_LOOK tag is enabled, the fields and methods are shown inside the ++# class node. If there are many fields or methods and many nodes the graph may ++# become too big to be useful. The UML_LIMIT_NUM_FIELDS threshold limits the ++# number of items for each type to make the size more manageable. Set this to 0 ++# for no limit. Note that the threshold may be exceeded by 50% before the limit ++# is enforced. So when you set the threshold to 10, up to 15 fields may appear, ++# but if the number exceeds 15, the total amount of fields shown is limited to ++# 10. ++# Minimum value: 0, maximum value: 100, default value: 10. ++# This tag requires that the tag UML_LOOK is set to YES. ++ ++UML_LIMIT_NUM_FIELDS = 10 ++ ++# If the DOT_UML_DETAILS tag is set to NO, doxygen will show attributes and ++# methods without types and arguments in the UML graphs. If the DOT_UML_DETAILS ++# tag is set to YES, doxygen will add type and arguments for attributes and ++# methods in the UML graphs. If the DOT_UML_DETAILS tag is set to NONE, doxygen ++# will not generate fields with class member information in the UML graphs. The ++# class diagrams will look similar to the default class diagrams but using UML ++# notation for the relationships. ++# Possible values are: NO, YES and NONE. ++# The default value is: NO. ++# This tag requires that the tag UML_LOOK is set to YES. ++ ++DOT_UML_DETAILS = NO ++ ++# The DOT_WRAP_THRESHOLD tag can be used to set the maximum number of characters ++# to display on a single line. If the actual line length exceeds this threshold ++# significantly it will wrapped across multiple lines. Some heuristics are apply ++# to avoid ugly line breaks. ++# Minimum value: 0, maximum value: 1000, default value: 17. ++# This tag requires that the tag HAVE_DOT is set to YES. ++ ++DOT_WRAP_THRESHOLD = 17 ++ ++# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and ++# collaboration graphs will show the relations between templates and their ++# instances. ++# The default value is: NO. ++# This tag requires that the tag HAVE_DOT is set to YES. ++ ++TEMPLATE_RELATIONS = NO ++ ++# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to ++# YES then doxygen will generate a graph for each documented file showing the ++# direct and indirect include dependencies of the file with other documented ++# files. ++# The default value is: YES. ++# This tag requires that the tag HAVE_DOT is set to YES. ++ ++INCLUDE_GRAPH = YES ++ ++# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are ++# set to YES then doxygen will generate a graph for each documented file showing ++# the direct and indirect include dependencies of the file with other documented ++# files. ++# The default value is: YES. ++# This tag requires that the tag HAVE_DOT is set to YES. ++ ++INCLUDED_BY_GRAPH = YES ++ ++# If the CALL_GRAPH tag is set to YES then doxygen will generate a call ++# dependency graph for every global function or class method. ++# ++# Note that enabling this option will significantly increase the time of a run. ++# So in most cases it will be better to enable call graphs for selected ++# functions only using the \callgraph command. Disabling a call graph can be ++# accomplished by means of the command \hidecallgraph. ++# The default value is: NO. ++# This tag requires that the tag HAVE_DOT is set to YES. ++ ++CALL_GRAPH = YES ++ ++# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller ++# dependency graph for every global function or class method. ++# ++# Note that enabling this option will significantly increase the time of a run. ++# So in most cases it will be better to enable caller graphs for selected ++# functions only using the \callergraph command. Disabling a caller graph can be ++# accomplished by means of the command \hidecallergraph. ++# The default value is: NO. ++# This tag requires that the tag HAVE_DOT is set to YES. ++ ++CALLER_GRAPH = NO ++ ++# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical ++# hierarchy of all classes instead of a textual one. ++# The default value is: YES. ++# This tag requires that the tag HAVE_DOT is set to YES. ++ ++GRAPHICAL_HIERARCHY = YES ++ ++# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the ++# dependencies a directory has on other directories in a graphical way. The ++# dependency relations are determined by the #include relations between the ++# files in the directories. ++# The default value is: YES. ++# This tag requires that the tag HAVE_DOT is set to YES. ++ ++DIRECTORY_GRAPH = YES ++ ++# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images ++# generated by dot. For an explanation of the image formats see the section ++# output formats in the documentation of the dot tool (Graphviz (see: ++# http://www.graphviz.org/)). ++# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order ++# to make the SVG files visible in IE 9+ (other browsers do not have this ++# requirement). ++# Possible values are: png, jpg, gif, svg, png:gd, png:gd:gd, png:cairo, ++# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and ++# png:gdiplus:gdiplus. ++# The default value is: png. ++# This tag requires that the tag HAVE_DOT is set to YES. ++ ++DOT_IMAGE_FORMAT = png ++ ++# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to ++# enable generation of interactive SVG images that allow zooming and panning. ++# ++# Note that this requires a modern browser other than Internet Explorer. Tested ++# and working are Firefox, Chrome, Safari, and Opera. ++# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make ++# the SVG files visible. Older versions of IE do not have SVG support. ++# The default value is: NO. ++# This tag requires that the tag HAVE_DOT is set to YES. ++ ++INTERACTIVE_SVG = NO ++ ++# The DOT_PATH tag can be used to specify the path where the dot tool can be ++# found. If left blank, it is assumed the dot tool can be found in the path. ++# This tag requires that the tag HAVE_DOT is set to YES. ++ ++DOT_PATH = ++ ++# The DOTFILE_DIRS tag can be used to specify one or more directories that ++# contain dot files that are included in the documentation (see the \dotfile ++# command). ++# This tag requires that the tag HAVE_DOT is set to YES. ++ ++DOTFILE_DIRS = ++ ++# The MSCFILE_DIRS tag can be used to specify one or more directories that ++# contain msc files that are included in the documentation (see the \mscfile ++# command). ++ ++MSCFILE_DIRS = ++ ++# The DIAFILE_DIRS tag can be used to specify one or more directories that ++# contain dia files that are included in the documentation (see the \diafile ++# command). ++ ++DIAFILE_DIRS = ++ ++# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the ++# path where java can find the plantuml.jar file. If left blank, it is assumed ++# PlantUML is not used or called during a preprocessing step. Doxygen will ++# generate a warning when it encounters a \startuml command in this case and ++# will not generate output for the diagram. ++ ++PLANTUML_JAR_PATH = ++ ++# When using plantuml, the PLANTUML_CFG_FILE tag can be used to specify a ++# configuration file for plantuml. ++ ++PLANTUML_CFG_FILE = ++ ++# When using plantuml, the specified paths are searched for files specified by ++# the !include statement in a plantuml block. ++ ++PLANTUML_INCLUDE_PATH = ++ ++# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes ++# that will be shown in the graph. If the number of nodes in a graph becomes ++# larger than this value, doxygen will truncate the graph, which is visualized ++# by representing a node as a red box. Note that doxygen if the number of direct ++# children of the root node in a graph is already larger than ++# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that ++# the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH. ++# Minimum value: 0, maximum value: 10000, default value: 50. ++# This tag requires that the tag HAVE_DOT is set to YES. ++ ++DOT_GRAPH_MAX_NODES = 200 ++ ++# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs ++# generated by dot. A depth value of 3 means that only nodes reachable from the ++# root by following a path via at most 3 edges will be shown. Nodes that lay ++# further from the root node will be omitted. Note that setting this option to 1 ++# or 2 may greatly reduce the computation time needed for large code bases. Also ++# note that the size of a graph can be further restricted by ++# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction. ++# Minimum value: 0, maximum value: 1000, default value: 0. ++# This tag requires that the tag HAVE_DOT is set to YES. ++ ++MAX_DOT_GRAPH_DEPTH = 0 ++ ++# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent ++# background. This is disabled by default, because dot on Windows does not seem ++# to support this out of the box. ++# ++# Warning: Depending on the platform used, enabling this option may lead to ++# badly anti-aliased labels on the edges of a graph (i.e. they become hard to ++# read). ++# The default value is: NO. ++# This tag requires that the tag HAVE_DOT is set to YES. ++ ++DOT_TRANSPARENT = NO ++ ++# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output ++# files in one run (i.e. multiple -o and -T options on the command line). This ++# makes dot run faster, but since only newer versions of dot (>1.8.10) support ++# this, this feature is disabled by default. ++# The default value is: NO. ++# This tag requires that the tag HAVE_DOT is set to YES. ++ ++DOT_MULTI_TARGETS = NO ++ ++# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page ++# explaining the meaning of the various boxes and arrows in the dot generated ++# graphs. ++# The default value is: YES. ++# This tag requires that the tag HAVE_DOT is set to YES. ++ ++GENERATE_LEGEND = YES ++ ++# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate ++# files that are used to generate the various graphs. ++# ++# Note: This setting is not only used for dot files but also for msc and ++# plantuml temporary files. ++# The default value is: YES. ++ ++DOT_CLEANUP = YES +diff --git a/src/hooks/dhcp/ping_check/Makefile.am b/src/hooks/dhcp/ping_check/Makefile.am +new file mode 100644 +index 0000000000..a7ea17f400 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/Makefile.am +@@ -0,0 +1,104 @@ ++SUBDIRS = . libloadtests tests ++ ++AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib ++AM_CPPFLAGS += $(BOOST_INCLUDES) $(CRYPTO_CFLAGS) $(CRYPTO_INCLUDES) ++AM_CXXFLAGS = $(KEA_CXXFLAGS) ++ ++# Ensure that the message file and doxygen file is included in the distribution ++EXTRA_DIST = ping_check_messages.mes ++ ++CLEANFILES = *.gcno *.gcda ++ ++# convenience archive ++ ++noinst_LTLIBRARIES = libping_check.la ++ ++libping_check_la_SOURCES = ping_check_callouts.cc ++libping_check_la_SOURCES += ping_check_log.cc ping_check_log.h ++libping_check_la_SOURCES += ping_check_messages.cc ping_check_messages.h ++libping_check_la_SOURCES += icmp_endpoint.h icmp_socket.h ++libping_check_la_SOURCES += ping_context.cc ping_context.h ++libping_check_la_SOURCES += ping_context_store.cc ping_context_store.h ++libping_check_la_SOURCES += icmp_msg.h icmp_msg.cc ++libping_check_la_SOURCES += ping_channel.cc ping_channel.h ++libping_check_la_SOURCES += ping_check_mgr.cc ping_check_mgr.h ++libping_check_la_SOURCES += ping_check_config.cc ping_check_config.h ++libping_check_la_SOURCES += config_cache.cc config_cache.h ++libping_check_la_SOURCES += version.cc ++ ++libping_check_la_CXXFLAGS = $(AM_CXXFLAGS) ++libping_check_la_CPPFLAGS = $(AM_CPPFLAGS) ++ ++# install the shared object into $(libdir)/kea/hooks ++lib_hooksdir = $(libdir)/kea/hooks ++lib_hooks_LTLIBRARIES = libdhcp_ping_check.la ++ ++libdhcp_ping_check_la_SOURCES = ++libdhcp_ping_check_la_LDFLAGS = $(AM_LDFLAGS) ++libdhcp_ping_check_la_LDFLAGS += -avoid-version -export-dynamic -module ++libdhcp_ping_check_la_LIBADD = libping_check.la ++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la ++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/process/libkea-process.la ++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/eval/libkea-eval.la ++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la ++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/stats/libkea-stats.la ++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/config/libkea-cfgclient.la ++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/http/libkea-http.la ++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la ++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la ++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/database/libkea-database.la ++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/cc/libkea-cc.la ++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la ++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/dns/libkea-dns++.la ++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la ++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/log/libkea-log.la ++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/util/libkea-util.la ++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la ++libdhcp_ping_check_la_LIBADD += $(LOG4CPLUS_LIBS) ++libdhcp_ping_check_la_LIBADD += $(CRYPTO_LIBS) ++libdhcp_ping_check_la_LIBADD += $(BOOST_LIBS) ++ ++# Doxygen documentation ++EXTRA_DIST += ping_check.dox Doxyfile ++ ++devel: ++ mkdir -p html ++ (cat Doxyfile; echo PROJECT_NUMBER=$(PACKAGE_VERSION)) | doxygen - > html/doxygen.log 2> html/doxygen-error.log ++ echo `grep -i ": warning:" html/doxygen-error.log | wc -l` warnings/errors detected. ++ ++clean-local: ++ rm -rf html ++ ++# If we want to get rid of all generated messages files, we need to use ++# make maintainer-clean. The proper way to introduce custom commands for ++# that operation is to define maintainer-clean-local target. However, ++# make maintainer-clean also removes Makefile, so running configure script ++# is required. To make it easy to rebuild messages without going through ++# reconfigure, a new target messages-clean has been added. ++maintainer-clean-local: ++ rm -f ping_check_messages.h ping_check_messages.cc ++ ++# To regenerate messages files, one can do: ++# ++# make messages-clean ++# make messages ++# ++# This is needed only when a .mes file is modified. ++messages-clean: maintainer-clean-local ++ ++if GENERATE_MESSAGES ++ ++# Define rule to build logging source files from message file ++messages: ping_check_messages.h ping_check_messages.cc ++ @echo Message files regenerated ++ ++ping_check_messages.h ping_check_messages.cc: ping_check_messages.mes ++ (cd $(top_srcdir); \ ++ $(abs_top_builddir)/src/lib/log/compiler/kea-msg-compiler src/hooks/dhcp/ping_check/ping_check_messages.mes) ++ ++else ++ ++messages ping_check_messages.h ping_check_messages.cc: ++ @echo Messages generation disabled. Configure with --enable-generate-messages to enable it. ++ ++endif +diff --git a/src/hooks/dhcp/ping_check/config_cache.cc b/src/hooks/dhcp/ping_check/config_cache.cc +new file mode 100644 +index 0000000000..9a8f9dd4bb +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/config_cache.cc +@@ -0,0 +1,107 @@ ++// Copyright (C) 2023-2025 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 <config_cache.h> ++#include <util/multi_threading_mgr.h> ++ ++using namespace isc; ++using namespace isc::data; ++using namespace isc::dhcp; ++using namespace isc::util; ++using namespace std; ++ ++namespace isc { ++namespace ping_check { ++ ++PingCheckConfigPtr& ++ConfigCache::getGlobalConfig() { ++ return (global_config_); ++} ++ ++void ++ConfigCache::setGlobalConfig(PingCheckConfigPtr& config) { ++ if (!config) { ++ isc_throw(BadValue, "ConfigCache - global config cannot be empty"); ++ } ++ ++ global_config_ = config; ++} ++ ++bool ++ConfigCache::findConfig(const SubnetID& subnet_id, PingCheckConfigPtr& config) { ++ MultiThreadingLock lock(*mutex_); ++ return (findConfigInternal(subnet_id, config)); ++} ++ ++bool ++ConfigCache::findConfigInternal(const SubnetID& subnet_id, PingCheckConfigPtr& config) const { ++ auto it = configs_.find(subnet_id); ++ if (it != configs_.end()) { ++ config = it->second; ++ return (true); ++ } ++ ++ config = PingCheckConfigPtr(); ++ return (false); ++} ++ ++PingCheckConfigPtr ++ConfigCache::parseAndCacheConfig(const SubnetID& subnet_id, ConstElementPtr& user_context) { ++ PingCheckConfigPtr config; ++ if (user_context) { ++ ConstElementPtr ping_check_params = user_context->get("ping-check"); ++ if (ping_check_params) { ++ // Copy construct from global to start with. ++ config.reset(new PingCheckConfig(*getGlobalConfig())); ++ ++ // Now parse in subnet-specific values. This may throw a DhcpConfigError but ++ // that's OK, dealt with by the caller. ++ try { ++ config->parse(ping_check_params); ++ } catch (...) { ++ throw; ++ } ++ } ++ } ++ ++ // Cache the config. We allow empty configs so higher precedence scopes may ++ // override lower precedence scopes. ++ cacheConfig(subnet_id, config); ++ return (config); ++} ++ ++void ++ConfigCache::cacheConfig(const SubnetID& subnet_id, PingCheckConfigPtr& config) { ++ MultiThreadingLock lock(*mutex_); ++ configs_[subnet_id] = config; ++} ++ ++void ++ConfigCache::flush() { ++ MultiThreadingLock lock(*mutex_); ++ // Discard the contents. ++ configs_.clear(); ++ ++ // We use modification time to remember the last time we flushed. ++ updateModificationTime(); ++} ++ ++size_t ++ConfigCache::size() { ++ MultiThreadingLock lock(*mutex_); ++ return (configs_.size()); ++} ++ ++boost::posix_time::ptime ++ConfigCache::getLastFlushTime() { ++ MultiThreadingLock lock(*mutex_); ++ return (BaseStampedElement::getModificationTime()); ++} ++ ++} // end of namespace ping_check ++} // end of namespace isc +diff --git a/src/hooks/dhcp/ping_check/config_cache.h b/src/hooks/dhcp/ping_check/config_cache.h +new file mode 100644 +index 0000000000..b69cf6f124 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/config_cache.h +@@ -0,0 +1,146 @@ ++// Copyright (C) 2023-2025 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 CONFIG_CACHE_H ++#define CONFIG_CACHE_H ++ ++#include <ping_check_config.h> ++#include <cc/base_stamped_element.h> ++#include <cc/data.h> ++#include <dhcpsrv/subnet.h> ++ ++#include <map> ++#include <mutex> ++ ++namespace isc { ++namespace ping_check { ++ ++/// @brief ConfigCache stores ping check config per subnet ++/// ++/// The intent is parse subnet ping-check parameters from its user-context ++/// as few times as possible rather than on every ping check request, while ++/// also allowing for run time updates via config back end or subnet cmds. ++/// ++/// For every subnet we store: ++/// ++/// -# subnet id ++/// -# PingCheckConfig pointer ++/// where: ++/// - empty config pointer means that subnet does not specify ping check config ++/// - non-empty means subnet specifies at least some ping check parameters ++/// ++/// Each time we clear the cache we update the modification time. ++/// ++/// When presented with a subnet: ++/// ++/// 1. no cache entry: ++/// cache it ++/// ++/// 2. entry exists: ++/// subnet mod time >= last flush ++/// cache is stale flush it ++/// cache it ++/// ++/// subnet mod time < last flush ++/// use it ++/// ++class ConfigCache : public data::BaseStampedElement { ++public: ++ /// @brief Constructor ++ ConfigCache() : configs_(), global_config_(new PingCheckConfig()), mutex_(new std::mutex) { ++ } ++ ++ /// @brief Destructor ++ virtual ~ConfigCache() = default; ++ ++ /// @brief Get the config for a given subnet. ++ /// ++ /// @param subnet_id ID of the subnet for which the config is desired. ++ /// @param[out] config a reference to a pointer in which to store the ++ /// config if found. If there is no entry for the subnet, it will be set ++ /// to an empty pointer. ++ /// ++ /// @return True if an entry for subnet was found, false otherwise. This ++ /// allows callers to distinguish between unknown subnets (entries that do ++ /// not exist) and subnets that are known but do not define a config. ++ bool findConfig(const dhcp::SubnetID& subnet_id, ++ PingCheckConfigPtr& config); ++ ++ /// @brief Parses a config string and caches for the given subnet. ++ /// ++ /// @param subnet_id ID of the subnet for which the config is desired. ++ /// @param user_context user-context Element map of the subnet. ++ /// ++ /// @return pointer to the parsed config. ++ /// @throw BadValue if an error occurred during config parsing. ++ PingCheckConfigPtr parseAndCacheConfig(const dhcp::SubnetID& subnet_id, ++ data::ConstElementPtr& user_context); ++ ++ /// @brief Adds (or replaces) the config for a given subnet to the cache. ++ /// ++ /// @param subnet_id ID of the subnet for which the config is desired. ++ /// @param config pointer to the config to store. This may be an ++ /// empty pointer. ++ void cacheConfig(const dhcp::SubnetID& subnet_id, ++ PingCheckConfigPtr& config); ++ ++ /// @brief Discards the subnet entries in the cache. ++ void flush(); ++ ++ /// @brief Get the number of entries in the cache. ++ /// ++ /// @return number of entries in the cache. ++ size_t size(); ++ ++ /// @brief Get the last time the cache was flushed. ++ /// ++ /// @return the last time the cache was flushed (or the time it was ++ /// created if it has never been flushed). ++ boost::posix_time::ptime getLastFlushTime(); ++ ++ /// @brief Get the global level configuration. ++ /// ++ /// @return pointer to the global configuration. ++ PingCheckConfigPtr& getGlobalConfig(); ++ ++ /// @brief Set the global level configuration. ++ /// ++ /// @param config configuration to store as the global configuration. ++ void setGlobalConfig(PingCheckConfigPtr& config); ++ ++private: ++ /// @brief Get the config for a given subnet. ++ /// ++ /// Must be called from with a thread-safe context. ++ /// ++ /// @param subnet_id ID of the subnet for which the config is desired. ++ /// @param[out] config a reference to a pointer in which to store the ++ /// config if found. If there is no entry for the subnet, it will be set ++ /// to an empty pointer. ++ /// ++ /// @return True if an entry for subnet was found, false otherwise. This ++ /// allows callers to distinguish between unknown subnets (entries that do ++ /// not exist) and subnets that are known but do not define a config. ++ bool findConfigInternal(const dhcp::SubnetID& subnet_id, ++ PingCheckConfigPtr& config) const; ++ ++ /// @brief Per subnet config cache. Note that the global config in stored ++ /// using SUBNET_ID_GLOBAL. ++ std::map<dhcp::SubnetID, PingCheckConfigPtr> configs_; ++ ++ /// @brief Stores the global configuration parameters. ++ PingCheckConfigPtr global_config_; ++ ++ /// @brief The mutex used to protect internal state. ++ const boost::scoped_ptr<std::mutex> mutex_; ++}; ++ ++/// @brief Defines a shared pointer to a ConfigCache. ++typedef boost::shared_ptr<ConfigCache> ConfigCachePtr; ++ ++} // end of namespace ping_check ++} // end of namespace isc ++#endif +diff --git a/src/hooks/dhcp/ping_check/icmp_endpoint.h b/src/hooks/dhcp/ping_check/icmp_endpoint.h +new file mode 100644 +index 0000000000..5d047d286f +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/icmp_endpoint.h +@@ -0,0 +1,134 @@ ++// Copyright (C) 2023-2025 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 ICMP_ENDPOINT_H ++#define ICMP_ENDPOINT_H 1 ++ ++#include <asiolink/io_endpoint.h> ++ ++namespace isc { ++namespace ping_check { ++ ++/// @brief The @c ICMPEndpoint class is a concrete derived class of ++/// @c IOEndpoint that represents an endpoint of a ICMP packet. ++/// ++/// Other notes about @c TCPEndpoint applies to this class, too. ++class ICMPEndpoint : public asiolink::IOEndpoint { ++public: ++ /// ++ /// @name Constructors and Destructor. ++ /// ++ //@{ ++ ++ /// @brief Default Constructor ++ /// ++ /// Creates an internal endpoint. This is expected to be set by some ++ /// external call. ++ ICMPEndpoint() : ++ asio_endpoint_placeholder_(new boost::asio::ip::icmp::endpoint()), ++ asio_endpoint_(*asio_endpoint_placeholder_) ++ {} ++ ++ /// @brief Constructor from an address. ++ /// ++ /// @param address The IP address of the endpoint. ++ explicit ICMPEndpoint(const asiolink::IOAddress& address) : ++ asio_endpoint_placeholder_( ++ new boost::asio::ip::icmp::endpoint(boost::asio::ip::make_address(address.toText()), 0)), ++ asio_endpoint_(*asio_endpoint_placeholder_) ++ {} ++ ++ /// @brief Copy Constructor from an ASIO ICMP endpoint. ++ /// ++ /// This constructor is designed to be an efficient wrapper for the ++ /// corresponding ASIO class, @c icmp::endpoint. ++ /// ++ /// @param asio_endpoint The ASIO representation of the ICMP endpoint. ++ explicit ICMPEndpoint(boost::asio::ip::icmp::endpoint& asio_endpoint) : ++ asio_endpoint_placeholder_(0), asio_endpoint_(asio_endpoint) ++ {} ++ ++ /// @brief Constructor from a const ASIO ICMP endpoint. ++ /// ++ /// This constructor is designed to be an efficient wrapper for the ++ /// corresponding ASIO class, @c icmp::endpoint. ++ /// ++ /// @param asio_endpoint The ASIO representation of the TCP endpoint. ++ explicit ICMPEndpoint(const boost::asio::ip::icmp::endpoint& asio_endpoint) : ++ asio_endpoint_placeholder_(new boost::asio::ip::icmp::endpoint(asio_endpoint)), ++ asio_endpoint_(*asio_endpoint_placeholder_) ++ {} ++ ++ /// @brief The destructor. ++ virtual ~ICMPEndpoint() { delete asio_endpoint_placeholder_; } ++ //@} ++ ++ /// @brief Fetches the IP address of the endpoint. ++ /// ++ /// @return the endpoint's IP address as an IOAddress. ++ virtual asiolink::IOAddress getAddress() const { ++ return (asio_endpoint_.address()); ++ } ++ ++ /// @brief Fetches the IP address of the endpoint in native form. ++ /// ++ /// @return the endpoint's IP address as a struct sockaddr. ++ virtual const struct sockaddr& getSockAddr() const { ++ return (*asio_endpoint_.data()); ++ } ++ ++ /// @brief Fetches the IP port number of the endpoint. ++ /// ++ /// @return the endpoint's port number as a unit16_t. ++ virtual uint16_t getPort() const { ++ return (asio_endpoint_.port()); ++ } ++ ++ /// @brief Fetches the network protocol of the endpoint. ++ /// ++ /// @return the endpoint's protocol as a short ++ virtual short getProtocol() const { ++ return (asio_endpoint_.protocol().protocol()); ++ } ++ ++ /// @brief Fetches the network protocol family of the endpoint. ++ /// ++ /// @return the endpoint's protocol as a short ++ virtual short getFamily() const { ++ return (asio_endpoint_.protocol().family()); ++ } ++ ++ /// @brief Fetches the underlying ASIO endpoint implementation ++ /// ++ /// This is not part of the exposed IOEndpoint API but allows ++ /// direct access to the ASIO implementation of the endpoint ++ /// ++ /// @return the wrapped ASIO endpoint instance as a const ++ inline const boost::asio::ip::icmp::endpoint& getASIOEndpoint() const { ++ return (asio_endpoint_); ++ } ++ ++ /// @brief Fetches the underlying ASIO endpoint implementation ++ /// ++ /// This is not part of the exposed IOEndpoint API but allows ++ /// direct access to the ASIO implementation of the endpoint ++ /// ++ /// @return the wrapped ASIO endpoint instance as a non-const ++ inline boost::asio::ip::icmp::endpoint& getASIOEndpoint() { ++ return (asio_endpoint_); ++ } ++ ++private: ++ /// @brief Pointer to the ASIO endpoint placeholder. ++ boost::asio::ip::icmp::endpoint* asio_endpoint_placeholder_; ++ ++ /// @brief Reference to the underlying ASIO endpoint instance. ++ boost::asio::ip::icmp::endpoint& asio_endpoint_; ++}; ++ ++} // namespace ping_check ++} // namespace isc ++#endif // ICMP_ENDPOINT_H +diff --git a/src/hooks/dhcp/ping_check/icmp_msg.cc b/src/hooks/dhcp/ping_check/icmp_msg.cc +new file mode 100644 +index 0000000000..3d236820da +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/icmp_msg.cc +@@ -0,0 +1,112 @@ ++// Copyright (C) 2023-2025 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 <icmp_msg.h> ++#include <util/io.h> ++#include <exceptions/exceptions.h> ++ ++#include <netinet/ip_icmp.h> ++#include <iostream> ++ ++using namespace isc; ++using namespace isc::asiolink; ++using namespace isc::util; ++ ++namespace isc { ++namespace ping_check { ++ ++ICMPMsg::ICMPMsg() ++ : source_(IOAddress::IPV4_ZERO_ADDRESS()), ++ destination_(IOAddress::IPV4_ZERO_ADDRESS()), ++ msg_type_(0), code_(0), check_sum_(0), id_(0), sequence_(0), ++ payload_(0) { ++} ++ ++ICMPMsgPtr ++ICMPMsg::unpack(const uint8_t* wire_data, size_t length) { ++ ICMPMsgPtr msg(new ICMPMsg()); ++ if (length < sizeof(struct ip)) { ++ isc_throw(BadValue, ++ "ICMPMsg::unpack - truncated ip header, length: " ++ << length); ++ } ++ ++ // Find the IP header length... ++ struct ip* ip_header = (struct ip*)(wire_data); ++ auto hlen = (ip_header->ip_hl << 2); ++ ++ // Make sure we received enough data. ++ if (length < (hlen + sizeof(struct icmp))) { ++ isc_throw(BadValue, "ICMPMsg::truncated packet? length: " ++ << length << ", hlen: " << hlen); ++ } ++ ++ // Grab the source and destination addresses. ++ msg->setSource(IOAddress(ntohl(ip_header->ip_src.s_addr))); ++ msg->setDestination(IOAddress(ntohl(ip_header->ip_dst.s_addr))); ++ ++ // Get the message type. ++ struct icmp* reply = (struct icmp*)(wire_data + hlen); ++ msg->setType(reply->icmp_type); ++ msg->setCode(reply->icmp_code); ++ ++ msg->setChecksum(ntohs(reply->icmp_cksum)); ++ msg->setId(ntohs(reply->icmp_hun.ih_idseq.icd_id)); ++ msg->setSequence(ntohs(reply->icmp_hun.ih_idseq.icd_seq)); ++ ++ auto payload_len = length - hlen - ICMP_HEADER_SIZE; ++ msg->setPayload((const uint8_t*)(&reply->icmp_dun), payload_len); ++ ++ return (msg); ++} ++ ++ICMPPtr ++ICMPMsg::pack() const { ++ ICMPPtr outbound(new struct icmp()); ++ memset(outbound.get(), 0x00, sizeof(struct icmp)); ++ outbound->icmp_type = msg_type_; ++ outbound->icmp_id = htons(id_); ++ outbound->icmp_seq = htons(sequence_); ++ /// @todo copy in payload - not needed for ECHO REQUEST ++ outbound->icmp_cksum = htons(~calcChecksum((const uint8_t*)(outbound.get()), sizeof(struct icmp))); ++ return (outbound); ++} ++ ++void ++ICMPMsg::setPayload(const uint8_t* data, size_t length) { ++ payload_.insert(payload_.end(), data, data + length); ++} ++ ++uint32_t ++ICMPMsg::calcChecksum(const uint8_t* buf, size_t length) { ++ uint32_t sum = 0; ++ ++ /* Checksum all the pairs of bytes first... */ ++ size_t i; ++ for (i = 0; i < (length & ~1U); i += 2) { ++ sum += static_cast<uint32_t>(readUint16(buf + i, sizeof(uint16_t))); ++ /* Add carry. */ ++ if (sum > 0xFFFF) { ++ sum -= 0xFFFF; ++ } ++ } ++ ++ /* If there's a single byte left over, checksum it, too. Network ++ byte order is big-endian, so the remaining byte is the high byte. */ ++ if (i < length) { ++ sum += buf[i] << 8; ++ /* Add carry. */ ++ if (sum > 0xFFFF) { ++ sum -= 0xFFFF; ++ } ++ } ++ ++ return (sum); ++} ++ ++} // end of namespace ping_check ++} // end of namespace isc +diff --git a/src/hooks/dhcp/ping_check/icmp_msg.h b/src/hooks/dhcp/ping_check/icmp_msg.h +new file mode 100644 +index 0000000000..ace322d1ca +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/icmp_msg.h +@@ -0,0 +1,223 @@ ++// Copyright (C) 2023-2025 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 ICMP_MSG_H ++#define ICMP_MSG_H ++ ++#include <asiolink/io_address.h> ++ ++#include <arpa/inet.h> ++#include <netinet/in.h> ++#include <netinet/ip.h> ++#include <unistd.h> ++#include <netinet/ip_icmp.h> ++#include <boost/shared_ptr.hpp> ++ ++namespace isc { ++namespace ping_check { ++ ++// Forward class definition. ++class ICMPMsg; ++ ++/// @brief Shared pointer type for ICMPMsg. ++typedef boost::shared_ptr<ICMPMsg> ICMPMsgPtr; ++ ++/// @brief Shared pointer type for struct icmp. ++typedef boost::shared_ptr<struct icmp> ICMPPtr; ++ ++/// @brief Embodies an ICMP message ++/// ++/// Provides functions for marshalling of ICMP protocol ++/// messages to and from wire form ++class ICMPMsg { ++public: ++ /// @brief ICMP message types. We only define the ones ++ /// we care about. ++ enum ICMPMsgType { ++ ECHO_REPLY = 0, ++ TARGET_UNREACHABLE = 3, ++ ECHO_REQUEST = 8 ++ }; ++ ++ /// @brief Size in octets of ICMP message header. ++ /// 1 (msg type) + 1 (code) + 2 (checksum) + 4 (either unused ++ /// or used differently basing on the ICMP type and code e.g ++ /// Identifier and Sequence Number for Echo or Echo Reply Message) ++ constexpr static size_t ICMP_HEADER_SIZE = 8; ++ ++ /// @brief Constructor. ++ ICMPMsg(); ++ ++ /// @brief Destructor. ++ virtual ~ICMPMsg() = default; ++ ++ /// @brief Unpacks an ICMP message from the given wire_data ++ /// ++ /// The wire data is expected to include the IP header followed ++ /// by an ICMP message. ++ /// ++ /// @param wire_data raw data received from the socket ++ /// @param length number of bytes in the wire_data contents ++ /// ++ /// @return Pointer to the newly constructed message ++ /// @throw BadValue if the wire data is invalid ++ static ICMPMsgPtr unpack(const uint8_t* wire_data, size_t length); ++ ++ /// @brief Packs the message into an ICMP structure. ++ /// ++ /// @return Pointer to the newly constructed ICMP structure. ++ ICMPPtr pack() const; ++ ++ /// @brief Fetches the ICMP message type (e.g. ECHO_REQUEST, ECHO_REPLY) ++ /// ++ /// @return message type as a uint8_t ++ uint8_t getType() const { ++ return (msg_type_); ++ } ++ ++ /// @brief Sets the ICMP message type ++ /// ++ /// @param msg_type new value for the message type ++ void setType(uint8_t msg_type) { ++ msg_type_ = msg_type; ++ } ++ ++ /// @brief Fetches the ICMP message code ++ /// ++ /// @return uint8_t containing the message code ++ uint8_t getCode() const { ++ return (code_); ++ } ++ ++ /// @brief Sets the ICMP code ++ /// ++ /// @param code new value for the message type ++ void setCode(uint8_t code) { ++ code_ = code; ++ } ++ ++ /// @brief Fetches the checksum ++ /// ++ /// @return uint16_t containing the message checksum ++ uint16_t getChecksum() const { ++ return (check_sum_); ++ } ++ ++ /// @brief Sets the check sum ++ /// ++ /// @param check_sum new value for the check sum ++ void setChecksum(uint16_t check_sum) { ++ check_sum_ = check_sum; ++ } ++ ++ /// @brief Fetches the message id ++ /// ++ /// @return uint16_t containing the id ++ uint16_t getId() const { ++ return (id_); ++ } ++ ++ /// @brief Sets the message id ++ /// ++ /// @param id new value for the message id ++ void setId(const uint16_t id) { ++ id_ = id; ++ } ++ ++ /// @brief Fetches the message sequence number ++ /// ++ /// @return uint16_t containing the sequence number ++ uint16_t getSequence() const { ++ return (sequence_); ++ } ++ ++ /// @brief Sets the message sequence number ++ /// ++ /// @param sequence new value for the message sequence number ++ void setSequence(uint16_t sequence) { ++ sequence_ = sequence; ++ } ++ ++ /// @brief Fetches the source IP address ++ /// ++ /// @return IOAddress containing the IP address of the message source ++ const isc::asiolink::IOAddress& getSource() const { ++ return (source_); ++ } ++ ++ /// @brief Sets the source IP address ++ /// ++ /// @param source new value for the source IP address ++ void setSource(const isc::asiolink::IOAddress& source) { ++ source_ = source; ++ } ++ ++ /// @brief Fetches the destination IP address ++ /// ++ /// @return IOAddress containing the IP address of the message destination ++ const isc::asiolink::IOAddress& getDestination() const { ++ return (destination_); ++ } ++ ++ /// @brief Sets the destination IP address ++ /// ++ /// @param destination new value for the destination IP address ++ void setDestination(const isc::asiolink::IOAddress& destination) { ++ destination_ = destination; ++ } ++ ++ /// @brief Fetches the message payload ++ /// ++ /// @return vector containing the message payload ++ const std::vector<uint8_t>& getPayload() const { ++ return (payload_); ++ } ++ ++ /// @brief Sets the message payload to the given data ++ /// ++ /// @param data pointer to data buffer from which to copy ++ /// @param length number of bytes in data buffer ++ void setPayload(const uint8_t* data, size_t length); ++ ++ /// @brief Calculates the checksum of the given data buffer ++ /// ++ /// @param data pointer to data buffer from which to copy ++ /// @param length number of bytes in data buffer ++ /// ++ /// @return uint32_t containing the calculated checksum ++ static uint32_t calcChecksum(const uint8_t* data, size_t length); ++ ++private: ++ /// @brief IP address from which the message origin ++ isc::asiolink::IOAddress source_; ++ ++ /// @brief IP address of the message destination ++ isc::asiolink::IOAddress destination_; ++ ++ /// @brief ICMP message type ++ uint8_t msg_type_; ++ ++ /// @brief ICMP message code ++ uint8_t code_; ++ ++ /// @brief Checksum of the message ++ uint16_t check_sum_; ++ ++ /// @brief Message ID ++ uint16_t id_; ++ ++ /// @brief Message sequence number ++ uint16_t sequence_; ++ ++ // data beyond the ICMP header ++ std::vector<uint8_t> payload_; ++}; ++ ++ ++} // end of namespace ping_check ++} // end of namespace isc ++ ++#endif +diff --git a/src/hooks/dhcp/ping_check/icmp_socket.h b/src/hooks/dhcp/ping_check/icmp_socket.h +new file mode 100644 +index 0000000000..091057d749 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/icmp_socket.h +@@ -0,0 +1,359 @@ ++// Copyright (C) 2023-2025 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 ICMP_SOCKET_H ++#define ICMP_SOCKET_H 1 ++ ++#include <netinet/in.h> ++#include <sys/socket.h> ++#include <unistd.h> ++ ++#include <cstddef> ++ ++#include <asiolink/io_asio_socket.h> ++#include <asiolink/io_service.h> ++#include <icmp_endpoint.h> ++ ++#include <exceptions/isc_assert.h> ++ ++namespace isc { ++namespace ping_check { ++ ++/// @brief The @c ICMPSocket class is a concrete derived class of @c IOAsioSocket ++/// that represents a ICMP socket. ++/// ++/// @param C Callback type ++template <typename C> ++class ICMPSocket : public asiolink::IOAsioSocket<C> { ++private: ++ /// @brief Class is non-copyable ++ explicit ICMPSocket(const ICMPSocket&); ++ ICMPSocket& operator=(const ICMPSocket&); ++ ++public: ++ enum { ++ MIN_SIZE = 4096 // Minimum send and receive size ++ }; ++ ++ /// @brief Constructor from an ASIO ICMP socket. ++ /// ++ /// @param socket The ASIO representation of the ICMP socket. It is assumed ++ /// that the caller will open and close the socket, so these ++ /// operations are a no-op for that socket. ++ explicit ICMPSocket(boost::asio::ip::icmp::socket& socket); ++ ++ /// @brief Constructor ++ /// ++ /// Used when the ICMPSocket is being asked to manage its own internal ++ /// socket. In this case, the open() and close() methods are used. ++ /// ++ /// @param service I/O Service object used to manage the socket. ++ explicit ICMPSocket(const asiolink::IOServicePtr& service); ++ ++ /// @brief Destructor ++ virtual ~ICMPSocket(); ++ ++ /// @brief Return file descriptor of underlying socket ++ /// ++ /// @return socket's native file descriptor as an int. ++ virtual int getNative() const { ++#if BOOST_VERSION < 106600 ++ return (socket_.native()); ++#else ++ return (socket_.native_handle()); ++#endif ++ } ++ ++ /// @brief Return protocol of socket ++ /// ++ /// @return Always IPPROTO_ICMP. ++ virtual int getProtocol() const { ++ return (IPPROTO_ICMP); ++ } ++ ++ /// @brief Is "open()" synchronous? ++ /// ++ /// Indicates that the opening of a ICMP socket is synchronous. ++ /// @return Always true. ++ virtual bool isOpenSynchronous() const { ++ return true; ++ } ++ ++ /// @brief Indicates if the socket is currently open. ++ /// ++ /// @return true if socket is open. ++ virtual bool isOpen() const { ++ return isopen_; ++ } ++ ++ /// @brief Open Socket ++ /// ++ /// Opens the ICMP socket. This is a synchronous operation. ++ /// ++ /// @param endpoint Endpoint to which the socket will send data. This is ++ /// used to determine the address family that should be used for the ++ /// underlying socket. ++ /// @param callback Unused as the operation is synchronous. ++ virtual void open(const asiolink::IOEndpoint* endpoint, C& callback); ++ ++ /// @brief Send Asynchronously ++ /// ++ /// Calls the underlying socket's async_send_to() method to send a packet of ++ /// data asynchronously to the remote endpoint. The callback will be called ++ /// on completion. ++ /// ++ /// @param data Data to send ++ /// @param length Length of data to send ++ /// @param endpoint Target of the send ++ /// @param callback Callback object. ++ virtual void asyncSend(const void* data, size_t length, ++ const asiolink::IOEndpoint* endpoint, C& callback); ++ ++ /// @brief Receive Asynchronously ++ /// ++ /// Calls the underlying socket's async_receive_from() method to read a ++ /// packet of data from a remote endpoint. Arrival of the data is signalled ++ /// via a call to the callback function. ++ /// ++ /// @param data Buffer to receive incoming message ++ /// @param length Length of the data buffer ++ /// @param offset Offset into buffer where data is to be put ++ /// @param endpoint Source of the communication ++ /// @param callback Callback object ++ virtual void asyncReceive(void* data, size_t length, size_t offset, ++ asiolink::IOEndpoint* endpoint, C& callback); ++ ++ /// @brief Process received data ++ /// ++ /// See the description of IOAsioSocket::receiveComplete for a complete ++ /// description of this method. ++ /// ++ /// @param staging Pointer to the start of the staging buffer. ++ /// @param length Amount of data in the staging buffer. ++ /// @param cumulative Amount of data received before the staging buffer is ++ /// processed. ++ /// @param offset Unused. ++ /// @param expected unused. ++ /// @param outbuff Output buffer. Data in the staging buffer is be copied ++ /// to this output buffer in the call. ++ /// ++ /// @return Always true ++ virtual bool processReceivedData(const void* staging, size_t length, ++ size_t& cumulative, size_t& offset, ++ size_t& expected, ++ isc::util::OutputBufferPtr& outbuff); ++ ++ /// @brief Cancel I/O On Socket ++ virtual void cancel(); ++ ++ /// @brief Close socket ++ virtual void close(); ++ ++ /// @brief Calculates the checksum for the given buffer of data. ++ /// ++ /// @param buf pointer to the data buffer. ++ /// @param buf_size number of bytes in the data buffer. ++ /// ++ /// @return calculated checksum of the data as a uint16_t. ++ static uint16_t calcChecksum(const uint8_t* buf, const uint32_t buf_size); ++ ++private: ++ /// @brief The IO service used to handle events. ++ isc::asiolink::IOServicePtr io_service_; ++ ++ // Two variables to hold the socket - a socket and a pointer to it. This ++ // handles the case where a socket is passed to the ICMPSocket on ++ // construction, or where it is asked to manage its own socket. ++ ++ /// Pointer to own socket ++ std::unique_ptr<boost::asio::ip::icmp::socket> socket_ptr_; ++ ++ // Socket ++ boost::asio::ip::icmp::socket& socket_; ++ ++ // True when socket is open ++ bool isopen_; ++}; ++ ++// Constructor - caller manages socket ++ ++template <typename C> ++ICMPSocket<C>::ICMPSocket(boost::asio::ip::icmp::socket& socket) : ++ socket_ptr_(), socket_(socket), isopen_(true) { ++} ++ ++// Constructor - create socket on the fly ++ ++template <typename C> ++ICMPSocket<C>::ICMPSocket(const asiolink::IOServicePtr& io_service) : ++ io_service_(io_service), ++ socket_ptr_(new boost::asio::ip::icmp::socket(io_service_->getInternalIOService())), ++ socket_(*socket_ptr_), isopen_(false) { ++} ++ ++// Destructor. ++ ++template <typename C> ++ICMPSocket<C>::~ICMPSocket() { ++} ++ ++// Open the socket. ++ ++template <typename C> void ++ICMPSocket<C>::open(const asiolink::IOEndpoint* endpoint, C&) { ++ ++ // Ignore opens on already-open socket. (Don't throw a failure because ++ // of uncertainties as to what precedes when using asynchronous I/O.) ++ // It also allows us a treat a passed-in socket in exactly the same way as ++ // a self-managed socket (in that we can call the open() and close() methods ++ // of this class). ++ if (!isopen_) { ++ if (endpoint->getFamily() == AF_INET) { ++ socket_.open(boost::asio::ip::icmp::v4()); ++ } else { ++ socket_.open(boost::asio::ip::icmp::v6()); ++ } ++ isopen_ = true; ++ ++ // Ensure it can send and receive at least 4K buffers. ++ boost::asio::ip::icmp::socket::send_buffer_size snd_size; ++ socket_.get_option(snd_size); ++ if (snd_size.value() < MIN_SIZE) { ++ snd_size = MIN_SIZE; ++ socket_.set_option(snd_size); ++ } ++ ++ boost::asio::ip::icmp::socket::receive_buffer_size rcv_size; ++ socket_.get_option(rcv_size); ++ if (rcv_size.value() < MIN_SIZE) { ++ rcv_size = MIN_SIZE; ++ socket_.set_option(rcv_size); ++ } ++ ++ boost::asio::socket_base::do_not_route option(true); ++ socket_.set_option(option); ++ } ++} ++ ++// Send a message. Should never do this if the socket is not open, so throw ++// an exception if this is the case. ++ ++template <typename C> void ++ICMPSocket<C>::asyncSend(const void* data, size_t length, ++ const asiolink::IOEndpoint* endpoint, C& callback) { ++ if (isopen_) { ++ ++ // Upconvert to a ICMPEndpoint. We need to do this because although ++ // IOEndpoint is the base class of ICMPEndpoint and TCPEndpoint, it ++ // does not contain a method for getting at the underlying endpoint ++ // type - that is in the derived class and the two classes differ on ++ // return type. ++ isc_throw_assert(endpoint->getProtocol() == IPPROTO_ICMP); ++ const ICMPEndpoint* udp_endpoint = ++ static_cast<const ICMPEndpoint*>(endpoint); ++ ++ // ... and send the message. ++ socket_.async_send_to(boost::asio::buffer(data, length), ++ udp_endpoint->getASIOEndpoint(), callback); ++ } else { ++ isc_throw(asiolink::SocketNotOpen, ++ "attempt to send on a ICMP socket that is not open"); ++ } ++} ++ ++// Receive a message. Should never do this if the socket is not open, so throw ++// an exception if this is the case. ++ ++template <typename C> void ++ICMPSocket<C>::asyncReceive(void* data, size_t length, size_t offset, ++ asiolink::IOEndpoint* endpoint, C& callback) { ++ if (isopen_) { ++ ++ // Upconvert the endpoint again. ++ isc_throw_assert(endpoint->getProtocol() == IPPROTO_ICMP); ++ ICMPEndpoint* udp_endpoint = static_cast<ICMPEndpoint*>(endpoint); ++ ++ // Ensure we can write into the buffer ++ if (offset >= length) { ++ isc_throw(asiolink::BufferOverflow, "attempt to read into area beyond end of " ++ "ICMP receive buffer"); ++ } ++ void* buffer_start = static_cast<void*>(static_cast<uint8_t*>(data) + offset); ++ ++ // Issue the read ++ socket_.async_receive_from(boost::asio::buffer(buffer_start, length - offset), ++ udp_endpoint->getASIOEndpoint(), callback); ++ } else { ++ isc_throw(asiolink::SocketNotOpen, ++ "attempt to receive from a ICMP socket that is not open"); ++ } ++} ++ ++// Receive complete. Just copy the data across to the output buffer and ++// update arguments as appropriate. ++ ++template <typename C> bool ++ICMPSocket<C>::processReceivedData(const void* staging, size_t length, ++ size_t& cumulative, size_t& offset, ++ size_t& expected, ++ isc::util::OutputBufferPtr& outbuff) { ++ // Set return values to what we should expect. ++ cumulative = length; ++ expected = length; ++ offset = 0; ++ ++ // Copy data across ++ outbuff->writeData(staging, length); ++ ++ // ... and mark that we have everything. ++ return (true); ++} ++ ++// Cancel I/O on the socket. No-op if the socket is not open. ++ ++template <typename C> void ++ICMPSocket<C>::cancel() { ++ if (isopen_) { ++ socket_.cancel(); ++ } ++} ++ ++// Close the socket down. Can only do this if the socket is open and we are ++// managing it ourself. ++ ++template <typename C> void ++ICMPSocket<C>::close() { ++ if (isopen_ && socket_ptr_) { ++ socket_.close(); ++ isopen_ = false; ++ } ++} ++ ++template <typename C> uint16_t ++ICMPSocket<C>::calcChecksum(const uint8_t* buf, const uint32_t buf_size) { ++ uint32_t sum = 0; ++ uint32_t i; ++ for (i = 0; i < (buf_size & ~1U); i += 2) { ++ uint16_t chunk = buf[i] << 8 | buf[i + 1]; ++ sum += chunk; ++ if (sum > 0xFFFF) { ++ sum -= 0xFFFF; ++ } ++ } ++ // If one byte has left, we also need to add it to the checksum. ++ if (i < buf_size) { ++ sum += buf[i] << 8; ++ if (sum > 0xFFFF) { ++ sum -= 0xFFFF; ++ } ++ } ++ ++ return (sum); ++} ++ ++} // namespace ping_check ++} // namespace isc ++#endif // ICMP_SOCKET_H +diff --git a/src/hooks/dhcp/ping_check/libloadtests/.gitignore b/src/hooks/dhcp/ping_check/libloadtests/.gitignore +new file mode 100644 +index 0000000000..ada6ed5036 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/libloadtests/.gitignore +@@ -0,0 +1 @@ ++hook_load_unittests +diff --git a/src/hooks/dhcp/ping_check/libloadtests/Makefile.am b/src/hooks/dhcp/ping_check/libloadtests/Makefile.am +new file mode 100644 +index 0000000000..139a068b3c +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/libloadtests/Makefile.am +@@ -0,0 +1,60 @@ ++SUBDIRS = . ++ ++AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib ++AM_CPPFLAGS += -I$(top_builddir)/src/hooks/dhcp/ping_check -I$(top_srcdir)/src/hooks/dhcp/ping_check ++AM_CPPFLAGS += $(BOOST_INCLUDES) $(CRYPTO_CFLAGS) $(CRYPTO_INCLUDES) ++AM_CPPFLAGS += -DPING_CHECK_LIB_SO=\"$(abs_top_builddir)/src/hooks/dhcp/ping_check/.libs/libdhcp_ping_check.so\" ++AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\" ++ ++AM_CXXFLAGS = $(KEA_CXXFLAGS) ++ ++if USE_STATIC_LINK ++AM_LDFLAGS = -static ++endif ++ ++# Unit test data files need to get installed. ++EXTRA_DIST = ++ ++CLEANFILES = *.gcno *.gcda ++ ++TESTS_ENVIRONMENT = $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND) ++ ++LOG_COMPILER = $(LIBTOOL) ++AM_LOG_FLAGS = --mode=execute ++ ++TESTS = ++if HAVE_GTEST ++TESTS += hook_load_unittests ++ ++hook_load_unittests_SOURCES = run_unittests.cc ++hook_load_unittests_SOURCES += load_unload_unittests.cc ++ ++hook_load_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) $(LOG4CPLUS_INCLUDES) ++ ++hook_load_unittests_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS) $(GTEST_LDFLAGS) ++ ++hook_load_unittests_CXXFLAGS = $(AM_CXXFLAGS) ++ ++hook_load_unittests_LDADD = $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la ++hook_load_unittests_LDADD += $(top_builddir)/src/lib/process/libkea-process.la ++hook_load_unittests_LDADD += $(top_builddir)/src/lib/eval/libkea-eval.la ++hook_load_unittests_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la ++hook_load_unittests_LDADD += $(top_builddir)/src/lib/stats/libkea-stats.la ++hook_load_unittests_LDADD += $(top_builddir)/src/lib/config/libkea-cfgclient.la ++hook_load_unittests_LDADD += $(top_builddir)/src/lib/http/libkea-http.la ++hook_load_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la ++hook_load_unittests_LDADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la ++hook_load_unittests_LDADD += $(top_builddir)/src/lib/database/libkea-database.la ++hook_load_unittests_LDADD += $(top_builddir)/src/lib/cc/libkea-cc.la ++hook_load_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la ++hook_load_unittests_LDADD += $(top_builddir)/src/lib/dns/libkea-dns++.la ++hook_load_unittests_LDADD += $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la ++hook_load_unittests_LDADD += $(top_builddir)/src/lib/log/libkea-log.la ++hook_load_unittests_LDADD += $(top_builddir)/src/lib/util/libkea-util.la ++hook_load_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la ++hook_load_unittests_LDADD += $(LOG4CPLUS_LIBS) ++hook_load_unittests_LDADD += $(CRYPTO_LIBS) ++hook_load_unittests_LDADD += $(BOOST_LIBS) ++hook_load_unittests_LDADD += $(GTEST_LDADD) ++endif ++noinst_PROGRAMS = $(TESTS) +diff --git a/src/hooks/dhcp/ping_check/libloadtests/load_unload_unittests.cc b/src/hooks/dhcp/ping_check/libloadtests/load_unload_unittests.cc +new file mode 100644 +index 0000000000..67275db617 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/libloadtests/load_unload_unittests.cc +@@ -0,0 +1,107 @@ ++// Copyright (C) 2023-2025 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 This file contains tests which exercise the load and unload ++/// functions in the ddns tuning hook library. In order to test the load ++/// function, one must be able to pass it hook library parameters. The ++/// the only way to populate these parameters is by actually loading the ++/// library via HooksManager::loadLibraries(). ++ ++#include <config.h> ++ ++#include <dhcpsrv/testutils/lib_load_test_fixture.h> ++#include <testutils/gtest_utils.h> ++ ++#include <gtest/gtest.h> ++#include <errno.h> ++ ++using namespace std; ++using namespace isc; ++using namespace isc::hooks; ++using namespace isc::data; ++using namespace isc::dhcp; ++using namespace isc::process; ++ ++namespace { ++ ++/// @brief Test fixture for testing loading and unloading the ddns tuning library ++class PingCheckLibLoadTest : public isc::test::LibLoadTest { ++public: ++ /// @brief Constructor ++ PingCheckLibLoadTest() : LibLoadTest(PING_CHECK_LIB_SO) { ++ } ++ ++ /// @brief Destructor ++ virtual ~PingCheckLibLoadTest() { ++ } ++ ++ /// @brief Registers hooks in the hook manager. ++ /// Normally this is done by the server core code (@c Dhcpv4Srv). ++ void registerHooks() { ++ hook_index_dhcp4_srv_configured_ = HooksManager::registerHook("dhcp4_srv_configured"); ++ hook_index_lease4_offer_ = HooksManager::registerHook("lease4_offer"); ++ } ++ ++ /// @brief Checks that expected callouts are present. ++ void calloutsPresent() { ++ bool result; ++ ASSERT_NO_THROW_LOG(result = HooksManager::calloutsPresent(hook_index_dhcp4_srv_configured_)); ++ EXPECT_TRUE(result); ++ ASSERT_NO_THROW_LOG(result = HooksManager::calloutsPresent(hook_index_lease4_offer_)); ++ EXPECT_TRUE(result); ++ } ++ ++ /// @brief Creates a valid set of ping-check hook parameters. ++ virtual ElementPtr validConfigParams() { ++ ElementPtr params = Element::createMap(); ++ params->set("min-ping-requests", Element::create(3)); ++ params->set("reply-timeout", Element::create(100)); ++ params->set("enable-ping-check", Element::create(true)); ++ params->set("ping-cltt-secs", Element::create(60)); ++ params->set("ping-channel-threads", Element::create(1)); ++ return (params); ++ } ++ ++ /// @brief Hook index values. ++ int hook_index_dhcp4_srv_configured_; ++ int hook_index_lease4_offer_; ++}; ++ ++// Simple V4 test that checks the library can be loaded and unloaded several times. ++TEST_F(PingCheckLibLoadTest, validLoad4) { ++ validDaemonTest("kea-dhcp4", AF_INET, valid_params_); ++} ++ ++// Simple test that checks the library cannot be loaded by invalid daemons. ++TEST_F(PingCheckLibLoadTest, invalidDaemonLoad) { ++ // V6 is invalid regardless of family. ++ invalidDaemonTest("kea-dhcp6", AF_INET, valid_params_); ++ invalidDaemonTest("kea-dhcp6", AF_INET6, valid_params_); ++ ++ invalidDaemonTest("kea-ctrl-agent", AF_INET, valid_params_); ++ invalidDaemonTest("kea-dhcp-ddns", AF_INET, valid_params_); ++ invalidDaemonTest("bogus", AF_INET, valid_params_); ++} ++ ++// Verifies that callout functions exist after loading the library. ++TEST_F(PingCheckLibLoadTest, verifyCallouts) { ++ // Set family and daemon's proc name and register hook points. ++ isc::dhcp::CfgMgr::instance().setFamily(AF_INET); ++ isc::process::Daemon::setProcName("kea-dhcp4"); ++ registerHooks(); ++ ++ // Add library to config and load it. ++ ASSERT_NO_THROW_LOG(addLibrary(lib_so_name_, valid_params_)); ++ ASSERT_NO_THROW_LOG(loadLibraries()); ++ ++ // Verify that expected callouts are present. ++ calloutsPresent(); ++ ++ // Unload the library. ++ ASSERT_NO_THROW_LOG(unloadLibraries()); ++} ++ ++} // end of anonymous namespace +diff --git a/src/hooks/dhcp/ping_check/libloadtests/meson.build b/src/hooks/dhcp/ping_check/libloadtests/meson.build +new file mode 100644 +index 0000000000..da8bf439c0 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/libloadtests/meson.build +@@ -0,0 +1,21 @@ ++if not TESTS_OPT.enabled() ++ subdir_done() ++endif ++ ++dhcp_ping_check_libloadtests = executable( ++ 'dhcp-ping-check-libload-tests', ++ 'load_unload_unittests.cc', ++ 'run_unittests.cc', ++ cpp_args: [ ++ f'-DPING_CHECK_LIB_SO="@TOP_BUILD_DIR@/src/hooks/dhcp/ping_check/libdhcp_ping_check.so"', ++ ], ++ dependencies: [GTEST_DEP, CRYPTO_DEP], ++ include_directories: [include_directories('.')] + INCLUDES, ++ link_with: LIBS_BUILT_SO_FAR, ++) ++test( ++ 'dhcp-ping-check-libloadtests', ++ dhcp_ping_check_libloadtests, ++ depends: [dhcp_ping_check_lib], ++ protocol: 'gtest', ++) +diff --git a/src/hooks/dhcp/ping_check/libloadtests/run_unittests.cc b/src/hooks/dhcp/ping_check/libloadtests/run_unittests.cc +new file mode 100644 +index 0000000000..d249e2362e +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/libloadtests/run_unittests.cc +@@ -0,0 +1,19 @@ ++// Copyright (C) 2023-2025 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 <gtest/gtest.h> ++ ++int ++main(int argc, char* argv[]) { ++ ::testing::InitGoogleTest(&argc, argv); ++ isc::log::initLogger(); ++ int result = RUN_ALL_TESTS(); ++ ++ return (result); ++} +diff --git a/src/hooks/dhcp/ping_check/meson.build b/src/hooks/dhcp/ping_check/meson.build +new file mode 100644 +index 0000000000..d3a1e70b49 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/meson.build +@@ -0,0 +1,41 @@ ++dhcp_ping_check_lib = shared_library( ++ 'dhcp_ping_check', ++ 'config_cache.cc', ++ 'icmp_msg.cc', ++ 'ping_channel.cc', ++ 'ping_check_callouts.cc', ++ 'ping_check_config.cc', ++ 'ping_check_log.cc', ++ 'ping_check_messages.cc', ++ 'ping_check_mgr.cc', ++ 'ping_context.cc', ++ 'ping_context_store.cc', ++ 'version.cc', ++ dependencies: [CRYPTO_DEP], ++ include_directories: [include_directories('.')] + INCLUDES, ++ install: true, ++ install_dir: HOOKS_PATH, ++ install_rpath: INSTALL_RPATH, ++ build_rpath: BUILD_RPATH, ++ link_with: LIBS_BUILT_SO_FAR, ++ name_suffix: 'so', ++) ++dhcp_ping_check_archive = static_library( ++ 'dhcp_ping_check', ++ objects: dhcp_ping_check_lib.extract_all_objects(recursive: false), ++) ++subdir('libloadtests') ++subdir('tests') ++ ++if KEA_MSG_COMPILER.found() ++ target_gen_messages = run_target( ++ 'src-hooks-dhcp-ping_check-ping_check_messages', ++ command: [ ++ CD_AND_RUN, ++ TOP_SOURCE_DIR, ++ KEA_MSG_COMPILER, ++ 'src/hooks/dhcp/ping_check/ping_check_messages.mes', ++ ], ++ ) ++ TARGETS_GEN_MESSAGES += [target_gen_messages] ++endif +diff --git a/src/hooks/dhcp/ping_check/ping_channel.cc b/src/hooks/dhcp/ping_check/ping_channel.cc +new file mode 100644 +index 0000000000..6a6a88c038 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/ping_channel.cc +@@ -0,0 +1,466 @@ ++// Copyright (C) 2023-2025 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 <ping_channel.h> ++#include <ping_check_log.h> ++#include <dhcp/iface_mgr.h> ++#include <exceptions/exceptions.h> ++#include <util/multi_threading_mgr.h> ++#include <iostream> ++ ++using namespace isc; ++using namespace isc::asiolink; ++using namespace isc::dhcp; ++using namespace isc::util; ++ ++namespace ph = std::placeholders; ++ ++namespace isc { ++namespace ping_check { ++ ++uint32_t ++PingChannel::nextEchoInstanceNum() { ++ static uint32_t echo_instance_num = 0x00010000; ++ if (echo_instance_num == UINT32_MAX) { ++ echo_instance_num = 0x00010001; ++ } else { ++ ++echo_instance_num; ++ } ++ ++ return (echo_instance_num); ++} ++ ++PingChannel::PingChannel(IOServicePtr& io_service, ++ NextToSendCallback next_to_send_cb, ++ EchoSentCallback echo_sent_cb, ++ ReplyReceivedCallback reply_received_cb, ++ ShutdownCallback shutdown_cb) ++ : io_service_(io_service), ++ next_to_send_cb_(next_to_send_cb), ++ echo_sent_cb_(echo_sent_cb), ++ reply_received_cb_(reply_received_cb), ++ shutdown_cb_(shutdown_cb), ++ socket_(0), input_buf_(256), ++ reading_(false), sending_(false), stopping_(false), mutex_(new std::mutex), ++ single_threaded_(!MultiThreadingMgr::instance().getMode()), ++ watch_socket_(0), registered_write_fd_(-1), registered_read_fd_(-1) { ++ if (!io_service_) { ++ isc_throw(BadValue, ++ "PingChannel ctor - io_service cannot be empty"); ++ } ++} ++ ++PingChannel::~PingChannel() { ++ close(); ++} ++ ++void ++PingChannel::open() { ++ try { ++ MultiThreadingLock lock(*mutex_); ++ if (socket_ && socket_->isOpen()) { ++ return; ++ } ++ ++ // For open(), the endpoint is only used to determine protocol, ++ // the address is irrelevant. ++ ICMPEndpoint ping_to_endpoint(IOAddress::IPV4_ZERO_ADDRESS()); ++ SocketCallback socket_cb( ++ [](boost::system::error_code ec, size_t /*length */) { ++ isc_throw(Unexpected, "ICMPSocket open is synchronous, should not invoke cb: " ++ << ec.message()); ++ } ++ ); ++ ++ socket_.reset(new PingSocket(io_service_)); ++ socket_->open(&ping_to_endpoint, socket_cb); ++ reading_ = false; ++ sending_ = false; ++ stopping_ = false; ++ ++ if (single_threaded_) { ++ // Open new watch socket. ++ watch_socket_.reset(new util::WatchSocket()); ++ ++ // Register the WatchSocket with IfaceMgr to signal data ready to write. ++ registered_write_fd_ = watch_socket_->getSelectFd(); ++ IfaceMgr::instance().addExternalSocket(registered_write_fd_, IfaceMgr::SocketCallback()); ++ ++ // Register ICMPSocket with IfaceMgr to signal data ready to read. ++ registered_read_fd_ = socket_->getNative(); ++ IfaceMgr::instance().addExternalSocket(registered_read_fd_, IfaceMgr::SocketCallback()); ++ } ++ ++ } catch (const std::exception& ex) { ++ isc_throw(Unexpected, "PingChannel::open failed:" << ex.what()); ++ } ++ ++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC, PING_CHECK_CHANNEL_SOCKET_OPENED); ++} ++ ++bool ++PingChannel::isOpen() const { ++ MultiThreadingLock lock(*mutex_); ++ return (socket_ && socket_->isOpen()); ++} ++ ++void ++PingChannel::close() { ++ try { ++ MultiThreadingLock lock(*mutex_); ++ ++ if (single_threaded_) { ++ // Unregister from IfaceMgr. ++ if (registered_write_fd_ != -1) { ++ IfaceMgr::instance().deleteExternalSocket(registered_write_fd_); ++ registered_write_fd_ = -1; ++ } ++ ++ if (registered_read_fd_ != -1) { ++ IfaceMgr::instance().deleteExternalSocket(registered_read_fd_); ++ registered_read_fd_ = -1; ++ } ++ ++ // Close watch socket. ++ if (watch_socket_) { ++ std::string error_string; ++ watch_socket_->closeSocket(error_string); ++ if (!error_string.empty()) { ++ LOG_ERROR(ping_check_logger, PING_CHECK_CHANNEL_WATCH_SOCKET_CLOSE_ERROR) ++ .arg(error_string); ++ } ++ ++ watch_socket_.reset(); ++ } ++ } ++ ++ if (!socket_ || !socket_->isOpen()) { ++ return; ++ } ++ ++ socket_->close(); ++ } catch (const std::exception& ex) { ++ // On close error, log but do not throw. ++ LOG_ERROR(ping_check_logger, PING_CHECK_CHANNEL_SOCKET_CLOSE_ERROR) ++ .arg(ex.what()); ++ } ++ ++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC, PING_CHECK_CHANNEL_SOCKET_CLOSED); ++} ++ ++void ++PingChannel::stopChannel() { ++ { ++ MultiThreadingLock lock(*mutex_); ++ if (stopping_) { ++ return; ++ } ++ ++ stopping_ = true; ++ } ++ ++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC, PING_CHECK_CHANNEL_STOP); ++ close(); ++ ++ if (shutdown_cb_) { ++ (shutdown_cb_)(); ++ } ++} ++ ++void ++PingChannel::asyncReceive(void* data, size_t length, size_t offset, ++ asiolink::IOEndpoint* endpoint, SocketCallback& callback) { ++ socket_->asyncReceive(data, length, offset, endpoint, callback); ++} ++ ++void ++PingChannel::asyncSend(void* data, size_t length, asiolink::IOEndpoint* endpoint, ++ SocketCallback& callback) { ++ socket_->asyncSend(data, length, endpoint, callback); ++ ++ if (single_threaded_) { ++ // Set IO ready marker so sender activity is visible to select() or poll(). ++ watch_socket_->markReady(); ++ } ++} ++ ++void ++PingChannel::doRead() { ++ try { ++ MultiThreadingLock lock(*mutex_); ++ if (!canRead()) { ++ return; ++ } ++ ++ reading_ = true; ++ ++ // Create instance of the callback. It is safe to pass the ++ // local instance of the callback, because the underlying ++ // std functions make copies as needed. ++ SocketCallback cb(std::bind(&PingChannel::socketReadCallback, ++ shared_from_this(), ++ ph::_1, // error ++ ph::_2)); // bytes_transferred ++ asyncReceive(static_cast<void*>(getInputBufData()), getInputBufSize(), ++ 0, &reply_endpoint_, cb); ++ } catch (const std::exception& ex) { ++ // Normal IO failures should be passed to the callback. A failure here ++ // indicates the call to asyncReceive() itself failed. ++ LOG_ERROR(ping_check_logger, PING_CHECK_UNEXPECTED_READ_ERROR) ++ .arg(ex.what()); ++ stopChannel(); ++ } ++} ++ ++void ++PingChannel::socketReadCallback(boost::system::error_code ec, size_t length) { ++ { ++ MultiThreadingLock lock(*mutex_); ++ if (stopping_) { ++ return; ++ } ++ } ++ ++ if (ec) { ++ if (ec.value() == boost::asio::error::operation_aborted) { ++ // IO service has been stopped and the connection is probably ++ // going to be shutting down. ++ return; ++ } else if ((ec.value() == boost::asio::error::try_again) || ++ (ec.value() == boost::asio::error::would_block)) { ++ // We got EWOULDBLOCK or EAGAIN which indicates that we may be able to ++ // read something from the socket on the next attempt. Just make sure ++ // we don't try to read anything now in case there is any garbage ++ // passed in length. ++ length = 0; ++ } else { ++ // Anything else is fatal for the socket. ++ LOG_ERROR(ping_check_logger, PING_CHECK_CHANNEL_SOCKET_READ_FAILED) ++ .arg(ec.message()); ++ stopChannel(); ++ return; ++ } ++ } ++ ++ // Unpack the reply and pass it to the reply callback. ++ ICMPMsgPtr reply; ++ if (length > 0) { ++ { ++ try { ++ MultiThreadingLock lock(*mutex_); ++ reply = ICMPMsg::unpack(getInputBufData(), getInputBufSize()); ++ if (reply->getType() == ICMPMsg::ECHO_REPLY) { ++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_DETAIL, ++ PING_CHECK_CHANNEL_ECHO_REPLY_RECEIVED) ++ .arg(reply->getSource()) ++ .arg(reply->getId()) ++ .arg(reply->getSequence()); ++ } ++ } catch (const std::exception& ex) { ++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC, ++ PING_CHECK_CHANNEL_MALFORMED_PACKET_RECEIVED) ++ .arg(ex.what()); ++ } ++ } ++ } ++ ++ { ++ MultiThreadingLock lock(*mutex_); ++ reading_ = false; ++ } ++ ++ if (reply) { ++ (reply_received_cb_)(reply); ++ } ++ ++ // Start the next read. ++ doRead(); ++} ++ ++void ++PingChannel::startSend() { ++ MultiThreadingLock lock(*mutex_); ++ if (canSend()) { ++ // Post the call to sendNext to the IOService. ++ // This ensures its carried out on a thread ++ // associated with the channel's IOService ++ // not the thread invoking this function. ++ auto f = [](PingChannelPtr ptr) { ptr->sendNext(); }; ++ io_service_->post(std::bind(f, shared_from_this())); ++ } ++} ++ ++void ++PingChannel::startRead() { ++ MultiThreadingLock lock(*mutex_); ++ if (canRead()) { ++ // Post the call to doRead to the IOService. ++ // This ensures its carried out on a thread ++ // associated with the channel's IOService ++ // not the thread invoking this function. ++ auto f = [](PingChannelPtr ptr) { ptr->doRead(); }; ++ io_service_->post(std::bind(f, shared_from_this())); ++ } ++} ++ ++void ++PingChannel::sendNext() { ++ try { ++ MultiThreadingLock lock(*mutex_); ++ if (!canSend()) { ++ // Can't send right now, get out. ++ return; ++ } ++ ++ // Fetch the next one to send. ++ IOAddress target("0.0.0.0"); ++ if (!((next_to_send_cb_)(target))) { ++ // Nothing to send. ++ return; ++ } ++ ++ // Have an target IP, build an ECHO REQUEST for it. ++ sending_ = true; ++ ICMPMsgPtr next_echo(new ICMPMsg()); ++ next_echo->setType(ICMPMsg::ECHO_REQUEST); ++ next_echo->setDestination(target); ++ ++ uint32_t instance_num = nextEchoInstanceNum(); ++ next_echo->setId(static_cast<uint16_t>(instance_num >> 16)); ++ next_echo->setSequence(static_cast<uint16_t>(instance_num & 0x0000FFFF)); ++ ++ // Get packed wire-form. ++ ICMPPtr echo_icmp = next_echo->pack(); ++ ++ // Create instance of the callback. It is safe to pass the ++ // local instance of the callback, because the underlying ++ // std functions make copies as needed. ++ SocketCallback cb(std::bind(&PingChannel::socketWriteCallback, ++ shared_from_this(), ++ next_echo, ++ ph::_1, // error ++ ph::_2)); // bytes_transferred ++ ++ ICMPEndpoint target_endpoint(target); ++ asyncSend(echo_icmp.get(), sizeof(struct icmp), &target_endpoint, cb); ++ } catch (const std::exception& ex) { ++ // Normal IO failures should be passed to the callback. A failure here ++ // indicates the call to asyncSend() itself failed. ++ LOG_ERROR(ping_check_logger, PING_CHECK_UNEXPECTED_WRITE_ERROR) ++ .arg(ex.what()); ++ stopChannel(); ++ return; ++ } ++} ++ ++void ++PingChannel::socketWriteCallback(ICMPMsgPtr echo, boost::system::error_code ec, ++ size_t length) { ++ { ++ MultiThreadingLock lock(*mutex_); ++ if (stopping_) { ++ return; ++ } ++ } ++ ++ if (single_threaded_) { ++ try { ++ // Clear the IO ready marker. ++ watch_socket_->clearReady(); ++ } catch (const std::exception& ex) { ++ // This can only happen if the WatchSocket's select_fd has been ++ // compromised which is a programmatic error. We'll log the error ++ // here, then continue on and process the IO result we were given. ++ // WatchSocket issue will resurface on the next send as a closed ++ // fd in markReady() rather than fail out of this callback. ++ LOG_ERROR(ping_check_logger, PING_CHECK_CHANNEL_WATCH_SOCKET_CLEAR_ERROR) ++ .arg(ex.what()); ++ } ++ } ++ ++ // Handle an error. Note we can't use a case statement as some values ++ // on some OSes are the same (e.g. try_again and would_block) which causes ++ // duplicate case compilation errors. ++ bool send_failed = false; ++ if (ec) { ++ auto error_value = ec.value(); ++ if (error_value == boost::asio::error::operation_aborted) { ++ // IO service has been stopped and the connection is probably ++ // going to be shutting down. ++ return; ++ } else if ((error_value == boost::asio::error::try_again) || ++ (error_value == boost::asio::error::would_block)) { ++ // We got EWOULDBLOCK or EAGAIN which indicates that we may be able to ++ // write something from the socket on the next attempt. Set the length ++ // to zero so we skip the completion callback. ++ length = 0; ++ } else if ((error_value == boost::asio::error::network_unreachable) || ++ (error_value == boost::asio::error::host_unreachable) || ++ (error_value == boost::asio::error::network_down)) { ++ // One of these implies an interface might be down, or there's no ++ // way to ping this network. Other networks might be working OK. ++ send_failed = true; ++ } else if (error_value == boost::asio::error::no_buffer_space) { ++ // Writing faster than the kernel will write them out. ++ send_failed = true; ++ } else if (error_value == boost::asio::error::access_denied) { ++ // Means the address we tried to ping is not allowed. Most likey a broadcast ++ // address. ++ send_failed = true; ++ } else { ++ // Anything else is fatal for the socket. ++ LOG_ERROR(ping_check_logger, PING_CHECK_CHANNEL_SOCKET_WRITE_FAILED) ++ .arg(ec.message()); ++ stopChannel(); ++ return; ++ } ++ } ++ ++ { ++ MultiThreadingLock lock(*mutex_); ++ sending_ = false; ++ } ++ ++ if (send_failed) { ++ // Invoke the callback with send failed. This instructs the manager ++ // to treat the address as free to use. ++ LOG_ERROR(ping_check_logger, PING_CHECK_CHANNEL_NETWORK_WRITE_ERROR) ++ .arg(echo->getDestination()) ++ .arg(ec.message()); ++ // Invoke the send completed callback. ++ (echo_sent_cb_)(echo, true); ++ } else if (length > 0) { ++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_DETAIL, ++ PING_CHECK_CHANNEL_ECHO_REQUEST_SENT) ++ .arg(echo->getDestination()) ++ .arg(echo->getId()) ++ .arg(echo->getSequence()); ++ // Invoke the send completed callback. ++ (echo_sent_cb_)(echo, false); ++ } ++ ++ // Schedule the next send. ++ sendNext(); ++} ++ ++size_t ++PingChannel::getInputBufSize() const { ++ return (input_buf_.size()); ++} ++ ++unsigned char* ++PingChannel::getInputBufData() { ++ if (input_buf_.empty()) { ++ isc_throw(InvalidOperation, ++ "PingChannel::getInputBufData() - cannot access empty buffer"); ++ } ++ ++ return (input_buf_.data()); ++} ++ ++} // end of namespace ping_check ++} // end of namespace isc +diff --git a/src/hooks/dhcp/ping_check/ping_channel.h b/src/hooks/dhcp/ping_check/ping_channel.h +new file mode 100644 +index 0000000000..ad798188e3 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/ping_channel.h +@@ -0,0 +1,371 @@ ++// Copyright (C) 2023-2025 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 PING_CHANNEL_H ++#define PING_CHANNEL_H ++ ++#include <asiolink/asio_wrapper.h> ++#include <asiolink/io_address.h> ++#include <asiolink/io_service.h> ++#include <util/watch_socket.h> ++#include <icmp_msg.h> ++#include <icmp_socket.h> ++ ++#include <boost/scoped_ptr.hpp> ++#include <boost/enable_shared_from_this.hpp> ++ ++#include <iostream> ++#include <mutex> ++ ++namespace isc { ++namespace ping_check { ++ ++/// @brief Type of the function implementing a callback invoked by the ++/// @c SocketCallback functor. ++typedef std::function<void(boost::system::error_code ec, size_t length)> SocketCallbackFunction; ++ ++/// @brief Functor associated with the socket object. ++/// ++/// This functor calls a callback function specified in the constructor. ++class SocketCallback { ++public: ++ /// @brief Constructor. ++ /// ++ /// @param socket_callback Callback to be invoked by the functor upon ++ /// an event associated with the socket. ++ explicit inline SocketCallback(SocketCallbackFunction socket_callback) ++ : callback_(socket_callback) { ++ }; ++ ++ /// @brief Operator called when event associated with a socket occurs. ++ /// ++ /// This operator returns immediately when received @c boost::system::error_code ++ /// is equal to @c boost::asio::error::operation_aborted. ++ /// ++ /// @param ec Error code. ++ /// @param length Data length. ++ inline void operator()(boost::system::error_code ec, size_t length = 0) { ++ if (ec.value() == boost::asio::error::operation_aborted) { ++ return; ++ } ++ ++ callback_(ec, length); ++ }; ++ ++private: ++ /// @brief Supplied callback. ++ SocketCallbackFunction callback_; ++}; ++ ++/// @brief Socket type for performing ICMP socket IO. ++typedef ICMPSocket<SocketCallback> PingSocket; ++ ++/// @brief Defines a pointer to PingSocket. ++typedef boost::shared_ptr<PingSocket> PingSocketPtr; ++ ++/// @brief Function type for callback that fetches next IOAddress to ping. ++typedef std::function<bool(asiolink::IOAddress& target)> NextToSendCallback; ++ ++/// @brief Function type for callback to invoke upon ECHO send completion. ++typedef std::function<void(ICMPMsgPtr& echo, bool send_failed)> EchoSentCallback; ++ ++/// @brief Function type for callback to invoke when an ICMP reply has been ++/// received. ++typedef std::function<void(ICMPMsgPtr& reply)> ReplyReceivedCallback; ++ ++/// @brief Function type for callback to invoke when the channel has shutdown. ++typedef std::function<void()> ShutdownCallback; ++ ++/// @brief Provides thread-safe ICMP ECHO REQUEST/ECHO REPLY service ++/// ++/// PingChannel uses a @ref PingSocket to send out ECHO REQUESTs and ++/// receive ICMP replies. It is thread-safe and can be driven either ++/// with a single-threaded IOService or a multi-threaded ++/// IOServiceThreadPool. It uses series of callbacks to perpetually ++/// send requests to target addresses and feed back replies received: ++/// ++/// -# next_to_send_cb_ - callback to invoke to fetch the next address to ping ++/// -# echo_sent_cb_ - callback to invoke when an ECHO REQUEST has been sent out ++/// -# reply_received_cb_ - callback to invoke when an ICMP reply has been received. ++/// -# channel_shutdown_cb_ - callback to invoke when the channel has shutdown ++/// ++/// Callback handlers are supplied via the PingChannel constructor. Higher order ++/// functions are provided, that once instantiated, can be used by calling layers ++/// to control the channel (e.g. open the channel, initiate reading, initiate ++/// writing, and close the channel). ++/// ++/// @note Callbacks handlers must be thread-safe if the channel is ++/// driven by an IOServiceThreadPool. ++/// ++class PingChannel : public boost::enable_shared_from_this<PingChannel> { ++public: ++ /// @brief Constructor ++ /// ++ /// Instantiates the channel with its socket closed. ++ /// ++ /// @param io_service pointer to the IOService instance that will manage ++ /// the channel's IO. Must not be empty ++ /// @param next_to_send_cb callback to invoke to fetch the next IOAddress ++ /// to ping ++ /// @param echo_sent_cb callback to invoke when an ECHO send has completed ++ /// @param reply_received_cb callback to invoke when an ICMP reply has been ++ /// received. This callback is passed all inbound ICMP messages (e.g. ECHO ++ /// REPLY, UNREACHABLE, etc...) ++ /// @param shutdown_cb callback to invoke when the channel has shutdown due ++ /// to an error ++ /// ++ /// @throw BadValue if io_service is empty. ++ PingChannel(asiolink::IOServicePtr& io_service, ++ NextToSendCallback next_to_send_cb, ++ EchoSentCallback echo_sent_cb, ++ ReplyReceivedCallback reply_received_cb, ++ ShutdownCallback shutdown_cb = ShutdownCallback()); ++ ++ /// @brief Destructor ++ /// ++ /// Closes the socket if its open. ++ virtual ~PingChannel(); ++ ++ /// @brief Opens the socket for communications ++ /// ++ /// (Re)Creates the @ref PingSocket instance and opens it. ++ /// ++ /// @throw Unexpected if the open fails. ++ void open(); ++ ++ /// @brief Indicates whether or not the channel socket is open. ++ /// ++ /// @return true if the socket is open. ++ bool isOpen() const; ++ ++ // @brief Schedules the next send. ++ // ++ // If the socket is not currently sending it posts a call to @c sendNext() ++ // to the channel's IOService. ++ virtual void startSend(); ++ ++ // @brief Schedules the next read. ++ // ++ // If the socket is not currently reading it posts a call to @c doRead() ++ // to the channel's IOService. ++ void startRead(); ++ ++ /// @brief Closes the channel's socket. ++ void close(); ++ ++ /// @brief Fetches the channel's IOService ++ /// ++ /// @return pointer to the IOService. ++ asiolink::IOServicePtr getIOService() { ++ return (io_service_); ++ } ++ ++protected: ++ /// @brief Receive data on the socket asynchronously ++ /// ++ /// Calls the underlying socket's asyncReceive() method to read a ++ /// packet of data from a remote endpoint. Arrival of the data is signalled ++ /// via a call to the callback function. ++ /// ++ /// This virtual function is provided as means to inject errors during ++ /// read operations to facilitate testing. ++ /// ++ /// @param data buffer to receive incoming message ++ /// @param length length of the data buffer ++ /// @param offset offset into buffer where data is to be put ++ /// @param endpoint source of the communication ++ /// @param callback callback object ++ virtual void asyncReceive(void* data, size_t length, size_t offset, ++ asiolink::IOEndpoint* endpoint, SocketCallback& callback); ++ ++ /// @brief Send data on the socket asynchronously ++ /// ++ /// Calls the underlying socket's asyncSend() method to send a ++ /// packet of data from a remote endpoint. Arrival of the data is signalled ++ /// via a call to the callback function. ++ /// ++ /// This virtual function is provided as means to inject errors during ++ /// write operations to facilitate testing. ++ /// ++ /// @param data buffer containing the data to send ++ /// @param length length of the data buffer ++ /// @param endpoint destination of the communication ++ /// @param callback callback object ++ virtual void asyncSend(void* data, size_t length, asiolink::IOEndpoint* endpoint, ++ SocketCallback& callback); ++ ++protected: ++ /// @brief Initiates an asynchronous socket read. ++ /// ++ /// If the channel is able to read (is open, not stopping and not ++ /// currently reading) it invokes @ref PingSocket::asyncReceive() ++ /// otherwise it simply returns. If the call to asyncReceive() fails ++ /// it calls @c stopChannel() otherwise, when it completes it will ++ /// invoke @c socketReadCallback(). ++ void doRead(); ++ ++ /// @brief Socket read completion callback ++ /// ++ /// Invoked when PingSocket::asyncRead() completes. ++ /// Upon read success and data received: ++ /// ++ /// -# Unpacks the wire data ++ /// -# Pass the resultant ICMPMsg to reply received callback ++ /// -# start next read ++ /// ++ /// On error conditions: ++ /// ++ /// -# Operation aborted: socket is shutting down, simply return ++ /// -# Operation would block/try again: start a new read ++ /// -# Any other error, shut down the channel ++ /// ++ /// @param ec error code indicating either success or the error encountered ++ /// @param length number of bytes read ++ void socketReadCallback(boost::system::error_code ec, size_t length); ++ ++ /// @brief Initiates sending the next ECHO REQUEST ++ /// ++ /// If the channel is able to send (i.e is open, not stopping and not ++ /// currently writing): ++ /// -# Invoke next to send callback to fetch the next target IP address ++ /// -# If there is no next target, return ++ /// -# Construct the ECHO REQUEST for the target and pack it into wire form ++ /// -# Begin sending the request by passing to @c PingSocket::asyncSend() ++ /// -# If the asyncSend() call fails shutdown the channel, otherwise when ++ /// it completes it invokes @c socketWriteCallback(). ++ virtual void sendNext(); ++ ++ /// @brief Socket write completion callback ++ /// ++ /// Invoked when PingSocket::asyncWrite() completes. ++ /// Upon write success: ++ /// ++ /// -# Pass the ECHO REQUEST (i.e. echo_sent) to echo sent callback ++ /// -# start next write ++ /// ++ /// On error conditions: ++ /// ++ /// -# Operation aborted: socket is shutting down, simply return ++ /// -# Operation would block/try again: start a new write ++ /// -# Any other error, shut down the channel ++ /// ++ /// @param echo_sent ECHO REQUEST that was written (or attempted to be ++ /// written) ++ /// @param ec error code indicating either success or the error encountered ++ /// @param length number of bytes written ++ void socketWriteCallback(ICMPMsgPtr echo_sent, boost::system::error_code ec, ++ size_t length); ++ ++ /// @brief Closes the socket channel and invokes the shutdown callback. ++ /// ++ /// This function is invoked to notify the calling layer that the socket ++ /// has encountered an unrecoverable error and is stopping operations. ++ void stopChannel(); ++ ++ /// @brief returns the next unique ECHO instance number. ++ /// ++ /// This method generates and returns the next ECHO instance ++ /// number by incrementing the current value. It is a strictly ++ /// monotonously increasing value beginning at 0x00010001. ++ /// At roll over it resets to 0x00010001. ++ /// ++ /// Must be called in a thread-safe context ++ /// ++ /// @return the next unique instance number. ++ static uint32_t nextEchoInstanceNum(); ++ ++ /// @brief Indicates whether or not a send can be initiated. ++ /// ++ /// Must be called in a thread-safe context ++ /// ++ /// @return True if the socket is open, is not attempting to stop, and is ++ /// not currently sending. ++ bool canSend() { ++ return (socket_ && socket_->isOpen() && !stopping_ && !sending_); ++ } ++ ++ /// @brief Indicates whether or not a read can be initiated. ++ /// ++ /// Must be called in a thread-safe context ++ /// ++ /// @return True if the socket is open, is not attempting to stop, and is ++ /// not currently reading. ++ bool canRead() { ++ return (socket_ && socket_->isOpen() && !stopping_ && !reading_); ++ } ++ ++ /// @brief Returns input buffer size. ++ /// ++ /// Must be called in a thread-safe context ++ /// ++ /// @return size of the input buf ++ size_t getInputBufSize() const; ++ ++ /// @brief Returns pointer to the first byte of the input buffer. ++ /// ++ /// Must be called in a thread-safe context ++ /// ++ /// @return pointer to the data buffer ++ /// @throw InvalidOperation if called when the buffer is empty. ++ unsigned char* getInputBufData(); ++ ++ /// @brief IOService instance the drives socket IO ++ asiolink::IOServicePtr io_service_; ++ ++ /// @brief Callback to invoke to fetch the next address to ping. ++ NextToSendCallback next_to_send_cb_; ++ ++ /// @brief Callback to invoke when an ECHO write has completed. ++ EchoSentCallback echo_sent_cb_; ++ ++ /// @brief Callback to invoke when an ICMP reply has been received. ++ ReplyReceivedCallback reply_received_cb_; ++ ++ /// @brief Callback to invoke when the channel has shutdown. ++ ShutdownCallback shutdown_cb_; ++ ++ /// @brief Socket through which to ping. ++ PingSocketPtr socket_; ++ ++ /// @brief Buffer to hold the contents for most recent socket read. ++ std::vector<uint8_t> input_buf_; ++ ++ /// @brief Retains the endpoint from which the most recent reply was received. ++ ICMPEndpoint reply_endpoint_; ++ ++ /// @brief Indicates whether or not the socket has a read in progress. ++ bool reading_; ++ ++ /// @brief Indicates whether or not the socket has a write in progress. ++ bool sending_; ++ ++ /// @brief Indicates whether or not the channel has been told to stop. ++ bool stopping_; ++ ++ /// @brief The mutex used to protect internal state. ++ const boost::scoped_ptr<std::mutex> mutex_; ++ ++ /// @brief True if channel was opened in single-threaded mode, false ++ /// otherwise. ++ bool single_threaded_; ++ ++ /// @brief Pointer to WatchSocket instance supplying the "select-fd". ++ util::WatchSocketPtr watch_socket_; ++ ++ /// @brief WatchSocket fd registered with IfaceMgr. ++ int registered_write_fd_; ++ ++ /// @brief ICMPSocket fd registered with IfaceMgr. ++ int registered_read_fd_; ++}; ++ ++/// @brief Defines a smart pointer to PingChannel ++typedef boost::shared_ptr<PingChannel> PingChannelPtr; ++ ++} // end of namespace ping_check ++} // end of namespace isc ++ ++#endif +diff --git a/src/hooks/dhcp/ping_check/ping_check.dox b/src/hooks/dhcp/ping_check/ping_check.dox +new file mode 100644 +index 0000000000..a7fbe839c0 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/ping_check.dox +@@ -0,0 +1,44 @@ ++// Copyright (C) 2023-2025 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/. ++ ++/** ++ ++@mainpage Kea Ping Check Hooks Library ++ ++Welcome to Kea Ping Check Hooks Library. This documentation is ++addressed at developers who are interested in internal operation of the ++library. This file provides information needed to understand and perhaps ++extend this library. ++ ++This documentation is stand-alone: you should have read and ++understood <a href="https://reports.kea.isc.org/dev_guide/">Kea ++Developer's Guide</a> and in particular its section about hooks: <a ++href="https://reports.kea.isc.org/dev_guide/df/d46/hooksdgDevelopersGuide.html"> ++Hooks Developer's Guide</a>. ++ ++@section cbPingCheckOverview Overview ++The @c ping_check hooks library provides the ability for kea-dhcp4 to carry ++out an ICMP ECHO test of a candidate IP address prior to sending that address to ++a DHCPv4 client in a DHCPOFFER message. ++ ++@section cbPingCheckInternals Library Internals ++ ++In addition to the requisite @ref load() and @ref unload() functions, the library ++implements the following callouts: ++ ++- @ref dhcp4_srv_configured() - schedules a (re)start of the ICMP IO layer ++- @ref lease4_offer() - handles requests from kea-dhcp4 core to initiate a ping check ++for a candidate lease ++ ++The load() function instantiates an instance of @ref isc::ping_check::PingCheckMgr. ++This class is the top level object that provides configuration processing and supervises ++the execution of ping checks. ++ ++@section cbPingCheckMTCompatibility Multi-Threading Compatibility ++ ++The @c ping_check hooks library requires multi-threading. ++ ++*/ +diff --git a/src/hooks/dhcp/ping_check/ping_check_callouts.cc b/src/hooks/dhcp/ping_check/ping_check_callouts.cc +new file mode 100644 +index 0000000000..ae006359be +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/ping_check_callouts.cc +@@ -0,0 +1,240 @@ ++// Copyright (C) 2023-2025 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_service_mgr.h> ++#include <database/audit_entry.h> ++#include <dhcpsrv/cfgmgr.h> ++#include <ping_check_log.h> ++#include <ping_check_mgr.h> ++#include <hooks/hooks.h> ++#include <process/daemon.h> ++#include <string> ++ ++namespace isc { ++namespace ping_check { ++ ++/// @brief PingCheckMgr singleton ++PingCheckMgrPtr mgr; ++ ++} // end of namespace ping_check ++} // end of namespace isc ++ ++using namespace isc; ++using namespace isc::asiolink; ++using namespace isc::log; ++using namespace isc::data; ++using namespace isc::db; ++using namespace isc::dhcp; ++using namespace isc::ping_check; ++using namespace isc::hooks; ++using namespace isc::process; ++using namespace std; ++ ++// Functions accessed by the hooks framework use C linkage to avoid the name ++// mangling that accompanies use of the C++ compiler as well as to avoid ++// issues related to namespaces. ++extern "C" { ++ ++/// @brief dhcp4_srv_configured implementation. ++/// ++/// @param handle callout handle. ++int dhcp4_srv_configured(CalloutHandle& handle) { ++ try { ++ SrvConfigPtr server_config; ++ handle.getArgument("server_config", server_config); ++ mgr->updateSubnetConfig(server_config); ++ ++ NetworkStatePtr network_state; ++ handle.getArgument("network_state", network_state); ++ ++ // Schedule a start of the services. This ensures we begin after ++ // the dust has settled and Kea MT mode has been firmly established. ++ mgr->startService(network_state); ++ IOServiceMgr::instance().registerIOService(mgr->getIOService()); ++ } catch (const std::exception& ex) { ++ LOG_ERROR(ping_check_logger, PING_CHECK_DHCP4_SRV_CONFIGURED_FAILED) ++ .arg(ex.what()); ++ ++ handle.setStatus(isc::hooks::CalloutHandle::NEXT_STEP_DROP); ++ ostringstream os; ++ os << "Error: " << ex.what(); ++ string error(os.str()); ++ handle.setArgument("error", error); ++ return (1); ++ } ++ ++ return (0); ++} ++ ++/// @brief cb4_updated callout implementation. ++/// ++/// If it detects that any subnets were altered by the update it ++/// replaces the subnet cache contents. If any of the subnets ++/// fail to parse, the error is logged and the function returns ++/// a non-zero value. ++/// ++/// @param handle CalloutHandle. ++/// ++/// @return 0 upon success, 1 otherwise ++int cb4_updated(CalloutHandle& handle) { ++ AuditEntryCollectionPtr audit_entries; ++ handle.getArgument("audit_entries", audit_entries); ++ ++ auto const& object_type_idx = audit_entries->get<AuditEntryObjectTypeTag>(); ++ auto range = object_type_idx.equal_range("dhcp4_subnet"); ++ if (std::distance(range.first, range.second)) { ++ try { ++ // Server config has been committed, so use the current configuration. ++ mgr->updateSubnetConfig(CfgMgr::instance().getCurrentCfg()); ++ } catch (const std::exception& ex) { ++ LOG_ERROR(ping_check_logger, PING_CHECK_CB4_UPDATE_FAILED) ++ .arg(ex.what()); ++ return (1); ++ } ++ } ++ ++ return (0); ++} ++ ++/// @brief lease4_offer callout implementation. ++/// ++/// @param handle callout handle. ++int lease4_offer(CalloutHandle& handle) { ++ CalloutHandle::CalloutNextStep status = handle.getStatus(); ++ if (status == CalloutHandle::NEXT_STEP_DROP || ++ status == CalloutHandle::NEXT_STEP_SKIP) { ++ return (0); ++ } ++ ++ Pkt4Ptr query4; ++ Lease4Ptr lease4; ++ ParkingLotHandlePtr parking_lot; ++ try { ++ // Get all arguments available for the leases4_committed hook point. ++ // If any of these arguments is not available this is a programmatic ++ // error. An exception will be thrown which will be caught by the ++ // caller and logged. ++ handle.getArgument("query4", query4); ++ ++ Lease4CollectionPtr leases4; ++ handle.getArgument("leases4", leases4); ++ ++ uint32_t offer_lifetime; ++ handle.getArgument("offer_lifetime", offer_lifetime); ++ ++ Lease4Ptr old_lease; ++ handle.getArgument("old_lease", old_lease); ++ ++ if (query4->getType() != DHCPDISCOVER) { ++ isc_throw(InvalidOperation, "query4 is not a DHCPDISCOVER"); ++ } ++ ++ if (!leases4) { ++ isc_throw(InvalidOperation, "leases4 is null"); ++ } ++ ++ if (!leases4->empty()) { ++ lease4 = (*leases4)[0]; ++ } ++ ++ if (!lease4) { ++ isc_throw(InvalidOperation, "leases4 is empty, no lease to check"); ++ } ++ ++ // Fetch the parking lot. If it's empty the server is not employing ++ // parking, which is fine. ++ // Create a reference to the parked packet. This signals that we have a ++ // stake in unparking it. ++ parking_lot = handle.getParkingLotHandlePtr(); ++ if (parking_lot) { ++ parking_lot->reference(query4); ++ } ++ ++ // Get configuration based on the lease's subnet. ++ auto const& config = mgr->getScopedConfig(lease4); ++ ++ // Call shouldPing() to determine if we should ping check or not. ++ // - status == PARK - ping check it ++ // - status == CONTINUE - check not needed, release DHCPOFFER to client ++ // - status == DROP - duplicate check, drop the duplicate DHCPOFFER ++ status = mgr->shouldPing(lease4, query4, old_lease, config); ++ handle.setStatus(status); ++ if (status == CalloutHandle::NEXT_STEP_PARK) { ++ mgr->startPing(lease4, query4, parking_lot, config); ++ } else { ++ // Dereference the parked packet. This releases our stake in it. ++ if (parking_lot) { ++ parking_lot->dereference(query4); ++ } ++ } ++ ++ } catch (const std::exception& ex) { ++ LOG_ERROR(ping_check_logger, PING_CHECK_LEASE4_OFFER_FAILED) ++ .arg(query4 ? query4->getLabel() : "<no query>") ++ .arg(lease4 ? lease4->addr_.toText() : "<no lease>") ++ .arg(ex.what()); ++ // Make sure we dereference. ++ if (parking_lot) { ++ parking_lot->dereference(query4); ++ } ++ ++ return (1); ++ } ++ ++ return (0); ++} ++ ++/// @brief This function is called when the library is loaded. ++/// ++/// @param handle library handle ++/// @return 0 when initialization is successful, 1 otherwise ++int load(LibraryHandle& handle) { ++ try { ++ // Make the hook library only loadable by kea-dhcp4. ++ const string& proc_name = Daemon::getProcName(); ++ if (proc_name != "kea-dhcp4") { ++ isc_throw(isc::Unexpected, "Bad process name: " << proc_name ++ << ", expected kea-dhcp4"); ++ } ++ ++ // Instantiate the manager singleton. ++ mgr.reset(new PingCheckMgr()); ++ ++ // Configure the manager using the hook library's parameters. ++ ConstElementPtr json = handle.getParameters(); ++ mgr->configure(json); ++ } catch (const exception& ex) { ++ LOG_ERROR(ping_check_logger, PING_CHECK_LOAD_ERROR) ++ .arg(ex.what()); ++ return (1); ++ } ++ ++ LOG_INFO(ping_check_logger, PING_CHECK_LOAD_OK); ++ return (0); ++} ++ ++/// @brief This function is called when the library is unloaded. ++/// ++/// @return always 0. ++int unload() { ++ if (mgr) { ++ IOServiceMgr::instance().unregisterIOService(mgr->getIOService()); ++ mgr.reset(); ++ } ++ LOG_INFO(ping_check_logger, PING_CHECK_UNLOAD); ++ return (0); ++} ++ ++/// @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/hooks/dhcp/ping_check/ping_check_config.cc b/src/hooks/dhcp/ping_check/ping_check_config.cc +new file mode 100644 +index 0000000000..a1c69da61e +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/ping_check_config.cc +@@ -0,0 +1,98 @@ ++// Copyright (C) 2023-2025 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 <ping_check_config.h> ++ ++using namespace isc; ++using namespace isc::data; ++using namespace isc::dhcp; ++ ++namespace isc { ++namespace ping_check { ++ ++const data::SimpleKeywords ++PingCheckConfig::CONFIG_KEYWORDS = ++{ ++ { "enable-ping-check", Element::boolean }, ++ { "min-ping-requests", Element::integer }, ++ { "reply-timeout", Element::integer }, ++ { "ping-cltt-secs", Element::integer}, ++ { "ping-channel-threads", Element::integer} ++}; ++ ++PingCheckConfig::PingCheckConfig() : ++ enable_ping_check_(true), ++ min_ping_requests_(1), ++ reply_timeout_(100), ++ ping_cltt_secs_(60), ++ ping_channel_threads_(0) { ++} ++ ++void ++PingCheckConfig::parse(data::ConstElementPtr config) { ++ // Use a local instance to collect values. This way we ++ // avoid corrupting current values if there are any errors. ++ PingCheckConfig local; ++ ++ // Note checkKeywords() will throw DhcpConfigError if there is a problem. ++ SimpleParser::checkKeywords(CONFIG_KEYWORDS, config); ++ ConstElementPtr value = config->get("enable-ping-check"); ++ if (value) { ++ local.setEnablePingCheck(value->boolValue()); ++ } ++ ++ value = config->get("min-ping-requests"); ++ if (value) { ++ int64_t val = value->intValue(); ++ if (val <= 0) { ++ isc_throw(DhcpConfigError, "invalid min-ping-requests: '" ++ << val << "', must be greater than 0"); ++ } ++ ++ local.setMinPingRequests(static_cast<size_t>(val)); ++ } ++ ++ value = config->get("reply-timeout"); ++ if (value) { ++ int64_t val = value->intValue(); ++ if (val <= 0) { ++ isc_throw(DhcpConfigError, "invalid reply-timeout: '" ++ << val << "', must be greater than 0"); ++ } ++ ++ local.setReplyTimeout(static_cast<size_t>(val)); ++ } ++ ++ value = config->get("ping-cltt-secs"); ++ if (value) { ++ int64_t val = value->intValue(); ++ if (val < 0) { ++ isc_throw(DhcpConfigError, "invalid ping-cltt-secs: '" ++ << val << "', cannot be less than 0"); ++ } ++ ++ local.setPingClttSecs(static_cast<size_t>(val)); ++ } ++ ++ value = config->get("ping-channel-threads"); ++ if (value) { ++ int64_t val = value->intValue(); ++ if (val < 0) { ++ isc_throw(DhcpConfigError, "invalid ping-channel-threads: '" ++ << val << "', cannot be less than 0"); ++ } ++ ++ local.setPingChannelThreads(static_cast<size_t>(val)); ++ } ++ ++ // All values good, copy from local instance. ++ *this = local; ++} ++ ++} // end of namespace ping_check ++} // end of namespace isc +diff --git a/src/hooks/dhcp/ping_check/ping_check_config.h b/src/hooks/dhcp/ping_check/ping_check_config.h +new file mode 100644 +index 0000000000..9fd23eba59 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/ping_check_config.h +@@ -0,0 +1,134 @@ ++// Copyright (C) 2023-2025 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 PING_CHECK_CONFIG_H ++#define PING_CHECK_CONFIG_H ++ ++#include <cc/data.h> ++#include <cc/simple_parser.h> ++ ++namespace isc { ++namespace ping_check { ++ ++/// @brief Houses the Ping check configuration parameters for a single scope ++/// (e.g. global, subnet...); ++class PingCheckConfig { ++public: ++ /// @brief List of valid parameters and expected types. ++ static const data::SimpleKeywords CONFIG_KEYWORDS; ++ ++ /// @brief Constructor ++ PingCheckConfig(); ++ ++ /// @brief Destructor ++ ~PingCheckConfig() = default; ++ ++ /// @brief Extracts member values from an Element::map ++ /// ++ /// @param config map of configuration parameters ++ /// ++ /// @throw BadValue if invalid values are detected. ++ void parse(data::ConstElementPtr config); ++ ++ /// @brief Fetches the value of enable-ping-check ++ /// ++ /// @return boolean value of enable-ping-check ++ bool getEnablePingCheck() const { ++ return (enable_ping_check_); ++ }; ++ ++ /// @brief Sets the value of enable-ping-check ++ /// ++ /// @param value new value for enable-ping-check ++ void setEnablePingCheck(bool value) { ++ enable_ping_check_ = value; ++ } ++ ++ /// @brief Fetches the value of min-ping-requests ++ /// ++ /// @return integer value of min-ping-requests ++ uint32_t getMinPingRequests() const { ++ return (min_ping_requests_); ++ }; ++ ++ /// @brief Sets the value of min-ping-requests ++ /// ++ /// @param value new value for min-ping-requests ++ void setMinPingRequests(uint32_t value) { ++ min_ping_requests_ = value; ++ } ++ ++ /// @brief Fetches the value of reply-timeout ++ /// ++ /// @return integer value of reply-timeout ++ uint32_t getReplyTimeout() const { ++ return (reply_timeout_); ++ } ++ ++ /// @brief Sets the value of reply-timeout ++ /// ++ /// @param value new value for reply-timeout ++ void setReplyTimeout(uint32_t value) { ++ reply_timeout_ = value; ++ } ++ ++ /// @brief Fetches the value of ping-cltt-secs ++ /// ++ /// @return integer value of ping-cltt-secs ++ uint32_t getPingClttSecs() const { ++ return (ping_cltt_secs_); ++ } ++ ++ /// @brief Sets the value of ping-cltt-secs ++ /// ++ /// @param value new value for ping-cltt-secs ++ void setPingClttSecs(uint32_t value) { ++ ping_cltt_secs_ = value; ++ } ++ ++ /// @brief Fetches the value of ping-channel-threads ++ /// ++ /// @return integer value of ping-channel-threads ++ uint32_t getPingChannelThreads() const { ++ return (ping_channel_threads_); ++ } ++ ++ /// @brief Sets the value of ping-channel-threads ++ /// ++ /// @param value new value for ping-channel-threads ++ void setPingChannelThreads(uint32_t value) { ++ ping_channel_threads_ = value; ++ } ++ ++private: ++ // @brief True if checking is enabled. ++ bool enable_ping_check_; ++ ++ /// @brief minimum number of ECHO REQUESTs sent, without replies received, ++ /// required to declare an address free to offer. ++ uint32_t min_ping_requests_; ++ ++ /// @brief maximum number of milliseconds to wait for an ECHO REPLY after ++ /// an ECHO REQUEST has been sent. ++ uint32_t reply_timeout_; ++ ++ /// @brief minimum number of seconds that must elapse after the lease's CLTT ++ /// before a ping check will be conducted, when the client is the lease's ++ /// previous owner. ++ uint32_t ping_cltt_secs_; ++ ++ /// @brief Number of threads to use if Kea core is multi-threaded. ++ /// Defaults to 0 (for now) which means follow core number of threads. ++ size_t ping_channel_threads_; ++}; ++ ++/// @brief Defines a shared pointer to a PingCheckConfig. ++typedef boost::shared_ptr<PingCheckConfig> PingCheckConfigPtr; ++ ++} // end of namespace ping_check ++} // end of namespace isc ++ ++#endif +diff --git a/src/hooks/dhcp/ping_check/ping_check_log.cc b/src/hooks/dhcp/ping_check/ping_check_log.cc +new file mode 100644 +index 0000000000..9e877ff9b5 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/ping_check_log.cc +@@ -0,0 +1,17 @@ ++// Copyright (C) 2023-2025 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 <ping_check_log.h> ++ ++namespace isc { ++namespace ping_check { ++ ++isc::log::Logger ping_check_logger("ping-check-hooks"); ++ ++} // namespace ping_check ++} // namespace isc +diff --git a/src/hooks/dhcp/ping_check/ping_check_log.h b/src/hooks/dhcp/ping_check/ping_check_log.h +new file mode 100644 +index 0000000000..22e0fca953 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/ping_check_log.h +@@ -0,0 +1,23 @@ ++// Copyright (C) 2023-2025 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 PING_CHECK_LOG_H ++#define PING_CHECK_LOG_H ++ ++#include <log/logger_support.h> ++#include <log/macros.h> ++#include <log/log_dbglevels.h> ++#include <ping_check_messages.h> ++#include <iostream> ++ ++namespace isc { ++namespace ping_check { ++ ++extern isc::log::Logger ping_check_logger; ++ ++} // end of namespace ping_check ++} // end of namespace isc ++#endif +diff --git a/src/hooks/dhcp/ping_check/ping_check_messages.cc b/src/hooks/dhcp/ping_check/ping_check_messages.cc +new file mode 100644 +index 0000000000..7dea2c2397 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/ping_check_messages.cc +@@ -0,0 +1,99 @@ ++// File created from src/hooks/dhcp/ping_check/ping_check_messages.mes ++ ++#include <cstddef> ++#include <log/message_types.h> ++#include <log/message_initializer.h> ++ ++extern const isc::log::MessageID PING_CHECK_CB4_UPDATE_FAILED = "PING_CHECK_CB4_UPDATE_FAILED"; ++extern const isc::log::MessageID PING_CHECK_CHANNEL_ECHO_REPLY_RECEIVED = "PING_CHECK_CHANNEL_ECHO_REPLY_RECEIVED"; ++extern const isc::log::MessageID PING_CHECK_CHANNEL_ECHO_REQUEST_SENT = "PING_CHECK_CHANNEL_ECHO_REQUEST_SENT"; ++extern const isc::log::MessageID PING_CHECK_CHANNEL_MALFORMED_PACKET_RECEIVED = "PING_CHECK_CHANNEL_MALFORMED_PACKET_RECEIVED"; ++extern const isc::log::MessageID PING_CHECK_CHANNEL_NETWORK_WRITE_ERROR = "PING_CHECK_CHANNEL_NETWORK_WRITE_ERROR"; ++extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_CLOSED = "PING_CHECK_CHANNEL_SOCKET_CLOSED"; ++extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_CLOSE_ERROR = "PING_CHECK_CHANNEL_SOCKET_CLOSE_ERROR"; ++extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_OPENED = "PING_CHECK_CHANNEL_SOCKET_OPENED"; ++extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_READ_FAILED = "PING_CHECK_CHANNEL_SOCKET_READ_FAILED"; ++extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_WRITE_FAILED = "PING_CHECK_CHANNEL_SOCKET_WRITE_FAILED"; ++extern const isc::log::MessageID PING_CHECK_CHANNEL_STOP = "PING_CHECK_CHANNEL_STOP"; ++extern const isc::log::MessageID PING_CHECK_CHANNEL_WATCH_SOCKET_CLEAR_ERROR = "PING_CHECK_CHANNEL_WATCH_SOCKET_CLEAR_ERROR"; ++extern const isc::log::MessageID PING_CHECK_CHANNEL_WATCH_SOCKET_CLOSE_ERROR = "PING_CHECK_CHANNEL_WATCH_SOCKET_CLOSE_ERROR"; ++extern const isc::log::MessageID PING_CHECK_DHCP4_SRV_CONFIGURED_FAILED = "PING_CHECK_DHCP4_SRV_CONFIGURED_FAILED"; ++extern const isc::log::MessageID PING_CHECK_DUPLICATE_CHECK = "PING_CHECK_DUPLICATE_CHECK"; ++extern const isc::log::MessageID PING_CHECK_LEASE4_OFFER_FAILED = "PING_CHECK_LEASE4_OFFER_FAILED"; ++extern const isc::log::MessageID PING_CHECK_LOAD_ERROR = "PING_CHECK_LOAD_ERROR"; ++extern const isc::log::MessageID PING_CHECK_LOAD_OK = "PING_CHECK_LOAD_OK"; ++extern const isc::log::MessageID PING_CHECK_MGR_CHANNEL_DOWN = "PING_CHECK_MGR_CHANNEL_DOWN"; ++extern const isc::log::MessageID PING_CHECK_MGR_LEASE_FREE_TO_USE = "PING_CHECK_MGR_LEASE_FREE_TO_USE"; ++extern const isc::log::MessageID PING_CHECK_MGR_NEXT_ECHO_SCHEDULED = "PING_CHECK_MGR_NEXT_ECHO_SCHEDULED"; ++extern const isc::log::MessageID PING_CHECK_MGR_RECEIVED_ECHO_REPLY = "PING_CHECK_MGR_RECEIVED_ECHO_REPLY"; ++extern const isc::log::MessageID PING_CHECK_MGR_RECEIVED_UNEXPECTED_ECHO_REPLY = "PING_CHECK_MGR_RECEIVED_UNEXPECTED_ECHO_REPLY"; ++extern const isc::log::MessageID PING_CHECK_MGR_RECEIVED_UNEXPECTED_UNREACHABLE_MSG = "PING_CHECK_MGR_RECEIVED_UNEXPECTED_UNREACHABLE_MSG"; ++extern const isc::log::MessageID PING_CHECK_MGR_RECEIVED_UNREACHABLE_MSG = "PING_CHECK_MGR_RECEIVED_UNREACHABLE_MSG"; ++extern const isc::log::MessageID PING_CHECK_MGR_REPLY_RECEIVED_ERROR = "PING_CHECK_MGR_REPLY_RECEIVED_ERROR"; ++extern const isc::log::MessageID PING_CHECK_MGR_REPLY_TIMEOUT_EXPIRED = "PING_CHECK_MGR_REPLY_TIMEOUT_EXPIRED"; ++extern const isc::log::MessageID PING_CHECK_MGR_SEND_COMPLETED_ERROR = "PING_CHECK_MGR_SEND_COMPLETED_ERROR"; ++extern const isc::log::MessageID PING_CHECK_MGR_STARTED = "PING_CHECK_MGR_STARTED"; ++extern const isc::log::MessageID PING_CHECK_MGR_STARTED_SINGLE_THREADED = "PING_CHECK_MGR_STARTED_SINGLE_THREADED"; ++extern const isc::log::MessageID PING_CHECK_MGR_START_PING_CHECK = "PING_CHECK_MGR_START_PING_CHECK"; ++extern const isc::log::MessageID PING_CHECK_MGR_STOPPED = "PING_CHECK_MGR_STOPPED"; ++extern const isc::log::MessageID PING_CHECK_MGR_STOPPING = "PING_CHECK_MGR_STOPPING"; ++extern const isc::log::MessageID PING_CHECK_MGR_SUBNET_CONFIG_FAILED = "PING_CHECK_MGR_SUBNET_CONFIG_FAILED"; ++extern const isc::log::MessageID PING_CHECK_PAUSE_FAILED = "PING_CHECK_PAUSE_FAILED"; ++extern const isc::log::MessageID PING_CHECK_PAUSE_ILLEGAL = "PING_CHECK_PAUSE_ILLEGAL"; ++extern const isc::log::MessageID PING_CHECK_PAUSE_PERMISSIONS_FAILED = "PING_CHECK_PAUSE_PERMISSIONS_FAILED"; ++extern const isc::log::MessageID PING_CHECK_RESUME_FAILED = "PING_CHECK_RESUME_FAILED"; ++extern const isc::log::MessageID PING_CHECK_UNEXPECTED_READ_ERROR = "PING_CHECK_UNEXPECTED_READ_ERROR"; ++extern const isc::log::MessageID PING_CHECK_UNEXPECTED_WRITE_ERROR = "PING_CHECK_UNEXPECTED_WRITE_ERROR"; ++extern const isc::log::MessageID PING_CHECK_UNLOAD = "PING_CHECK_UNLOAD"; ++ ++namespace { ++ ++const char* values[] = { ++ "PING_CHECK_CB4_UPDATE_FAILED", "A subnet ping-check parameters failed to parse after being updated %1", ++ "PING_CHECK_CHANNEL_ECHO_REPLY_RECEIVED", "from address %1, id %2, sequence %3", ++ "PING_CHECK_CHANNEL_ECHO_REQUEST_SENT", "to address %1, id %2, sequence %3", ++ "PING_CHECK_CHANNEL_MALFORMED_PACKET_RECEIVED", "error occurred unpacking message %1, discarding it", ++ "PING_CHECK_CHANNEL_NETWORK_WRITE_ERROR", "occurred trying to ping %1, error %2", ++ "PING_CHECK_CHANNEL_SOCKET_CLOSED", "ICMP socket has been closed.", ++ "PING_CHECK_CHANNEL_SOCKET_CLOSE_ERROR", "an attempt to close the ICMP socket failed %1", ++ "PING_CHECK_CHANNEL_SOCKET_OPENED", "ICMP socket been opened successfully.", ++ "PING_CHECK_CHANNEL_SOCKET_READ_FAILED", "socket read completed with an error %1", ++ "PING_CHECK_CHANNEL_SOCKET_WRITE_FAILED", "socket write completed with an error %1", ++ "PING_CHECK_CHANNEL_STOP", "channel is stopping operations.", ++ "PING_CHECK_CHANNEL_WATCH_SOCKET_CLEAR_ERROR", "an attempt to clear the WatchSocket associated with", ++ "PING_CHECK_CHANNEL_WATCH_SOCKET_CLOSE_ERROR", "an attempt to close the WatchSocket associated with", ++ "PING_CHECK_DHCP4_SRV_CONFIGURED_FAILED", "dhcp4_srv_configured callout failed %1", ++ "PING_CHECK_DUPLICATE_CHECK", "Ping check already in progress for %1, initiated by %2", ++ "PING_CHECK_LEASE4_OFFER_FAILED", "lease4_offer callout failed for query %1, lease address %2, reason %3", ++ "PING_CHECK_LOAD_ERROR", "loading Ping Check hooks library failed %1", ++ "PING_CHECK_LOAD_OK", "Ping Check hooks library loaded successfully.", ++ "PING_CHECK_MGR_CHANNEL_DOWN", "Ping Channel has shutdown, ping checking will be skipped", ++ "PING_CHECK_MGR_LEASE_FREE_TO_USE", "address %1 is free to use for %2", ++ "PING_CHECK_MGR_NEXT_ECHO_SCHEDULED", "for %1, scheduling ECHO_REQUEST %2 of %3", ++ "PING_CHECK_MGR_RECEIVED_ECHO_REPLY", "from %1, id %2, sequence %3", ++ "PING_CHECK_MGR_RECEIVED_UNEXPECTED_ECHO_REPLY", "from %1, id %2, sequence %3 received after reply-timeout expired", ++ "PING_CHECK_MGR_RECEIVED_UNEXPECTED_UNREACHABLE_MSG", "for %1, id %2, sequence %3 received after reply-timeout expired", ++ "PING_CHECK_MGR_RECEIVED_UNREACHABLE_MSG", "for %1, id %2, sequence %3", ++ "PING_CHECK_MGR_REPLY_RECEIVED_ERROR", "an error occurred processing an ICMP reply message %1", ++ "PING_CHECK_MGR_REPLY_TIMEOUT_EXPIRED", "for %1, ECHO REQUEST %2 of %3, reply-timeout %4", ++ "PING_CHECK_MGR_SEND_COMPLETED_ERROR", "an error occurred in the send completion callback %1", ++ "PING_CHECK_MGR_STARTED", "ping channel operations are running, number of threads %1", ++ "PING_CHECK_MGR_STARTED_SINGLE_THREADED", "single-threaded ping channel operations are running", ++ "PING_CHECK_MGR_START_PING_CHECK", "for %1, initiated by %2", ++ "PING_CHECK_MGR_STOPPED", "channel operations have stopped", ++ "PING_CHECK_MGR_STOPPING", "ping channel operations are stopping", ++ "PING_CHECK_MGR_SUBNET_CONFIG_FAILED", "user-context for subnet id %1, contains invalid ping-check %2", ++ "PING_CHECK_PAUSE_FAILED", "Pausing ping channel operations failed %1", ++ "PING_CHECK_PAUSE_ILLEGAL", "Pausing ping channel operations not allowed %1", ++ "PING_CHECK_PAUSE_PERMISSIONS_FAILED", "Permissions check for ping-channel pause failed %1", ++ "PING_CHECK_RESUME_FAILED", "Resuming ping channel operations failed %1", ++ "PING_CHECK_UNEXPECTED_READ_ERROR", "could not start next socket read %1", ++ "PING_CHECK_UNEXPECTED_WRITE_ERROR", "could not start next socket write %1", ++ "PING_CHECK_UNLOAD", "Ping Check hooks library has been unloaded", ++ NULL ++}; ++ ++const isc::log::MessageInitializer initializer(values); ++ ++} // Anonymous namespace ++ +diff --git a/src/hooks/dhcp/ping_check/ping_check_messages.h b/src/hooks/dhcp/ping_check/ping_check_messages.h +new file mode 100644 +index 0000000000..9326c699e8 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/ping_check_messages.h +@@ -0,0 +1,50 @@ ++// File created from src/hooks/dhcp/ping_check/ping_check_messages.mes ++ ++#ifndef PING_CHECK_MESSAGES_H ++#define PING_CHECK_MESSAGES_H ++ ++#include <log/message_types.h> ++ ++extern const isc::log::MessageID PING_CHECK_CB4_UPDATE_FAILED; ++extern const isc::log::MessageID PING_CHECK_CHANNEL_ECHO_REPLY_RECEIVED; ++extern const isc::log::MessageID PING_CHECK_CHANNEL_ECHO_REQUEST_SENT; ++extern const isc::log::MessageID PING_CHECK_CHANNEL_MALFORMED_PACKET_RECEIVED; ++extern const isc::log::MessageID PING_CHECK_CHANNEL_NETWORK_WRITE_ERROR; ++extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_CLOSED; ++extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_CLOSE_ERROR; ++extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_OPENED; ++extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_READ_FAILED; ++extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_WRITE_FAILED; ++extern const isc::log::MessageID PING_CHECK_CHANNEL_STOP; ++extern const isc::log::MessageID PING_CHECK_CHANNEL_WATCH_SOCKET_CLEAR_ERROR; ++extern const isc::log::MessageID PING_CHECK_CHANNEL_WATCH_SOCKET_CLOSE_ERROR; ++extern const isc::log::MessageID PING_CHECK_DHCP4_SRV_CONFIGURED_FAILED; ++extern const isc::log::MessageID PING_CHECK_DUPLICATE_CHECK; ++extern const isc::log::MessageID PING_CHECK_LEASE4_OFFER_FAILED; ++extern const isc::log::MessageID PING_CHECK_LOAD_ERROR; ++extern const isc::log::MessageID PING_CHECK_LOAD_OK; ++extern const isc::log::MessageID PING_CHECK_MGR_CHANNEL_DOWN; ++extern const isc::log::MessageID PING_CHECK_MGR_LEASE_FREE_TO_USE; ++extern const isc::log::MessageID PING_CHECK_MGR_NEXT_ECHO_SCHEDULED; ++extern const isc::log::MessageID PING_CHECK_MGR_RECEIVED_ECHO_REPLY; ++extern const isc::log::MessageID PING_CHECK_MGR_RECEIVED_UNEXPECTED_ECHO_REPLY; ++extern const isc::log::MessageID PING_CHECK_MGR_RECEIVED_UNEXPECTED_UNREACHABLE_MSG; ++extern const isc::log::MessageID PING_CHECK_MGR_RECEIVED_UNREACHABLE_MSG; ++extern const isc::log::MessageID PING_CHECK_MGR_REPLY_RECEIVED_ERROR; ++extern const isc::log::MessageID PING_CHECK_MGR_REPLY_TIMEOUT_EXPIRED; ++extern const isc::log::MessageID PING_CHECK_MGR_SEND_COMPLETED_ERROR; ++extern const isc::log::MessageID PING_CHECK_MGR_STARTED; ++extern const isc::log::MessageID PING_CHECK_MGR_STARTED_SINGLE_THREADED; ++extern const isc::log::MessageID PING_CHECK_MGR_START_PING_CHECK; ++extern const isc::log::MessageID PING_CHECK_MGR_STOPPED; ++extern const isc::log::MessageID PING_CHECK_MGR_STOPPING; ++extern const isc::log::MessageID PING_CHECK_MGR_SUBNET_CONFIG_FAILED; ++extern const isc::log::MessageID PING_CHECK_PAUSE_FAILED; ++extern const isc::log::MessageID PING_CHECK_PAUSE_ILLEGAL; ++extern const isc::log::MessageID PING_CHECK_PAUSE_PERMISSIONS_FAILED; ++extern const isc::log::MessageID PING_CHECK_RESUME_FAILED; ++extern const isc::log::MessageID PING_CHECK_UNEXPECTED_READ_ERROR; ++extern const isc::log::MessageID PING_CHECK_UNEXPECTED_WRITE_ERROR; ++extern const isc::log::MessageID PING_CHECK_UNLOAD; ++ ++#endif // PING_CHECK_MESSAGES_H +diff --git a/src/hooks/dhcp/ping_check/ping_check_messages.mes b/src/hooks/dhcp/ping_check/ping_check_messages.mes +new file mode 100644 +index 0000000000..21d407bedf +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/ping_check_messages.mes +@@ -0,0 +1,229 @@ ++# Copyright (C) 2023-2025 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/. ++ ++% PING_CHECK_CB4_UPDATE_FAILED A subnet ping-check parameters failed to parse after being updated %1 ++This error message is emitted when an error occurs trying to parse a subnet ++ping-check parameters after the subnet was updated via configuration backend. ++This implies one or more of the parameters is invalid and must be corrected. ++ ++% PING_CHECK_CHANNEL_ECHO_REPLY_RECEIVED from address %1, id %2, sequence %3 ++Logged at debug log level 50. ++This debug message is issued when an ECHO REPLY has been received on ++the ping channel's ICMP socket. ++ ++% PING_CHECK_CHANNEL_ECHO_REQUEST_SENT to address %1, id %2, sequence %3 ++Logged at debug log level 50. ++This debug message is issued when an ECHO REQUEST has been written to the ++ping channel's ICMP socket. ++ ++% PING_CHECK_CHANNEL_MALFORMED_PACKET_RECEIVED error occurred unpacking message %1, discarding it ++Logged at debug log level 40. ++This debug message is emitted when an ICMP packet has been received ++that could not be unpacked. ++ ++% PING_CHECK_CHANNEL_NETWORK_WRITE_ERROR occurred trying to ping %1, error %2 ++This error message occurs when an asynchronous write on the ICMP socket ++failed trying to send on the ping target's network. This may mean an interface ++is down or there is a configuration error. The lease address to ping and the ++type of the error are provided in the arguments. ++ ++% PING_CHECK_CHANNEL_SOCKET_CLOSED ICMP socket has been closed. ++Logged at debug log level 40. ++This debug message is emitted when the ICMP socket for carrying out ++ping checks has been closed. ++ ++% PING_CHECK_CHANNEL_SOCKET_CLOSE_ERROR an attempt to close the ICMP socket failed %1 ++This error message is emitted when an unexpected error occurred ++while closing the ping check ICMP socket. The error detail is ++provided as an argument of the log message. ++ ++% PING_CHECK_CHANNEL_SOCKET_OPENED ICMP socket been opened successfully. ++Logged at debug log level 40. ++This debug message is emitted when the ICMP socket for carrying out ++ping checks has been successfully opened. ++ ++% PING_CHECK_CHANNEL_SOCKET_READ_FAILED socket read completed with an error %1 ++This error message occurs when an asynchronous read on the ICMP socket ++failed. The details of the error are provided as an argument of the log ++message. ++ ++% PING_CHECK_CHANNEL_SOCKET_WRITE_FAILED socket write completed with an error %1 ++This error message occurs when an asynchronous write on the ICMP socket ++failed. The details of the error are provided as an argument of the log ++message. ++ ++% PING_CHECK_CHANNEL_STOP channel is stopping operations. ++Logged at debug log level 40. ++This debug message indicates that the channel is stopping operations and ++closing the ICMP socket. The reason for stopping should be apparent in ++preceding log messages. ++ ++% PING_CHECK_CHANNEL_WATCH_SOCKET_CLEAR_ERROR an attempt to clear the WatchSocket associated with ++the single-threaded ping-channel failed %1 ++This error message is emitted when an unexpected error occurred ++while clearing the ready marker of the WatchSocket associated with ++the ping check channel. This can only occur when running in ++single-threaded mode. The error detail is provided as an argument ++of the log message. ++ ++% PING_CHECK_CHANNEL_WATCH_SOCKET_CLOSE_ERROR an attempt to close the WatchSocket associated with ++the single-threaded ping-channel failed %1 ++This error message is emitted when an unexpected error occurred ++while closing the WatchSocket associated with the ping check channel. ++This can only occur when running in single-threaded mode. ++The error detail is provided as an argument of the log message. ++ ++% PING_CHECK_DHCP4_SRV_CONFIGURED_FAILED dhcp4_srv_configured callout failed %1 ++This error message indicates an error during the Ping Check hook ++library dhcp4_srv_configured callout. The details of the error are ++provided as argument of the log message. ++ ++% PING_CHECK_DUPLICATE_CHECK Ping check already in progress for %1, initiated by %2 ++Logged at debug log level 40. ++This debug message is emitted when a duplicate request to test an address ++is received. When this occurs the duplicate test will be skipped and ++the associated DHCPOFFER will be dropped. ++ ++% PING_CHECK_LEASE4_OFFER_FAILED lease4_offer callout failed for query %1, lease address %2, reason %3 ++This error message indicates an error during the Ping Check hook ++library lease4_offer callout. The details of the error are ++provided as argument of the log message. ++ ++% PING_CHECK_LOAD_ERROR loading Ping Check hooks library failed %1 ++This error message indicates an error during loading the Ping Check ++hooks library. The details of the error are provided as argument of ++the log message. ++ ++% PING_CHECK_LOAD_OK Ping Check hooks library loaded successfully. ++This info message indicates that the Ping Check hooks library has ++been loaded successfully. ++ ++% PING_CHECK_MGR_CHANNEL_DOWN Ping Channel has shutdown, ping checking will be skipped ++This error message is emitted when the underlying ICMP channel ++has stopped due to an unrecoverable error. DHCP service may continue ++to function but without performing ping checks. Prior log messages should ++provide details. ++ ++% PING_CHECK_MGR_LEASE_FREE_TO_USE address %1 is free to use for %2 ++Logged at debug log level 40. ++This debug message is emitted when ping check has deemed an ++address is free to use. The log arguments detail the lease address ++checked and the query which initiated the check. ++ ++% PING_CHECK_MGR_NEXT_ECHO_SCHEDULED for %1, scheduling ECHO_REQUEST %2 of %3 ++Logged at debug log level 50. ++This debug message is emitted when the minimum number of ECHO REQUESTs ++is greater than 1 and the next ECHO REQUEST for a given lease address has ++been scheduled. ++ ++% PING_CHECK_MGR_RECEIVED_ECHO_REPLY from %1, id %2, sequence %3 ++Logged at debug log level 40. ++This debug message is emitted when an ECHO REPLY message has been received. ++The log argument details the source IP address, id, and sequence number of ++the ECHO REPLY. ++ ++% PING_CHECK_MGR_RECEIVED_UNEXPECTED_ECHO_REPLY from %1, id %2, sequence %3 received after reply-timeout expired ++Logged at debug log level 50. ++This debug message is emitted when an ECHO REPLY has been received after the ++reply-timeout has expired and is no longer of interest. This may be an errant ++ECHO REPLY or it may indicate that the reply-timeout value is too short. The ++log argument details the source IP address, id, and sequence number of the reply. ++ ++% PING_CHECK_MGR_RECEIVED_UNEXPECTED_UNREACHABLE_MSG for %1, id %2, sequence %3 received after reply-timeout expired ++Logged at debug log level 50. ++This debug message is emitted when an UNREACHABLE message has been received ++after the reply-timeout has expired and is no longer of interest. This may ++be an errant message or it may indicate that the reply-timeout value is ++too short. ++ ++% PING_CHECK_MGR_RECEIVED_UNREACHABLE_MSG for %1, id %2, sequence %3 ++Logged at debug log level 50. ++This debug message is emitted when an UNREACHABLE message has been received. ++The log argument details the target IP address, id, and sequence number from ++the embedded ECHO REQUEST. ++ ++% PING_CHECK_MGR_REPLY_RECEIVED_ERROR an error occurred processing an ICMP reply message %1 ++This debug message is emitted when an error occurred while processing an inbound ++ICMP message. The log argument describes the specific error. ++ ++% PING_CHECK_MGR_REPLY_TIMEOUT_EXPIRED for %1, ECHO REQUEST %2 of %3, reply-timeout %4 ++Logged at debug log level 50. ++This debug message is emitted when no reply is received to an ++ECHO REQUEST before the configured timeout value, `reply-timeout` ++was reached. The log arguments provides details. ++ ++% PING_CHECK_MGR_SEND_COMPLETED_ERROR an error occurred in the send completion callback %1 ++This error message is emitted when an unexpected error occurred after the completion of ++a successful write to the PingChannel socket. The log argument describes the ++specific error. ++ ++% PING_CHECK_MGR_STARTED ping channel operations are running, number of threads %1 ++This message is emitted when the ping check channel has been opened ++and is ready to process requests. The log argument includes the number of ++threads in the channel's thread pool. ++ ++% PING_CHECK_MGR_STARTED_SINGLE_THREADED single-threaded ping channel operations are running ++This message is emitted when the ping check channel has been opened ++and is ready to process requests in single-threaded mode. ++ ++% PING_CHECK_MGR_START_PING_CHECK for %1, initiated by %2 ++Logged at debug log level 40. ++This debug message is emitted when a ping check for an address ++has been initiated. The log arguments detail the lease address to ++ping and the query which initiated the check. ++ ++% PING_CHECK_MGR_STOPPED channel operations have stopped ++This message is emitted when the ping check channel operations ++have been stopped. ++ ++% PING_CHECK_MGR_STOPPING ping channel operations are stopping ++Logged at debug log level 40. ++This debug message is emitted when the ping check channel is stopping ++operations, typically due to configuration event or server shutdown. ++ ++% PING_CHECK_MGR_SUBNET_CONFIG_FAILED user-context for subnet id %1, contains invalid ping-check %2 ++This error message indicates that a subnet was updated via subnet commands ++and its 'user-context' contains invalid 'ping-check' configuration. The ++server will log the error once and then use global ping-check parameters ++for the subnet until the configuration is corrected. ++ ++% PING_CHECK_PAUSE_FAILED Pausing ping channel operations failed %1 ++This error message is emitted when an unexpected error occurred while ++attempting to pause the ping channel's thread pool. This error is highly ++unlikely and indicates a programmatic issue that should be reported as ++defect. ++ ++% PING_CHECK_PAUSE_ILLEGAL Pausing ping channel operations not allowed %1 ++This error message is emitted when attempting to pause the ping channel's ++thread pool. This indicates that a channel thread attempted to use a critical ++section which would result in a dead-lock. This error is highly unlikely ++and indicates a programmatic issue that should be reported as a defect. ++ ++% PING_CHECK_PAUSE_PERMISSIONS_FAILED Permissions check for ping-channel pause failed %1 ++This error message is emitted when an unexpected error occurred while ++validating an attempt to pause the ping channel's thread pool. This error ++is highly unlikely and indicates a programmatic issue that should be ++reported as a defect. ++ ++% PING_CHECK_RESUME_FAILED Resuming ping channel operations failed %1 ++This error message is emitted when an unexpected error occurred while ++attempting to resume operation of the ping channel's thread pool. This ++error is highly unlikely and indicates a programmatic issue that should ++be reported as defect. ++ ++% PING_CHECK_UNEXPECTED_READ_ERROR could not start next socket read %1 ++This error message occurs when initiating an asynchronous read on the ICMP ++socket failed in an unexpected fashion. The details of the error are provided ++as an argument of the log message. ++ ++% PING_CHECK_UNEXPECTED_WRITE_ERROR could not start next socket write %1 ++This error message occurs when initiating an asynchronous write on the ICMP ++socket failed in an unexpected fashion. The details of the error are provided ++as an argument of the log message. ++ ++% PING_CHECK_UNLOAD Ping Check hooks library has been unloaded ++This info message indicates that the Ping Check hooks library has been ++unloaded. +diff --git a/src/hooks/dhcp/ping_check/ping_check_mgr.cc b/src/hooks/dhcp/ping_check/ping_check_mgr.cc +new file mode 100644 +index 0000000000..cb4f2ee1dc +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/ping_check_mgr.cc +@@ -0,0 +1,798 @@ ++// Copyright (C) 2023-2025 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 <ping_check_mgr.h> ++#include <ping_check_log.h> ++#include <dhcpsrv/cfgmgr.h> ++#include <hooks/hooks_manager.h> ++#include <util/multi_threading_mgr.h> ++#include <util/chrono_time_utils.h> ++ ++using namespace isc; ++using namespace isc::asiolink; ++using namespace isc::dhcp; ++using namespace isc::data; ++using namespace isc::hooks; ++using namespace isc::util; ++using namespace std; ++using namespace std::chrono; ++ ++namespace ph = std::placeholders; ++ ++namespace isc { ++namespace ping_check { ++ ++PingCheckMgr::PingCheckMgr() ++ : io_service_(new IOService()), thread_pool_(), ++ store_(new PingContextStore()), ++ channel_(), ++ config_cache_(new ConfigCache()), ++ mutex_(new mutex()), ++ suspended_(false) { ++} ++ ++PingCheckMgr::PingCheckMgr(uint32_t num_threads, ++ uint32_t min_echos, ++ uint32_t reply_timeout) ++ : io_service_(new IOService()), thread_pool_(), ++ store_(new PingContextStore()), ++ channel_(), ++ config_cache_(new ConfigCache()), ++ mutex_(new mutex()), ++ suspended_(false) { ++ PingCheckConfigPtr config(new PingCheckConfig()); ++ config->setMinPingRequests(min_echos); ++ config->setReplyTimeout(reply_timeout); ++ config->setPingChannelThreads(num_threads); ++ config_cache_->setGlobalConfig(config); ++} ++ ++PingCheckMgr::~PingCheckMgr() { ++ stop(); ++} ++ ++void ++PingCheckMgr::configure(ConstElementPtr params) { ++ if (!params) { ++ isc_throw(dhcp::DhcpConfigError, "params must not be null"); ++ return; ++ } ++ ++ if (params->getType() != Element::map) { ++ isc_throw(dhcp::DhcpConfigError, "params must be an Element::map"); ++ return; ++ } ++ ++ PingCheckConfigPtr config(new PingCheckConfig()); ++ config->parse(params); ++ config_cache_->setGlobalConfig(config); ++} ++ ++void ++PingCheckMgr::updateSubnetConfig(SrvConfigPtr server_config) { ++ // Iterate over subnets and cache configurations for each. ++ ConfigCachePtr local_cache(new ConfigCache()); ++ local_cache->setGlobalConfig(config_cache_->getGlobalConfig()); ++ auto const& subnets = server_config->getCfgSubnets4()->getAll(); ++ for (auto const& subnet : (*subnets)) { ++ auto user_context = subnet->getContext(); ++ local_cache->parseAndCacheConfig(subnet->getID(), user_context); ++ } ++ ++ // No errors above, replace the existing cache. ++ config_cache_ = local_cache; ++} ++ ++const PingCheckConfigPtr ++PingCheckMgr::getGlobalConfig() const { ++ return (config_cache_->getGlobalConfig()); ++} ++ ++const PingCheckConfigPtr ++PingCheckMgr::getScopedConfig(Lease4Ptr& lease) { ++ if (!lease) { ++ // This really shouldn't happen. ++ isc_throw(InvalidOperation, "PingCheckConfig::getScopedConfig() - lease cannot be empty"); ++ } ++ ++ auto subnet_id = lease->subnet_id_; ++ ++ // If the cache is stale, update it. We do this to catch subnets that have been updated ++ // via subnet_cmds. ++ auto server_config = CfgMgr::instance().getCurrentCfg(); ++ auto const& subnet = server_config->getCfgSubnets4()->getBySubnetId(subnet_id); ++ if (!subnet) { ++ // This really shouldn't happen. ++ isc_throw(InvalidOperation, "PingCheckMgr::getScopedConfig() - " ++ "no subnet for id: " << subnet_id ++ << ", for lease address: " << lease->addr_); ++ } ++ ++ // If cache is stale flush it and we'll lazy init subnets as we see them. ++ if (subnet->getModificationTime() > config_cache_->getLastFlushTime()) { ++ config_cache_->flush(); ++ } ++ ++ // If we don't find an entry for this subnet then we haven't seen it ++ // before so parse and cache it. If the subnet doesn't specify ping-check ++ // we cache an empty entry. ++ PingCheckConfigPtr config; ++ if (!config_cache_->findConfig(subnet_id, config)) { ++ auto user_context = subnet->getContext(); ++ try { ++ config = config_cache_->parseAndCacheConfig(subnet_id, user_context); ++ } catch (const std::exception& ex) { ++ // We emit and error and then cache an empty entry. This causes us ++ // to log the error once and then default to global settings afterward. ++ // This avoids us relentlessly logging and failing. Remember this ++ // is happening because a subnet was updated with an invalid context via ++ // subnet-cmd. ++ LOG_ERROR(ping_check_logger, PING_CHECK_MGR_SUBNET_CONFIG_FAILED) ++ .arg(subnet_id) ++ .arg(ex.what()); ++ config_cache_->cacheConfig(subnet_id, config); ++ } ++ } ++ ++ // Return subnet's ping-check config if it specified one, otherwise ++ // return the global config. ++ return (config ? config : config_cache_->getGlobalConfig()); ++} ++ ++void ++PingCheckMgr::startPing(dhcp::Lease4Ptr& lease, dhcp::Pkt4Ptr& query, hooks::ParkingLotHandlePtr& parking_lot, ++ const PingCheckConfigPtr& config) { ++ if (checkSuspended()) { ++ // Server should not be submitting requests. ++ isc_throw(InvalidOperation, "PingCheckMgr::startPing() - DHCP service is suspended!"); ++ } ++ ++ if (!channel_ || !channel_->isOpen()) { ++ isc_throw(InvalidOperation, "PingCheckMgr::startPing() - channel isn't open"); ++ } ++ ++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC, ++ PING_CHECK_MGR_START_PING_CHECK) ++ .arg(lease->addr_) ++ .arg(query->getLabel()); ++ ++ // Adds a context to the store ++ store_->addContext(lease, query, config->getMinPingRequests(), ++ config->getReplyTimeout(), parking_lot); ++ ++ // Posts a call to channel's startSend() and startRead(). This will kick-start perpetual ++ // write and read cycles if they are not already running. ++ if (channel_) { ++ channel_->startSend(); ++ channel_->startRead(); ++ } ++} ++ ++void ++PingCheckMgr::startPing(dhcp::Lease4Ptr& lease, dhcp::Pkt4Ptr& query, hooks::ParkingLotHandlePtr& parking_lot) { ++ startPing(lease, query, parking_lot, getGlobalConfig()); ++} ++ ++bool ++PingCheckMgr::nextToSend(IOAddress& next) { ++ if (checkSuspended()) { ++ return (false); ++ } ++ ++ PingContextPtr context = store_->getNextToSend(); ++ if (!context) { ++ return (false); ++ } ++ ++ next = context->getTarget(); ++ // Transition to sending. ++ context->setState(PingContext::SENDING); ++ store_->updateContext(context); ++ ++ return (true); ++} ++ ++void ++PingCheckMgr::sendCompleted(const ICMPMsgPtr& echo, bool send_failed) { ++ if (checkSuspended()) { ++ return; ++ } ++ ++ try { ++ if (!echo) { ++ isc_throw(BadValue, "PingCheckMgr::sendCompleted() - echo is empty"); ++ } ++ ++ if (echo->getType() != ICMPMsg::ECHO_REQUEST) { ++ isc_throw(BadValue, "PingCheckMgr::sendCompleted() - message type: " ++ << echo->getType() << " is not an ECHO_REQUEST"); ++ } ++ ++ // Update the context associated with this ECHO_REQUEST. ++ PingContextPtr context = store_->getContextByAddress(echo->getDestination()); ++ if (!context) { ++ isc_throw(Unexpected, "PingCheckMgr::sendCompleted() " ++ " no context found for: " << echo->getDestination()); ++ } ++ ++ if (send_failed) { ++ // Recoverable error occurred which means we can't get to the target's ++ // network (interface down?). Treat this the same as TARGET UNREACHABLE. ++ finishFree(context); ++ } else { ++ // Transition the context to WAITING_FOR_REPLY. ++ context->beginWaitingForReply(); ++ store_->updateContext(context); ++ } ++ ++ // Update the expiration timer if necessary. ++ setNextExpiration(); ++ } catch (const std::exception& ex) { ++ LOG_ERROR(ping_check_logger, PING_CHECK_MGR_SEND_COMPLETED_ERROR) ++ .arg(ex.what()); ++ } ++} ++ ++void ++PingCheckMgr::replyReceived(const ICMPMsgPtr& reply) { ++ if (checkSuspended()) { ++ return; ++ } ++ ++ try { ++ if (!reply) { ++ isc_throw(BadValue, "PingCheckMgr::replyReceived() - echo is empty"); ++ } ++ ++ switch (reply->getType()) { ++ case ICMPMsg::ECHO_REPLY: ++ handleEchoReply(reply); ++ break; ++ case ICMPMsg::TARGET_UNREACHABLE: ++ // Extract embedded ECHO REQUEST ++ handleTargetUnreachable(reply); ++ break; ++ default: ++ // Ignore anything else. ++ return; ++ } ++ ++ setNextExpiration(); ++ } catch (const std::exception& ex) { ++ LOG_ERROR(ping_check_logger, PING_CHECK_MGR_REPLY_RECEIVED_ERROR) ++ .arg(ex.what()); ++ } ++} ++ ++void ++PingCheckMgr::handleEchoReply(const ICMPMsgPtr& echo_reply) { ++ // Update the context associated with this ECHO_REQUEST. ++ PingContextPtr context = store_->getContextByAddress(echo_reply->getSource()); ++ if (!context) { ++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_DETAIL, ++ PING_CHECK_MGR_RECEIVED_UNEXPECTED_ECHO_REPLY) ++ .arg(echo_reply->getSource()) ++ .arg(echo_reply->getId()) ++ .arg(echo_reply->getSequence()); ++ return; ++ } ++ ++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC, ++ PING_CHECK_MGR_RECEIVED_ECHO_REPLY) ++ .arg(echo_reply->getSource()) ++ .arg(echo_reply->getId()) ++ .arg(echo_reply->getSequence()); ++ ++ context->setState(PingContext::TARGET_IN_USE); ++ store_->updateContext(context); ++ ++ // If parking is employed, unpark the query from the parking lot, ++ // and set the offer_address_in_use argument in the callout handle ++ // to true, indicating to the server that the lease should be declined ++ // and the DHCPOFFER discarded. ++ auto parking_lot = context->getParkingLot(); ++ if (parking_lot) { ++ auto query = context->getQuery(); ++ auto callout_handle = query->getCalloutHandle(); ++ callout_handle->setArgument("offer_address_in_use", true); ++ parking_lot->unpark(query); ++ } ++ ++ // Remove the context from the store. ++ store_->deleteContext(context); ++} ++ ++void ++PingCheckMgr::handleTargetUnreachable(const ICMPMsgPtr& unreachable) { ++ // Unpack the embedded ECHO REQUEST. ++ ICMPMsgPtr embedded_echo; ++ auto payload = unreachable->getPayload(); ++ embedded_echo = ICMPMsg::unpack(payload.data(), payload.size()); ++ ++ // Fetch the context associated with the ECHO_REQUEST. ++ PingContextPtr context = store_->getContextByAddress(embedded_echo->getDestination()); ++ if (!context) { ++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_DETAIL, ++ PING_CHECK_MGR_RECEIVED_UNEXPECTED_UNREACHABLE_MSG) ++ .arg(embedded_echo->getDestination()) ++ .arg(embedded_echo->getId()) ++ .arg(embedded_echo->getSequence()); ++ return; ++ } ++ ++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_DETAIL, ++ PING_CHECK_MGR_RECEIVED_UNREACHABLE_MSG) ++ .arg(embedded_echo->getDestination()) ++ .arg(embedded_echo->getId()) ++ .arg(embedded_echo->getSequence()); ++ ++ // Render the address usable. ++ finishFree(context); ++} ++ ++void ++PingCheckMgr::finishFree(const PingContextPtr& context) { ++ context->setState(PingContext::TARGET_FREE); ++ store_->updateContext(context); ++ ++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC, ++ PING_CHECK_MGR_LEASE_FREE_TO_USE) ++ .arg(context->getTarget()) ++ .arg(context->getQuery()->getLabel()); ++ ++ // If parking is employed, unpark the query from the parking lot, ++ // and set the offer_address_in_use argument in the callout handle ++ // to false, indicating to the server that the lease is available ++ // and the DHCPOFFER should be sent to the client. ++ auto parking_lot = context->getParkingLot(); ++ if (parking_lot) { ++ auto query = context->getQuery(); ++ auto callout_handle = query->getCalloutHandle(); ++ callout_handle->setArgument("offer_address_in_use", false); ++ parking_lot->unpark(context->getQuery()); ++ } ++ ++ // Remove the context from the store. ++ store_->deleteContext(context); ++} ++ ++void ++PingCheckMgr::channelShutdown() { ++ LOG_ERROR(ping_check_logger, PING_CHECK_MGR_CHANNEL_DOWN); ++ if (io_service_) { ++ // As this is a callback that may be invoked by a channel ++ // thread we post a call to stopService() rather than call ++ // it directly. ++ io_service_->post([&]() { stopService(true); }); ++ } ++} ++ ++size_t ++PingCheckMgr::processExpiredSince(const TimeStamp& since /* = PingContext::now() */) { ++ auto expired_pings = store_->getExpiredSince(since); ++ size_t more_pings = 0; ++ for (auto const& context : *(expired_pings)) { ++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_DETAIL, ++ PING_CHECK_MGR_REPLY_TIMEOUT_EXPIRED) ++ .arg(context->getTarget()) ++ .arg(context->getEchosSent()) ++ .arg(context->getMinEchos()) ++ .arg(context->getReplyTimeout()); ++ ++ if (context->getEchosSent() < context->getMinEchos()) { ++ doNextEcho(context); ++ ++more_pings; ++ } else { ++ finishFree(context); ++ } ++ } ++ ++ return (more_pings); ++} ++ ++void ++PingCheckMgr::doNextEcho(const PingContextPtr& context) { ++ // Position to do another ping by re-entering WAITING_TO_SEND ++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_DETAIL, ++ PING_CHECK_MGR_NEXT_ECHO_SCHEDULED) ++ .arg(context->getTarget()) ++ .arg(context->getEchosSent() + 1) ++ .arg(context->getMinEchos()); ++ ++ context->beginWaitingToSend(); ++ store_->updateContext(context); ++} ++ ++TimeStamp ++PingCheckMgr::getNextExpiry() { ++ MultiThreadingLock lock(*mutex_); ++ return (next_expiry_); ++} ++ ++void ++PingCheckMgr::setNextExpiration() { ++ MultiThreadingLock lock(*mutex_); ++ if (checkSuspendedInternal()) { ++ return; ++ } ++ ++ setNextExpirationInternal(); ++} ++ ++void ++PingCheckMgr::setNextExpirationInternal() { ++ // Find the context that expires soonest. ++ PingContextPtr context = store_->getExpiresNext(); ++ if (context) { ++ // if the context's expiry is sooner than current expiry ++ // reschedule expiration timer ++ if ((next_expiry_ == PingContext::EMPTY_TIME()) || ++ (context->getNextExpiry() < next_expiry_)) { ++ auto now = PingContext::now(); ++ auto timeout = duration_cast<milliseconds>(context->getNextExpiry() - now); ++ /// @todo For now we'll impose a 2 ms minimum to avoid thrashing the timer. ++ timeout = (timeout > milliseconds(2) ? timeout : milliseconds(2)); ++ next_expiry_ = now + timeout; ++ expiration_timer_->setup(std::bind(&PingCheckMgr::expirationTimedOut, ++ shared_from_this()), ++ timeout.count(), IntervalTimer::ONE_SHOT); ++ } ++ } else { ++ // Nothing waiting to expire. Cancel the timer. ++ cancelExpirationTimerInternal(); ++ } ++} ++ ++void ++PingCheckMgr::cancelExpirationTimer() { ++ MultiThreadingLock lock(*mutex_); ++ cancelExpirationTimerInternal(); ++} ++ ++void ++PingCheckMgr::cancelExpirationTimerInternal() { ++ if (expiration_timer_) { ++ expiration_timer_->cancel(); ++ next_expiry_ = PingContext::EMPTY_TIME(); ++ } ++} ++ ++void ++PingCheckMgr::expirationTimedOut() { ++ MultiThreadingLock lock(*mutex_); ++ if (checkSuspendedInternal()) { ++ return; ++ } ++ ++ // Process everything that has expired since current time. ++ auto more_pings = processExpiredSince(); ++ ++ // Update the expiration timer. ++ next_expiry_ = PingContext::EMPTY_TIME(); ++ setNextExpirationInternal(); ++ ++ // In the event there was nothing left to process when timed out, ++ // poke the channel to make sure things are moving. ++ if (more_pings && channel_) { ++ channel_->startSend(); ++ channel_->startRead(); ++ } ++} ++ ++CalloutHandle::CalloutNextStep ++PingCheckMgr::shouldPing(Lease4Ptr& lease, Pkt4Ptr& query, ++ Lease4Ptr& old_lease, ++ const PingCheckConfigPtr& config) { ++ ++ // If ping-check is disabled or the channel isn't open, ++ // drop the query from parking and release the offer to the client. ++ if (!config->getEnablePingCheck() || !channel_ || !channel_->isOpen()) { ++ return (CalloutHandle::CalloutNextStep::NEXT_STEP_CONTINUE); ++ } ++ ++ // If we're already running check on this address then drop the ++ // query from parking and discard the offer. ++ if (store_->getContextByAddress(lease->addr_)) { ++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC, ++ PING_CHECK_DUPLICATE_CHECK) ++ .arg(lease->addr_) ++ .arg(query->getLabel()); ++ return (CalloutHandle::CalloutNextStep::NEXT_STEP_DROP); ++ } ++ ++ // If there's a previous lease that belongs to this client and ++ // it was touched by the client less than ping-cltt-secs ago then ++ // no check is needed. Drop the query from parking and release the ++ // offer to the client, ++ if (old_lease && (old_lease->addr_ == lease->addr_)) { ++ if (old_lease->belongsToClient(lease->hwaddr_, lease->client_id_)) { ++ auto now = time(0); ++ if ((now - old_lease->cltt_) < config->getPingClttSecs()) { ++ return (CalloutHandle::CalloutNextStep::NEXT_STEP_CONTINUE); ++ } ++ } ++ } ++ ++ // Leave it parked and do the ping check. ++ return (CalloutHandle::CalloutNextStep::NEXT_STEP_PARK); ++} ++ ++void ++PingCheckMgr::startService(NetworkStatePtr network_state) { ++ network_state_ = network_state; ++ io_service_->post([&]() { start(); }); ++} ++ ++bool ++PingCheckMgr::checkSuspended() { ++ MultiThreadingLock lock(*mutex_); ++ return (checkSuspendedInternal()); ++} ++ ++bool ++PingCheckMgr::checkSuspendedInternal() { ++ if (!network_state_ || network_state_->isServiceEnabled()) { ++ suspended_ = false; ++ } else { ++ if (!suspended_) { ++ suspended_ = true; ++ ++ // Flush the context store, dropping parked queries. ++ flush(false); ++ } ++ } ++ ++ return (suspended_); ++} ++ ++void ++PingCheckMgr::stopService(bool finish_free) { ++ // Pause the thread pool while we flush the store. ++ pause(); ++ ++ // Flush the context store. If finish_free is true ++ // the flush will treat the remaining context lease ++ // addresses as free to use and unpark them. This ++ // will cause the server to send out the associated ++ // OFFERs. If it's false we just drop them from ++ // the parking lot. ++ flush(finish_free); ++ ++ // Stop the thread pool, destroy the channel and the like. ++ stop(); ++} ++ ++void ++PingCheckMgr::start() { ++ if (MultiThreadingMgr::instance().isTestMode()) { ++ return; ++ } ++ if (!MultiThreadingMgr::instance().getMode()) { ++ startSingleThreaded(); ++ return; ++ } ++ ++ // We must be in multi-threading mode. ++ // Add critical section callbacks. ++ MultiThreadingMgr::instance().addCriticalSectionCallbacks("PING_CHECK", ++ std::bind(&PingCheckMgr::checkPermissions, this), ++ std::bind(&PingCheckMgr::pause, this), ++ std::bind(&PingCheckMgr::resume, this)); ++ ++ // Punt if we're already started. ++ if (thread_pool_ && thread_pool_->isStopped()) { ++ isc_throw(InvalidOperation, "PingCheckMgr already started!"); ++ } ++ ++ try { ++ auto config = config_cache_->getGlobalConfig(); ++ auto use_threads = (config->getPingChannelThreads() ? config->getPingChannelThreads() ++ : MultiThreadingMgr::instance().getThreadPoolSize()); ++ thread_pool_.reset(new IoServiceThreadPool(IOServicePtr(), use_threads, true)); ++ IOServicePtr pool_ios = thread_pool_->getIOService(); ++ channel_ = createChannel(pool_ios); ++ channel_->open(); ++ expiration_timer_.reset(new IntervalTimer(pool_ios)); ++ thread_pool_->run(); ++ LOG_INFO(ping_check_logger, PING_CHECK_MGR_STARTED) ++ .arg(use_threads); ++ } catch (const std::exception& ex) { ++ channel_.reset(); ++ thread_pool_.reset(); ++ isc_throw(Unexpected, "PingCheckMgr::start failed:" << ex.what()); ++ } ++} ++ ++void ++PingCheckMgr::startSingleThreaded() { ++ try { ++ auto config = config_cache_->getGlobalConfig(); ++ channel_ = createChannel(io_service_); ++ channel_->open(); ++ expiration_timer_.reset(new IntervalTimer(io_service_)); ++ LOG_INFO(ping_check_logger, PING_CHECK_MGR_STARTED_SINGLE_THREADED); ++ } catch (const std::exception& ex) { ++ channel_.reset(); ++ isc_throw(Unexpected, "PingCheckMgr::startSingleThreaded() failed:" << ex.what()); ++ } ++} ++ ++PingChannelPtr ++PingCheckMgr::createChannel(IOServicePtr io_service) { ++ return (PingChannelPtr(new PingChannel(io_service, ++ std::bind(&PingCheckMgr::nextToSend, ++ this, ph::_1), ++ std::bind(&PingCheckMgr::sendCompleted, ++ this, ph::_1, ph::_2), ++ std::bind(&PingCheckMgr::replyReceived, ++ this, ph::_1), ++ std::bind(&PingCheckMgr::channelShutdown, ++ this)))); ++} ++ ++void ++PingCheckMgr::checkPermissions() { ++ // Since this function is used as CS callback all exceptions must be ++ // suppressed, unlikely though they may be. ++ try { ++ if (thread_pool_) { ++ thread_pool_->checkPausePermissions(); ++ } ++ } catch (const isc::MultiThreadingInvalidOperation& ex) { ++ LOG_ERROR(ping_check_logger, PING_CHECK_PAUSE_ILLEGAL) ++ .arg(ex.what()); ++ // The exception needs to be propagated to the caller of the ++ // @ref MultiThreadingCriticalSection constructor. ++ throw; ++ } catch (const std::exception& ex) { ++ LOG_ERROR(ping_check_logger, PING_CHECK_PAUSE_PERMISSIONS_FAILED) ++ .arg(ex.what()); ++ } ++} ++ ++void ++PingCheckMgr::pause() { ++ if (!MultiThreadingMgr::instance().getMode()) { ++ return; ++ } ++ ++ // Since this function is used as CS callback all exceptions must be ++ // suppressed, unlikely though they may be. ++ try { ++ // Cancel the expiration timer. ++ cancelExpirationTimer(); ++ ++ // Pause the thread pool. ++ if (thread_pool_) { ++ thread_pool_->pause(); ++ } ++ } catch (const std::exception& ex) { ++ LOG_ERROR(ping_check_logger, PING_CHECK_PAUSE_FAILED) ++ .arg(ex.what()); ++ } ++} ++ ++void ++PingCheckMgr::resume() { ++ if (!MultiThreadingMgr::instance().getMode()) { ++ return; ++ } ++ ++ // Since this function is used as CS callback all exceptions must be ++ // suppressed, unlikely though they may be. ++ try { ++ if (thread_pool_) { ++ thread_pool_->run(); ++ } ++ ++ // Restore the expiration timer. ++ setNextExpiration(); ++ } catch (const std::exception& ex) { ++ LOG_ERROR(ping_check_logger, PING_CHECK_RESUME_FAILED) ++ .arg(ex.what()); ++ } ++} ++ ++void ++PingCheckMgr::stop() { ++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC, PING_CHECK_MGR_STOPPING); ++ ++ // Cancel the expiration timer. ++ cancelExpirationTimer(); ++ ++ if (channel_) { ++ channel_->close(); ++ } ++ ++ if (thread_pool_) { ++ // Remove critical section callbacks. ++ MultiThreadingMgr::instance().removeCriticalSectionCallbacks("PING_CHECK"); ++ ++ // Stop the thread pool. ++ thread_pool_->stop(); ++ ++ thread_pool_->getIOService()->stopAndPoll(); ++ ++ // Ditch the thread_pool ++ thread_pool_.reset(); ++ } ++ // Ditch the timer. It must be destroyed before the thread pool because in ++ // MT it holds a reference to the pool's IOService. ++ expiration_timer_.reset(); ++ ++ // Get rid of the channel. ++ channel_.reset(); ++ ++ if (io_service_) { ++ io_service_->stopAndPoll(); ++ } ++ ++ LOG_INFO(ping_check_logger, PING_CHECK_MGR_STOPPED); ++} ++ ++bool ++PingCheckMgr::isRunning() { ++ // In ST mode, running is an open channel. ++ if (!MultiThreadingMgr::instance().getMode()) { ++ return (channel_ && channel_->isOpen()); ++ } ++ ++ if (thread_pool_) { ++ return (thread_pool_->isRunning()); ++ } ++ ++ return (false); ++} ++ ++bool ++PingCheckMgr::isStopped() { ++ // In ST mode, stopped equates to no channel. ++ if (!MultiThreadingMgr::instance().getMode()) { ++ return (!channel_); ++ } ++ ++ if (thread_pool_) { ++ return (thread_pool_->isStopped()); ++ } ++ ++ return (true); ++} ++ ++bool ++PingCheckMgr::isPaused() { ++ if (thread_pool_) { ++ return (thread_pool_->isPaused()); ++ } ++ ++ return (false); ++} ++ ++void ++PingCheckMgr::flush(bool finish_free /* = false */) { ++ if (!store_) { ++ return; ++ } ++ ++ // Fetch them all. ++ auto contexts = store_->getAll(); ++ for (auto const& context : *contexts) { ++ if (finish_free) { ++ finishFree(context); ++ } else { ++ auto parking_lot = context->getParkingLot(); ++ if (parking_lot) { ++ parking_lot->drop(context->getQuery()); ++ } ++ } ++ } ++ ++ store_->clear(); ++} ++ ++} // end of namespace ping_check ++} // end of namespace isc +diff --git a/src/hooks/dhcp/ping_check/ping_check_mgr.h b/src/hooks/dhcp/ping_check/ping_check_mgr.h +new file mode 100644 +index 0000000000..42d11c1b48 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/ping_check_mgr.h +@@ -0,0 +1,436 @@ ++// Copyright (C) 2023-2025 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 PING_CHECK_MGR_H ++#define PING_CHECK_MGR_H ++ ++#include <asiolink/interval_timer.h> ++#include <asiolink/io_address.h> ++#include <asiolink/io_service.h> ++#include <asiolink/io_service_thread_pool.h> ++#include <cc/data.h> ++#include <cc/simple_parser.h> ++#include <dhcpsrv/srv_config.h> ++#include <hooks/callout_handle.h> ++#include <dhcp/pkt4.h> ++#include <dhcpsrv/lease.h> ++#include <dhcpsrv/network_state.h> ++#include <ping_context_store.h> ++#include <ping_channel.h> ++#include <config_cache.h> ++ ++#include <boost/enable_shared_from_this.hpp> ++ ++#include <mutex> ++ ++namespace isc { ++namespace ping_check { ++ ++/// @brief Defines a pointer to a PingContextStore. ++typedef boost::shared_ptr<PingContextStore> PingContextStorePtr; ++ ++/// @brief Ping Check Manager. ++/// ++/// PinCheckMgr carries out the higher order management of requests for ping ++/// checks from the server. It is a singleton, instantiated when the library ++/// is loaded. It is responsible for: ++/// 1. Parsing and applying configuration. ++/// 2. Maintaining in-memory store of current ping requests (PingContextStore). ++/// 3. Creating and managing the PingChannel through which individual ICMP ECHO/REPLY ++/// cycles are conducted. ++/// 4. When in multi-threaded mode, it creates an IOServiceThread and synchronizes ++/// its state with Kea core MT. ++class PingCheckMgr : public boost::enable_shared_from_this<PingCheckMgr> { ++public: ++ /// @brief Constructor. ++ explicit PingCheckMgr(); ++ ++ /// @brief Constructor. ++ /// ++ /// This constructor is used in testing. It permits setting some basic behavior ++ /// parameters directly, rather than requiring calls to @c configure(). ++ /// ++ /// @param num_threads number of threads to use in the thread pool (0 means follow ++ /// core thread pool size). ++ /// @param min_echos minimum number of ECHO REQUESTs sent without replies ++ /// received required to declare an address free to offer. Defaults to 1, ++ /// must be greater than zero. ++ /// @param reply_timeout maximum number of milliseconds to wait for an ++ /// ECHO REPLY after an ECHO REQUEST has been sent. Defaults to 100. ++ PingCheckMgr(uint32_t num_threads, ++ uint32_t min_echos = 1, ++ uint32_t reply_timeout = 100); ++ ++ /// @brief Destructor. ++ virtual ~PingCheckMgr(); ++ ++ /// @brief Configure the PingCheckMgr. ++ /// ++ /// @param params map containing the hook library parameters. ++ /// @throw BadValue and similar exceptions on error. ++ void configure(data::ConstElementPtr params); ++ ++ /// @brief Update the cache of subnet ping check configurations. ++ /// ++ /// Iterates over the subnets in the given server configuration, ++ /// and caches their ping-check configuration. ++ /// ++ /// @param server_config Server configuration containing the ++ /// configured subnets to process. ++ void updateSubnetConfig(dhcp::SrvConfigPtr server_config); ++ ++ /// @brief Creates a ping channel instance. ++ /// ++ /// @param io_service IOService that will drive the channel. ++ /// ++ /// @return pointer to the newly created channel. ++ virtual PingChannelPtr createChannel(asiolink::IOServicePtr io_service); ++ ++ /// @brief Initiates a ping check for a given lease and its associated ++ /// DHCPDISCOVER packet. ++ /// ++ /// Adds a context to the store and posts a call to @c PingChannel::startSend(). ++ /// ++ /// @param lease lease whose address needs to be ping checked. ++ /// @param query parked DHCPDISCOVER associated with the lease. ++ /// @param parking_lot parking lot in which query is parked. If empty, ++ /// parking is assumed to not be employed. ++ /// @param config configuration parameters to employ. ++ void startPing(dhcp::Lease4Ptr& lease, dhcp::Pkt4Ptr& query, ++ hooks::ParkingLotHandlePtr& parking_lot, ++ const PingCheckConfigPtr& config); ++ ++ /// @brief Initiates a ping check for a given lease and its associated ++ /// DHCPDISCOVER packet. ++ /// ++ /// Convenience method used in unit tests which uses global ++ /// configuration parameters only. ++ /// ++ /// @param lease lease whose address needs to be ping checked. ++ /// @param query parked DHCPDISCOVER associated with the lease. ++ /// @param parking_lot parking lot in which query is parked. If empty, ++ /// parking is assumed to not be employed. ++ void startPing(dhcp::Lease4Ptr& lease, dhcp::Pkt4Ptr& query, ++ hooks::ParkingLotHandlePtr& parking_lot); ++ ++ /// @brief Callback passed to PingChannel to use to retrieve the next ++ /// address to check. ++ /// ++ /// Fetches the context which has been in the WAITING_TO_SEND state the ++ /// longest and returns its lease address. ++ /// ++ /// @param[out] next upon return it will contain the next target address. ++ /// Contents are only meaningful if the function returns true. ++ /// ++ /// @return True another target address exists, false otherwise. ++ virtual bool nextToSend(asiolink::IOAddress& next); ++ ++ /// @brief Callback passed to PingChannel to invoke when an ECHO REQUEST ++ /// send has completed. ++ /// ++ /// If the send completed successfully we'll transition the context to ++ /// WAITING_FOR_REPLY, update the context in the store, and the update ++ /// next expiration. ++ /// ++ /// If the send failed, this implies that a recoverable error occurred, such ++ /// as a interface being down and thus, there is currently no way to send ++ /// the ping to the target network. We'll treat this the same as an ICMP ++ /// TARGET_UNREACHABLE and release the OFFER by calling @c finishFree(). ++ /// ++ /// @param echo ICMP echo message that as sent. ++ /// @param send_failed True if the send completed with a non-fatal error, ++ /// false otherwise. ++ virtual void sendCompleted(const ICMPMsgPtr& echo, bool send_failed); ++ ++ /// @brief Callback passed to PingChannel to invoke when an ICMP ++ /// reply has been received. ++ /// ++ /// If the reply type is an ECHO REQUEST, it is passed to ++ /// handleEchoRequest(), if it is an UNREACHABLE message it ++ /// is passed to handleTargetUnreachable(), any other message ++ /// type is dropped on the floor and the function returns. ++ /// Upon handler completion, it calls setNextExpiration() to ++ /// update the expiration timer. ++ /// ++ /// @param reply ICMP message that was received. ++ virtual void replyReceived(const ICMPMsgPtr& reply); ++ ++ /// @brief Process an ECHO REPLY message. ++ /// ++ /// @param echo_reply ICMP ECHO REPLY message to process. ++ void handleEchoReply(const ICMPMsgPtr& echo_reply); ++ ++ /// @brief Process an UNREACHABLE message. ++ /// ++ /// @param unreachable ICMP UNREACHABLE message to process. ++ void handleTargetUnreachable(const ICMPMsgPtr& unreachable); ++ ++ /// @brief Processes a context whose address has been deemed free to use. ++ /// ++ /// -# Moves the context to TARGET_FREE state ++ /// -# Updates the context in the store ++ /// -# Unparks the query which will release the DHCPOFFER to the client ++ /// -# Invokes the target free callback (do we still need this?) ++ /// -# Deletes the store from the context ++ /// ++ /// @param context context to process. ++ void finishFree(const PingContextPtr& context); ++ ++ /// @brief Position a context to do another ping test. ++ /// ++ /// -# Moves the context to WAITING_SEND_STATE ++ /// -# Updates the context in the store ++ /// ++ /// @param context context to process. ++ void doNextEcho(const PingContextPtr& context); ++ ++ /// @brief Callback passed to PingChannel to invoke when it shuts down. ++ /// ++ /// Logs the shutdown and then posts a call to @c stopService() to the ++ /// main IOService. ++ virtual void channelShutdown(); ++ ++ /// @brief Performs expiration processing for contexts whose WAITING_FOR_REPLY ++ /// states expired prior to a given point in time. ++ /// ++ /// expired_pings = store_->getExpiredSince(since) ++ /// for context : expired_pings { ++ /// unpark context->getQuery() ++ /// store_->deleteContext(context) ++ /// } ++ /// ++ /// @param since point in time to select against. Defaults to current time. ++ /// @return number of contexts scheduled for another ping, zero if none. ++ virtual size_t processExpiredSince(const TimeStamp& since = PingContext::now()); ++ ++ /// @brief Fetches the time at which expiration timer will next expire. ++ /// ++ /// @return TimeStamp containing the next expiration time. ++ TimeStamp getNextExpiry(); ++ ++ /// @brief Updates the expiration timer (thread safe). ++ /// ++ /// PingContextPtr next = pings->getExpiresNext() ++ /// if next ++ /// reschedule expiration timer for next->getNextExpiry(); ++ /// else ++ /// cancel expiration timer ++ virtual void setNextExpiration(); ++ ++ /// @brief Updates the expiration timer. ++ /// ++ /// PingContextPtr next = pings->getExpiresNext() ++ /// if next ++ /// reschedule expiration timer for next->getNextExpiry(); ++ /// else ++ /// cancel expiration timer ++ virtual void setNextExpirationInternal(); ++ ++ /// @brief Cancels the expiration timer (thread safe). ++ void cancelExpirationTimer(); ++ ++ /// @brief Cancels the expiration timer. ++ void cancelExpirationTimerInternal(); ++ ++ /// @brief Callback passed to expiration timer to invoke on timeout. ++ virtual void expirationTimedOut(); ++ ++ /// @brief Determines whether or not a lease should be ping checked. ++ /// ++ /// Employs the following logic to determine if a ping-check should ++ /// be conducted: ++ /// ++ /// If there's a previous lease that belongs to this client and ++ /// it was touched by the client less than ping-cltt-secs ago, ++ /// then send the offer to the client without ping checking. ++ /// ++ /// Otherwise a ping-check is called for, leave the query parked. ++ /// ++ /// @param lease prospective lease to check. ++ /// @param query DHCPDISCOVER associated with the lease. ++ /// @param old_lease pre-existing lease for this client (if one). ++ /// @param config configuration parameters to employ. ++ /// ++ /// @return CalloutNextStep indicating what should happen next: ++ /// - status == PARK - ping check it ++ /// - status == CONTINUE - check not needed, release DHCPOFFER to client ++ /// - status == DROP - duplicate check, drop the duplicate DHCPOFFER ++ virtual hooks::CalloutHandle::CalloutNextStep shouldPing(dhcp::Lease4Ptr& lease, ++ dhcp::Pkt4Ptr& query, ++ dhcp::Lease4Ptr& old_lease, ++ const PingCheckConfigPtr& config); ++ ++ /// @brief Check if the current thread can perform thread pool state ++ /// transition. ++ /// ++ /// @throw MultiThreadingInvalidOperation if the state transition is done on ++ /// any of the worker threads. ++ void checkPermissions(); ++ ++ /// @brief Performs a deferred start by posting an invocation of @c start() ++ /// to the given IOService. ++ /// ++ /// @param network_state pointer to server's networks state object. ++ void startService(dhcp::NetworkStatePtr network_state); ++ ++ /// @brief Shuts down the manager's channel, flushes the store. ++ /// ++ /// This function gracefully winds down operation: ++ /// ++ /// 1. Pauses the thread pool. ++ /// 2. Flushes the context store, either finishing all contexts as free ++ /// or just dropping them from parking, depending on finish_free parameter. ++ /// 3. Stop the thread pool, shutdown the channel. ++ /// ++ /// @param finish_free if true finishFree() will be invoke on all remaining ++ /// contexts in the store, otherwise their queries are simply dropped from ++ /// the parking lot. ++ void stopService(bool finish_free = false); ++ ++ /// @brief Start PingChannel operations. ++ /// ++ /// Will start multi-threaded if core MT is enabled, or calls ++ /// @c startSingleThreaded() if core MT is disabled. Creates ++ /// a thread pool with its own IOService, uses that IOService ++ /// when creating the channel. ++ void start(); ++ ++ /// @brief Start single-threaded PingChannel operations. ++ /// ++ /// Does not create a thread pool. Uses main thread's IOService ++ /// when creating the channel. ++ void startSingleThreaded(); ++ ++ /// @brief Pause PingChannel operations. ++ /// ++ /// In multi-threaded mode this pauses the thread pool threads, in ++ /// single-threaded mode it does nothing. ++ void pause(); ++ ++ /// @brief Resume PingChannel operations. ++ /// ++ /// In multi-threaded mode this resumes the thread pool threads, in ++ /// single-threaded mode it does nothing. ++ void resume(); ++ ++ /// @brief Flushes the ping context store. ++ /// ++ /// This function iterates over the contexts in the store and then ++ /// either invokes finishFree() or drops their queries from parking ++ /// depending upon finish_free parameter. It assumes the operations ++ /// have ceased (i.e. thread pool is not running). ++ /// ++ /// @param finish_free if true finishFree() will be invoke on all remaining ++ /// contexts in the store, otherwise their queries are simply dropped from ++ /// the parking lot. ++ void flush(bool finish_free = false); ++ ++ /// @brief Stop PingChannel operations. ++ void stop(); ++ ++ /// @brief Indicates if the thread pool is running. ++ /// ++ /// @return True if the thread pool exists and it is in the RUNNING state in ++ /// multi-threaded mode, true if the channel exists and is open in single-threaded ++ /// mode, false otherwise. ++ bool isRunning(); ++ ++ /// @brief Indicates if the thread pool is stopped. ++ /// ++ /// @return True if the thread pool does not exist or it is in the STOPPED ++ /// state in multi-threaded mode, true if the channel does not exist in ++ /// single-threaded mode, false otherwise. ++ bool isStopped(); ++ ++ /// @brief Indicates if the thread pool is paused. ++ /// ++ /// @return True if the thread pool exists and it is in the PAUSED state, ++ /// false otherwise. Always returns false in single-threaded mode. ++ bool isPaused(); ++ ++ /// @brief Checks if operations are currently suspended due to NetworkState. ++ /// ++ /// Thread-safe wrapper around checkSuspendedInternal(). ++ /// ++ /// @return True if operations are suspended, false otherwise. ++ bool checkSuspended(); ++ ++ /// @brief Checks if operations are currently suspended due to NetworkState. ++ /// ++ /// If DHCP service is enabled, operations are not suspended and the function ++ /// returns false. Otherwise operations, if not already suspended, are suspended ++ /// by flushing the PingContext store and the function returns true. The queries ++ /// for flushed contexts are dropped from parking and thus their offers discarded. ++ /// ++ /// @return True if operations are suspended, false otherwise. ++ bool checkSuspendedInternal(); ++ ++ /// @brief Fetches the current, global configuration parameters. ++ /// ++ /// @return PingCheckConfig reference containing the current configuration. ++ const PingCheckConfigPtr getGlobalConfig() const; ++ ++ /// @brief Fetches the current, scoped configuration parameters. ++ /// ++ /// @param lease lease for which the parameters are desired. ++ /// ++ /// @return PingCheckConfig reference containing the current configuration. ++ const PingCheckConfigPtr getScopedConfig(dhcp::Lease4Ptr& lease); ++ ++ /// @brief Get the hook I/O service. ++ /// ++ /// @return the hook I/O service. ++ isc::asiolink::IOServicePtr getIOService() { ++ return (io_service_); ++ } ++ ++ /// @brief Set the hook I/O service. ++ /// ++ /// @param io_service the hook I/O service. ++ void setIOService(isc::asiolink::IOServicePtr io_service) { ++ io_service_ = io_service; ++ } ++ ++protected: ++ ++ /// @brief The hook I/O service. ++ isc::asiolink::IOServicePtr io_service_; ++ ++ /// @brief Thread pool used when running multi-threaded. ++ asiolink::IoServiceThreadPoolPtr thread_pool_; ++ ++ /// @brief In-memory store of PingContexts. ++ PingContextStorePtr store_; ++ ++ /// @brief Channel that conducts ICMP messaging. ++ PingChannelPtr channel_; ++ ++ /// @brief Warehouses parsed global and subnet configuration. ++ ConfigCachePtr config_cache_; ++ ++ /// @brief Tracks whether or not the server is processing DHCP packets. ++ dhcp::NetworkStatePtr network_state_; ++ ++ /// @brief TimeStamp of the next expiration event. ++ TimeStamp next_expiry_; ++ ++ /// @brief Timer which tracks the next expiration event. ++ asiolink::IntervalTimerPtr expiration_timer_; ++ ++ /// @brief The mutex used to protect internal state. ++ const boost::scoped_ptr<std::mutex> mutex_; ++ ++ /// @brief Indicates whether or not operations have been suspended. ++ bool suspended_; ++}; ++ ++/// @brief Defines a shared pointer to a PingCheckMgr. ++typedef boost::shared_ptr<PingCheckMgr> PingCheckMgrPtr; ++ ++} // end of namespace ping_check ++} // end of namespace isc ++ ++#endif +diff --git a/src/hooks/dhcp/ping_check/ping_context.cc b/src/hooks/dhcp/ping_check/ping_context.cc +new file mode 100644 +index 0000000000..45e896f948 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/ping_context.cc +@@ -0,0 +1,237 @@ ++// Copyright (C) 2023-2025 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 <ping_context.h> ++#include <ping_check_log.h> ++#include <exceptions/exceptions.h> ++#include <util/chrono_time_utils.h> ++#include <iostream> ++ ++using namespace std; ++using namespace isc; ++using namespace isc::asiolink; ++using namespace isc::dhcp; ++using namespace isc::hooks; ++using namespace std::chrono; ++ ++namespace isc { ++namespace ping_check { ++ ++PingContext::PingContext(Lease4Ptr& lease, Pkt4Ptr& query, ++ uint32_t min_echos /* = 1 */, ++ uint32_t reply_timeout /* = 100 */, ++ ParkingLotHandlePtr& parking_lot /* = EMPTY_LOT() */) ++ : min_echos_(min_echos), ++ reply_timeout_(reply_timeout), ++ echos_sent_(0), ++ last_echo_sent_time_(EMPTY_TIME()), ++ send_wait_start_(EMPTY_TIME()), ++ next_expiry_(EMPTY_TIME()), ++ created_time_(PingContext::now()), ++ lease_(lease), ++ query_(query), ++ state_(NEW), ++ parking_lot_(parking_lot) { ++ if (!lease_) { ++ isc_throw(BadValue, "PingContext ctor - lease cannot be empty"); ++ } ++ ++ if (!query_) { ++ isc_throw(BadValue, "PingContext ctor - query cannot be empty"); ++ } ++ ++ if (getTarget() == IOAddress::IPV4_ZERO_ADDRESS()) { ++ isc_throw(BadValue, "PingContext ctor - target address cannot be 0.0.0.0"); ++ } ++ ++ if (min_echos_ == 0) { ++ isc_throw(BadValue, "PingContext ctor - min_echos must be greater than 0"); ++ } ++ ++ if (reply_timeout_ == 0) { ++ isc_throw(BadValue, "PingContext ctor - reply_timeout must be greater than 0"); ++ } ++} ++ ++PingContext::State ++PingContext::stringToState(const std::string& state_str) { ++ if (state_str == "NEW") { ++ return (NEW); ++ } ++ ++ if (state_str == "WAITING_TO_SEND") { ++ return (WAITING_TO_SEND); ++ } ++ ++ if (state_str == "SENDING") { ++ return (SENDING); ++ } ++ ++ if (state_str == "WAITING_FOR_REPLY") { ++ return (WAITING_FOR_REPLY); ++ } ++ ++ if (state_str == "TARGET_FREE") { ++ return (TARGET_FREE); ++ } ++ ++ if (state_str == "TARGET_IN_USE") { ++ return (TARGET_IN_USE); ++ } ++ ++ isc_throw(BadValue, "Invalid PingContext::State: '" << state_str << "'"); ++} ++ ++TimeStamp ++PingContext::now() { ++ return (time_point_cast<milliseconds>(std::chrono::system_clock::now())); ++} ++ ++std::string ++PingContext::stateToString(const PingContext::State& state) { ++ std::string label = ""; ++ switch (state) { ++ case NEW: ++ label = "NEW"; ++ break; ++ case WAITING_TO_SEND: ++ label = "WAITING_TO_SEND"; ++ break; ++ case SENDING: ++ label = "SENDING"; ++ break; ++ case WAITING_FOR_REPLY: ++ label = "WAITING_FOR_REPLY"; ++ break; ++ case TARGET_FREE: ++ label = "TARGET_FREE"; ++ break; ++ case TARGET_IN_USE: ++ label = "TARGET_IN_USE"; ++ break; ++ } ++ ++ return (label); ++} ++ ++const IOAddress& PingContext::getTarget() const { ++ return (lease_->addr_); ++} ++ ++uint32_t ++PingContext::getMinEchos() const { ++ return (min_echos_); ++} ++ ++void ++PingContext::setMinEchos(uint32_t value) { ++ min_echos_ = value; ++} ++ ++uint32_t ++PingContext::getReplyTimeout() const { ++ return (reply_timeout_); ++} ++ ++void ++PingContext::setReplyTimeout(uint32_t value) { ++ reply_timeout_ = value; ++} ++ ++uint32_t ++PingContext::getEchosSent() const { ++ return (echos_sent_); ++} ++ ++void ++PingContext::setEchosSent(uint32_t value) { ++ echos_sent_ = value; ++} ++ ++const TimeStamp& ++PingContext::getLastEchoSentTime() const { ++ return (last_echo_sent_time_); ++} ++ ++void ++PingContext::setLastEchoSentTime(const TimeStamp& value) { ++ last_echo_sent_time_ = value; ++} ++ ++const TimeStamp& ++PingContext::getSendWaitStart() const { ++ return (send_wait_start_); ++} ++ ++bool ++PingContext::isWaitingToSend() const { ++ return (state_ == WAITING_TO_SEND); ++} ++ ++void ++PingContext::setSendWaitStart(const TimeStamp& value) { ++ send_wait_start_ = value; ++} ++ ++const TimeStamp& ++PingContext::getNextExpiry() const { ++ return (next_expiry_); ++} ++ ++bool ++PingContext::isWaitingForReply() const { ++ return (state_ == WAITING_FOR_REPLY); ++} ++ ++void ++PingContext::setNextExpiry(const TimeStamp& value) { ++ next_expiry_ = value; ++} ++ ++const TimeStamp& ++PingContext::getCreatedTime() const { ++ return (created_time_); ++} ++ ++PingContext::State ++PingContext::getState() const { ++ return (state_); ++} ++ ++void ++PingContext::setState(const PingContext::State& value) { ++ state_ = value; ++} ++ ++Pkt4Ptr ++PingContext::getQuery() const { ++ return (query_); ++} ++ ++Lease4Ptr ++PingContext::getLease() const { ++ return (lease_); ++} ++ ++void ++PingContext::beginWaitingToSend(const TimeStamp& begin_time /* = now() */) { ++ state_ = WAITING_TO_SEND; ++ send_wait_start_ = begin_time; ++} ++ ++void ++PingContext::beginWaitingForReply(const TimeStamp& begin_time /* = now() */) { ++ ++echos_sent_; ++ last_echo_sent_time_ = begin_time; ++ next_expiry_ = begin_time + milliseconds(reply_timeout_); ++ state_ = WAITING_FOR_REPLY; ++} ++ ++} // end of namespace ping_check ++} // end of namespace isc ++ +diff --git a/src/hooks/dhcp/ping_check/ping_context.h b/src/hooks/dhcp/ping_check/ping_context.h +new file mode 100644 +index 0000000000..2c5b704a04 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/ping_context.h +@@ -0,0 +1,280 @@ ++// Copyright (C) 2023-2025 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 PING_CONTEXT_H ++#define PING_CONTEXT_H ++ ++#include <dhcp/pkt4.h> ++#include <dhcpsrv/lease.h> ++#include <hooks/parking_lots.h> ++ ++#include <chrono> ++ ++namespace isc { ++namespace ping_check { ++ ++/// @brief Specifies the type for time stamps. ++using TimeStamp = std::chrono::time_point<std::chrono::system_clock>; ++ ++/// @brief Embodies the life cycle of a ping check test for a single address ++/// for a single DHCPDISCOVER. ++/// ++/// The class uses a state-model to direct the tasks needed to execute one ++/// or more ECHO REQUEST SEND/WAIT FOR REPLY cycles until the address is ++/// either deemed free to offer or in-use and should not be offered. The ++/// number of cycles conducted is dictated by the minimum number of echos ++/// (@c min_echos_) and whether or not either an ECHO REPLY or DESTINATION ++/// UNREACHABLE are received. ++class PingContext { ++public: ++ ++ /// @brief Defines PingContext life cycle states ++ enum State { ++ NEW, // Newly created ++ WAITING_TO_SEND, // Waiting to send next ECHO REQUEST ++ SENDING, // Next ECHO REQUEST is being sent ++ WAITING_FOR_REPLY, // ECHO REQUEST sent, Waiting for reply or timeout ++ TARGET_FREE, // Target has been deemed free to offer. ++ TARGET_IN_USE // Target has been deemed in-use, do not offer ++ }; ++ ++ /// @brief Converts a string to State ++ /// ++ /// @param state_str Upper case string label to convert ++ /// @return State value corresponding to the given string ++ /// ++ /// @throw BadValue if the string is not a valid state label ++ static State stringToState(const std::string& state_str); ++ ++ /// @brief Converts a State to a string ++ /// ++ /// @param state State to convert ++ /// @return string label corresponding to the given state ++ static std::string stateToString(const State& state); ++ ++ /// @brief Constructor ++ /// ++ /// @param lease pointer to the lease whose address needs to be checked ++ /// @param query DHCPDISCOVER that instigated the check ++ /// @param min_echos minimum number of ECHO REQUESTs sent without replies ++ /// received required to declare an address free to offer. Defaults to 1, ++ /// must be greater than zero. ++ /// @param reply_timeout maximum number of milliseconds to wait for an ++ /// ECHO REPLY after an ECHO REQUEST has been sent. Defaults to 100, ++ /// must be greater than 0. ++ /// @param parking_lot parking lot in which the query is parked. Defaults ++ /// to an empty pointer. ++ /// ++ /// @throw BadValue if either lease or query are empty, or if the lease ++ /// address is 0.0.0.0 ++ PingContext(isc::dhcp::Lease4Ptr& lease, isc::dhcp::Pkt4Ptr& query, ++ uint32_t min_echos = 1, uint32_t reply_timeout = 100, ++ isc::hooks::ParkingLotHandlePtr& parking_lot = EMPTY_LOT()); ++ ++ /// @brief Destructor ++ virtual ~PingContext() = default; ++ ++ /// @brief Fetches the current timestamp (UTC/milliseconds precision) ++ /// ++ /// @return current time as a TimeStamp ++ static TimeStamp now(); ++ ++ /// @brief Fetches an empty timestamp ++ /// ++ /// @return an empty TimeStamp ++ static const TimeStamp& EMPTY_TIME() { ++ static TimeStamp empty_time; ++ return (empty_time); ++ } ++ ++ /// @brief Fetches the minimum timestamp ++ /// ++ /// @return the minimum timestamp ++ static const TimeStamp& MIN_TIME() { ++ static TimeStamp min_time = std::chrono::system_clock::time_point::min(); ++ return (min_time); ++ } ++ ++ /// @brief Fetches an empty parking lot handle ++ /// ++ /// @return an empty ParkingLotHandlePtr ++ static hooks::ParkingLotHandlePtr& EMPTY_LOT() { ++ static hooks::ParkingLotHandlePtr empty_lot(0); ++ return (empty_lot); ++ } ++ ++ /// @brief Fetches the IP address that is under test. ++ /// ++ /// @return IP address as an IOAddress ++ const isc::asiolink::IOAddress& getTarget() const; ++ ++ /// @brief Fetches the minimum number of ECHO REQUESTs ++ /// ++ /// @return minimum number of echos as a uint32_t ++ uint32_t getMinEchos() const; ++ ++ /// @brief Sets the minimum number of ECHO REQUESTs ++ /// ++ /// @param value new value, must be greater than 0 ++ /// ++ /// @throw BadValue if the given value is 0 ++ void setMinEchos(uint32_t value); ++ ++ /// @brief Fetches the reply timeout (milliseconds) ++ /// ++ /// @return reply timeout as a unit32_t ++ uint32_t getReplyTimeout() const; ++ ++ /// @brief Sets the reply timeout ++ /// ++ /// @param value new value in milliseconds, must be greater than 0 ++ /// ++ /// @throw BadValue if the given value is 0. ++ void setReplyTimeout(uint32_t value); ++ ++ /// @brief Fetches the number of ECHO REQUESTs sent. ++ /// ++ /// @return number of echos sent as a unit32_t ++ uint32_t getEchosSent() const; ++ ++ /// @brief Sets the number of ECHO REQUESTs sent. ++ /// ++ /// @param value new value ++ void setEchosSent(uint32_t value); ++ ++ /// @brief Fetches the timestamp of when the most recent ECHO REQUEST ++ /// was sent ++ /// ++ /// @return time the last echo was sent as a TimeStamp ++ const TimeStamp& getLastEchoSentTime() const; ++ ++ /// @brief Sets the timestamp the most recent ECHO REQUEST was sent ++ /// ++ /// @param value new value ++ void setLastEchoSentTime(const TimeStamp& value); ++ ++ /// @brief Fetches the time the context went into WAITING_TO_SEND state ++ /// ++ /// The value returned is only meaningful when the context state is WAITING_TO_SEND. ++ /// ++ /// @return send waits start time as a TimeStamp ++ const TimeStamp& getSendWaitStart() const; ++ ++ /// @brief Sets the send wait start timestamp ++ /// ++ /// @param value new value ++ void setSendWaitStart(const TimeStamp& value); ++ ++ /// @brief Returns true if state is WAITING_TO_SEND ++ /// ++ /// @return True if the context is in WAITING_TO_SEND state ++ bool isWaitingToSend() const; ++ ++ /// @brief Fetches the time at which the WAITING_FOR_REPLY state expires(ed) ++ /// ++ /// The value returned is only meaningful when the context state is WAITING_FOR_REPLY. ++ /// ++ /// @return expiration ++ const TimeStamp& getNextExpiry() const; ++ ++ /// @brief Sets the timestamp which specifies the time at which the WAITING_FOR_REPLY state expires ++ /// @param value new value ++ void setNextExpiry(const TimeStamp& value); ++ ++ /// @brief Returns true if state is WAITING_FOR_REPLY ++ /// ++ /// @return True if the context is in WAITING_TO_REPLY state ++ bool isWaitingForReply() const; ++ ++ /// @brief Fetches the time at which the context was created ++ /// ++ /// @return creation time as a TimeStamp ++ const TimeStamp& getCreatedTime() const; ++ ++ /// @brief Fetches the current state. ++ /// ++ /// @return current state as PingContext::State ++ State getState() const; ++ ++ /// @brief Sets the state. ++ /// ++ /// @param value new state value ++ void setState(const State& value); ++ ++ /// @brief Returns the query that instigated this check ++ /// ++ /// @return query as a Pkt4Ptr ++ isc::dhcp::Pkt4Ptr getQuery() const; ++ ++ /// @brief Returns the candidate lease whose address is the target to check ++ /// ++ /// @return lease under test as a Lease4Ptr ++ isc::dhcp::Lease4Ptr getLease() const; ++ ++ /// @brief Enters WAITING_TO_SEND state ++ /// ++ /// @param begin_time timestamp of when the state began. Defaults to ++ /// time now. Provided for testing purposes. ++ void beginWaitingToSend(const TimeStamp& begin_time = PingContext::now()); ++ ++ /// @brief Enters WAITING_TO_REPLY state ++ /// ++ /// @param begin_time timestamp of when the state began. Defaults to ++ /// time now. Provided for testing purposes. ++ void beginWaitingForReply(const TimeStamp& begin_time = PingContext::now()); ++ ++ /// @brief Fetches the parking lot used for this context. ++ /// ++ /// @return Pointer to the parking lot handle or empty if parking is not ++ /// employed. ++ isc::hooks::ParkingLotHandlePtr getParkingLot() { ++ return (parking_lot_); ++ }; ++ ++private: ++ /// @brief Minimum number of echos to send without receiving a reply ++ /// before giving up ++ uint32_t min_echos_ = 0; ++ ++ /// @brief Amount of time (likely in ms) to wait for an echo reply ++ uint32_t reply_timeout_ = 0; ++ ++ /// @brief Number of echos sent since instantiation ++ uint32_t echos_sent_ = 0; ++ ++ /// @brief Timestamp the most recent echo send completed ++ TimeStamp last_echo_sent_time_; ++ ++ /// @brief Timestamp of entry into waiting_to_send ++ TimeStamp send_wait_start_; ++ ++ /// @brief Timestamp the most recent echo times out ++ TimeStamp next_expiry_; ++ ++ /// @brief Time context was created ++ TimeStamp created_time_; ++ ++ /// @brief Candidate lease to check ++ isc::dhcp::Lease4Ptr lease_; ++ ++ /// @brief DHCPDISCOVER packet that instigated this check. ++ isc::dhcp::Pkt4Ptr query_; ++ ++ /// @brief Current state of this context ++ State state_; ++ ++ /// @brief Parking lot where the associated query is parked. ++ /// If empty parking is not being employed. ++ isc::hooks::ParkingLotHandlePtr parking_lot_; ++}; ++ ++/// @brief Defines a shared pointer to a PingContext. ++typedef boost::shared_ptr<PingContext> PingContextPtr; ++ ++} // end of namespace ping_check ++} // end of namespace isc ++ ++#endif +diff --git a/src/hooks/dhcp/ping_check/ping_context_store.cc b/src/hooks/dhcp/ping_check/ping_context_store.cc +new file mode 100644 +index 0000000000..35712d5afe +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/ping_context_store.cc +@@ -0,0 +1,144 @@ ++// Copyright (C) 2023-2025 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 <ping_context_store.h> ++#include <util/multi_threading_mgr.h> ++ ++using namespace std; ++using namespace isc; ++using namespace isc::asiolink; ++using namespace isc::dhcp; ++using namespace isc::hooks; ++using namespace isc::util; ++using namespace std::chrono; ++ ++namespace isc { ++namespace ping_check { ++ ++PingContextPtr ++PingContextStore::addContext(Lease4Ptr& lease, Pkt4Ptr& query, ++ uint32_t min_echos, uint32_t reply_timeout, ++ ParkingLotHandlePtr& parking_lot) { ++ ++ MultiThreadingLock lock(*mutex_); ++ PingContextPtr context; ++ try { ++ context.reset(new PingContext(lease, query, min_echos, reply_timeout, parking_lot)); ++ } catch (const std::exception& ex) { ++ isc_throw(BadValue, "PingContextStore::addContext failed: " << ex.what()); ++ } ++ ++ context->beginWaitingToSend(); ++ auto ret = pings_.insert(context); ++ if (ret.second == false) { ++ isc_throw(DuplicateContext, "PingContextStore::addContex: context already exists for: " ++ << lease->addr_); ++ } ++ ++ return (context); ++} ++ ++void ++PingContextStore::updateContext(const PingContextPtr& context) { ++ MultiThreadingLock lock(*mutex_); ++ auto& index = pings_.get<AddressIndexTag>(); ++ auto context_iter = index.find(context->getTarget()); ++ if (context_iter == index.end()) { ++ isc_throw(InvalidOperation, "PingContextStore::updateContext failed for address: " ++ << context->getTarget() << ", not in store"); ++ } ++ ++ // Use replace() to re-index contexts. ++ index.replace(context_iter, PingContextPtr(new PingContext(*context))); ++} ++ ++void ++PingContextStore::deleteContext(const PingContextPtr& context) { ++ MultiThreadingLock lock(*mutex_); ++ auto& index = pings_.get<AddressIndexTag>(); ++ auto context_iter = index.find(context->getTarget()); ++ if (context_iter == index.end()) { ++ // Not there, just return. ++ return; ++ } ++ ++ // Remove the context from the store. ++ pings_.erase(context_iter); ++} ++ ++PingContextPtr ++PingContextStore::getContextByAddress(const IOAddress& address) { ++ MultiThreadingLock lock(*mutex_); ++ auto const& index = pings_.get<AddressIndexTag>(); ++ auto context_iter = index.find(address); ++ return (context_iter == index.end() ? PingContextPtr() ++ : PingContextPtr(new PingContext(**context_iter))); ++} ++ ++PingContextPtr ++PingContextStore::getContextByQuery(Pkt4Ptr& query) { ++ MultiThreadingLock lock(*mutex_); ++ auto const& index = pings_.get<QueryIndexTag>(); ++ auto context_iter = index.find(query); ++ return (context_iter == index.end() ? PingContextPtr() ++ : PingContextPtr(new PingContext(**context_iter))); ++} ++ ++PingContextPtr ++PingContextStore::getNextToSend() { ++ MultiThreadingLock lock(*mutex_); ++ auto const& index = pings_.get<NextToSendIndexTag>(); ++ auto context_iter = index.lower_bound(boost::make_tuple(true, PingContext::MIN_TIME())); ++ return (context_iter == index.end() ? PingContextPtr() ++ : PingContextPtr(new PingContext(**context_iter))); ++} ++ ++PingContextPtr ++PingContextStore::getExpiresNext() { ++ MultiThreadingLock lock(*mutex_); ++ auto const& index = pings_.get<ExpirationIndexTag>(); ++ auto context_iter = index.lower_bound(boost::make_tuple(true, PingContext::now() + milliseconds(1))); ++ return (context_iter == index.end() ? PingContextPtr() ++ : PingContextPtr(new PingContext(**context_iter))); ++} ++ ++PingContextCollectionPtr ++PingContextStore::getExpiredSince(const TimeStamp& since) { ++ MultiThreadingLock lock(*mutex_); ++ auto const& index = pings_.get<ExpirationIndexTag>(); ++ auto lower_limit = index.lower_bound(boost::make_tuple(true, PingContext::MIN_TIME())); ++ auto upper_limit = index.upper_bound(boost::make_tuple(true, since)); ++ ++ PingContextCollectionPtr collection(new PingContextCollection()); ++ for (auto context_iter = lower_limit; context_iter != upper_limit; ++context_iter) { ++ PingContextPtr context(new PingContext(**context_iter)); ++ collection->push_back(context); ++ } ++ ++ return (collection); ++} ++ ++PingContextCollectionPtr ++PingContextStore::getAll() { ++ MultiThreadingLock lock(*mutex_); ++ auto const& index = pings_.get<AddressIndexTag>(); ++ PingContextCollectionPtr collection(new PingContextCollection()); ++ for (auto const& context_iter : index) { ++ collection->push_back(PingContextPtr(new PingContext(*context_iter))); ++ } ++ ++ return (collection); ++} ++ ++void PingContextStore::clear() { ++ MultiThreadingLock lock(*mutex_); ++ pings_.clear(); ++} ++ ++} // end of namespace ping_check ++} // end of namespace isc +diff --git a/src/hooks/dhcp/ping_check/ping_context_store.h b/src/hooks/dhcp/ping_check/ping_context_store.h +new file mode 100644 +index 0000000000..3a7664bfca +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/ping_context_store.h +@@ -0,0 +1,240 @@ ++// Copyright (C) 2023-2025 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 PING_CONTEXT_STORE_H ++#define PING_CONTEXT_STORE_H ++ ++#include <asiolink/io_address.h> ++#include <ping_context.h> ++ ++#include <boost/multi_index/indexed_by.hpp> ++#include <boost/multi_index/member.hpp> ++#include <boost/multi_index/mem_fun.hpp> ++#include <boost/multi_index/ordered_index.hpp> ++#include <boost/multi_index_container.hpp> ++#include <boost/multi_index/composite_key.hpp> ++#include <boost/scoped_ptr.hpp> ++ ++#include <mutex> ++#include <vector> ++ ++namespace isc { ++namespace ping_check { ++ ++/// @brief Exception thrown when an attempt was made to add a duplicate context ++class DuplicateContext : public Exception { ++public: ++ DuplicateContext(const char* file, size_t line, const char* what) : ++ isc::Exception(file, line, what) {} ++}; ++ ++/// @brief Tag for index by target address. ++struct AddressIndexTag { }; ++ ++/// @brief Tag for index by the query packet. ++struct QueryIndexTag { }; ++ ++/// @brief Tag for index by send wait start time. ++struct NextToSendIndexTag { }; ++ ++/// @brief Tag for index by expiration time. ++struct ExpirationIndexTag { }; ++ ++/// @brief Tag for index by state. ++struct StateIndexTag { }; ++ ++/// @brief A multi index container holding pointers to PingContexts. ++/// ++/// The contexts in the container may be accessed using different indexes: ++/// - using an IPv4 address, ++/// - using a query packet ++/// - using a send wait start time ++/// - using an expiration time ++/// - using a context state ++/// ++/// Indexes can be accessed using the index number (from 0 to 2) or a ++/// name tag. It is recommended to use the tags to access indexes as ++/// they do not depend on the order of indexes in the container. ++typedef boost::multi_index_container< ++ // It holds pointers to Lease6 objects. ++ PingContextPtr, ++ boost::multi_index::indexed_by< ++ // Specification of the first index starts here. ++ // This index sorts PingContexts by IPv4 addresses represented as ++ // IOAddress objects. ++ /// @todo Does it need to be ordered or only unique? ++ boost::multi_index::ordered_unique< ++ boost::multi_index::tag<AddressIndexTag>, ++ boost::multi_index::const_mem_fun<PingContext, const isc::asiolink::IOAddress&, ++ &PingContext::getTarget> ++ >, ++ ++ // Specification of the second index starts here. ++ // This index sorts contexts by query. ++ boost::multi_index::ordered_unique< ++ boost::multi_index::tag<QueryIndexTag>, ++ boost::multi_index::const_mem_fun<PingContext, isc::dhcp::Pkt4Ptr, ++ &PingContext::getQuery> ++ >, ++ ++ // Specification of the third index starts here. ++ // This index sorts contexts by send_wait_start. ++ boost::multi_index::ordered_non_unique< ++ boost::multi_index::tag<NextToSendIndexTag>, ++ boost::multi_index::composite_key< ++ PingContext, ++ // The boolean value specifying if context is waiting to send ++ boost::multi_index::const_mem_fun<PingContext, bool, ++ &PingContext::isWaitingToSend>, ++ // Context expiration time. ++ boost::multi_index::const_mem_fun<PingContext, const TimeStamp&, ++ &PingContext::getSendWaitStart> ++ > ++ >, ++ ++ // Specification of the fourth index starts here. ++ // This index sorts contexts by next_expiry. ++ boost::multi_index::ordered_non_unique< ++ boost::multi_index::tag<ExpirationIndexTag>, ++ boost::multi_index::composite_key< ++ PingContext, ++ // The boolean value specifying if context is waiting for a reply ++ boost::multi_index::const_mem_fun<PingContext, bool, ++ &PingContext::isWaitingForReply>, ++ // Context expiration time. ++ boost::multi_index::const_mem_fun<PingContext, const TimeStamp&, ++ &PingContext::getNextExpiry> ++ > ++ >, ++ ++ // Specification of the fifth index starts here. ++ // This index sorts contexts by State. ++ boost::multi_index::ordered_non_unique< ++ boost::multi_index::tag<StateIndexTag>, ++ boost::multi_index::const_mem_fun<PingContext, PingContext::State, ++ &PingContext::getState> ++ > ++ > ++> PingContextContainer; ++ ++/// @brief Type for a collection of PingContextPtrs. ++typedef std::vector<PingContextPtr> PingContextCollection; ++/// @brief Type for a pointer to a collection of PingContextPtrs. ++typedef boost::shared_ptr<PingContextCollection> PingContextCollectionPtr; ++ ++/// @brief Maintains an in-memory store of PingContexts ++/// ++/// Provides essential CRUD functions for managing a collection of ++/// PingContexts. Additionally there are finders that can return ++/// contexts by target IP address, instigating query, WAITING_TO_SEND ++/// start time, WAITING_FOR_REPLY expiration time, and context state. ++/// All finders return copies of the contexts found, rather than the ++/// stored context itself. ++class PingContextStore { ++public: ++ ++ /// @brief Constructor ++ PingContextStore() : pings_(), mutex_(new std::mutex) { ++ } ++ ++ /// @brief Destructor ++ ~PingContextStore() = default; ++ ++ /// @brief Creates a new PingContext and adds it to the store ++ /// ++ /// @param lease lease whose address is to be ping checked ++ /// @param query query that instigated the lease ++ /// @param min_echos minimum number of ECHO REQUESTs sent without replies ++ /// received required to declare an address free to offer. Must be ++ /// greater than zero. ++ /// @param reply_timeout maximum number of milliseconds to wait for an ++ /// ECHO REPLY after an ECHO REQUEST has been sent. Must be greater than 0. ++ /// @param parking_lot parking lot in which query is parked. If empty, ++ /// parking is assumed to not be employed. ++ /// ++ /// @return pointer to the newly created context ++ /// @throw DuplicateContext is a context for the lease address already ++ /// exists in the store. ++ PingContextPtr addContext(isc::dhcp::Lease4Ptr& lease, ++ isc::dhcp::Pkt4Ptr& query, ++ uint32_t min_echos, ++ uint32_t reply_timeout, ++ isc::hooks::ParkingLotHandlePtr& parking_lot ++ = PingContext::EMPTY_LOT()); ++ ++ /// @brief Updates a context in the store. ++ /// ++ /// The context is assumed to already exist in the store. ++ /// ++ /// @param context context to update. ++ /// ++ /// @throw InvalidOperation if PingContext does not exist in the store. ++ void updateContext(const PingContextPtr& context); ++ ++ /// @brief Removes the context from the store. ++ /// ++ /// If the context does not exist in the store, it simply returns. ++ /// ++ /// @param context context to delete. ++ void deleteContext(const PingContextPtr& context); ++ ++ /// @brief Fetches the context with a given target address ++ /// ++ /// @param address target IP address for which to search ++ /// ++ /// @return pointer to the matching PingContext or an empty pointer if ++ /// not found. ++ PingContextPtr getContextByAddress(const isc::asiolink::IOAddress& address); ++ ++ /// @brief Fetches the context with a given query packet ++ /// ++ /// @param query query for which to search ++ /// ++ /// @return pointer to the matching PingContext or an empty pointer if ++ /// not found. ++ PingContextPtr getContextByQuery(isc::dhcp::Pkt4Ptr& query); ++ ++ /// @brief Fetches the context in WAITING_TO_SEND with the oldest send wait ++ /// start time. ++ /// ++ /// @return pointer to the matching PingContext or an empty pointer if ++ /// not found. ++ PingContextPtr getNextToSend(); ++ ++ /// @brief Fetches the context in WAITING_FOR_REPLY with the oldest expiration ++ /// time that has not already passed (i.e. is still in the future) ++ /// ++ /// @return pointer to the matching PingContext or an empty pointer if ++ /// not found. ++ PingContextPtr getExpiresNext(); ++ ++ /// @brief Fetches the contexts in WAITING_FOR_REPLY that expired since a given time ++ /// ++ /// @param since timestamp to search by. Defaults to current time. ++ /// ++ /// @return a collection of the matching contexts, ordered by expiration time. ++ PingContextCollectionPtr getExpiredSince(const TimeStamp& since = PingContext::now()); ++ ++ /// @brief Fetches all of the contexts (in order by target) ++ /// ++ /// @return a collection of all contexts in the store. ++ PingContextCollectionPtr getAll(); ++ ++ /// @brief Removes all contexts from the store. ++ void clear(); ++ ++private: ++ /// @brief Container instance. ++ PingContextContainer pings_; ++ ++ /// @brief The mutex used to protect internal state. ++ const boost::scoped_ptr<std::mutex> mutex_; ++}; ++ ++} // end of namespace ping_check ++} // end of namespace isc ++ ++#endif +diff --git a/src/hooks/dhcp/ping_check/tests/.gitignore b/src/hooks/dhcp/ping_check/tests/.gitignore +new file mode 100644 +index 0000000000..7e12f9e5be +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/tests/.gitignore +@@ -0,0 +1 @@ ++ping_check_unittests +diff --git a/src/hooks/dhcp/ping_check/tests/Makefile.am b/src/hooks/dhcp/ping_check/tests/Makefile.am +new file mode 100644 +index 0000000000..a8c2ea4d92 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/tests/Makefile.am +@@ -0,0 +1,70 @@ ++SUBDIRS = . ++ ++AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib ++AM_CPPFLAGS += -I$(top_builddir)/src/hooks/dhcp/ping_check -I$(top_srcdir)/src/hooks/dhcp/ping_check ++AM_CPPFLAGS += $(BOOST_INCLUDES) $(CRYPTO_CFLAGS) $(CRYPTO_INCLUDES) ++AM_CPPFLAGS += -DPING_CHECK_LIB_SO=\"$(abs_top_builddir)/src/hooks/dhcp/ping_check/.libs/libdhcp_ping_check.so\" ++AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\" ++ ++AM_CXXFLAGS = $(KEA_CXXFLAGS) ++ ++if USE_STATIC_LINK ++AM_LDFLAGS = -static ++endif ++ ++# Unit test data files need to get installed. ++EXTRA_DIST = ++ ++CLEANFILES = *.gcno *.gcda ++ ++TESTS_ENVIRONMENT = $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND) ++ ++LOG_COMPILER = $(LIBTOOL) ++AM_LOG_FLAGS = --mode=execute ++ ++TESTS = ++if HAVE_GTEST ++TESTS += ping_check_unittests ++ ++ping_check_unittests_SOURCES = run_unittests.cc ++ping_check_unittests_SOURCES += icmp_endpoint_unittests.cc ++ping_check_unittests_SOURCES += icmp_socket_unittests.cc ++ping_check_unittests_SOURCES += ping_context_unittests.cc ++ping_check_unittests_SOURCES += ping_context_store_unittests.cc ++ping_check_unittests_SOURCES += icmp_msg_unittests.cc ++ping_check_unittests_SOURCES += ping_test_utils.h ++ping_check_unittests_SOURCES += ping_channel_unittests.cc ++ping_check_unittests_SOURCES += ping_check_mgr_unittests.cc ++ping_check_unittests_SOURCES += ping_check_config_unittests.cc ++ping_check_unittests_SOURCES += config_cache_unittests.cc ++ ++ping_check_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) $(LOG4CPLUS_INCLUDES) ++ ++ping_check_unittests_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS) $(GTEST_LDFLAGS) ++ ++ping_check_unittests_CXXFLAGS = $(AM_CXXFLAGS) ++ ++ping_check_unittests_LDADD = $(top_builddir)/src/hooks/dhcp/ping_check/libping_check.la ++ping_check_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la ++ping_check_unittests_LDADD += $(top_builddir)/src/lib/process/libkea-process.la ++ping_check_unittests_LDADD += $(top_builddir)/src/lib/eval/libkea-eval.la ++ping_check_unittests_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la ++ping_check_unittests_LDADD += $(top_builddir)/src/lib/stats/libkea-stats.la ++ping_check_unittests_LDADD += $(top_builddir)/src/lib/config/libkea-cfgclient.la ++ping_check_unittests_LDADD += $(top_builddir)/src/lib/http/libkea-http.la ++ping_check_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la ++ping_check_unittests_LDADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la ++ping_check_unittests_LDADD += $(top_builddir)/src/lib/database/libkea-database.la ++ping_check_unittests_LDADD += $(top_builddir)/src/lib/cc/libkea-cc.la ++ping_check_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la ++ping_check_unittests_LDADD += $(top_builddir)/src/lib/dns/libkea-dns++.la ++ping_check_unittests_LDADD += $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la ++ping_check_unittests_LDADD += $(top_builddir)/src/lib/log/libkea-log.la ++ping_check_unittests_LDADD += $(top_builddir)/src/lib/util/libkea-util.la ++ping_check_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la ++ping_check_unittests_LDADD += $(LOG4CPLUS_LIBS) ++ping_check_unittests_LDADD += $(CRYPTO_LIBS) ++ping_check_unittests_LDADD += $(BOOST_LIBS) ++ping_check_unittests_LDADD += $(GTEST_LDADD) ++endif ++noinst_PROGRAMS = $(TESTS) +diff --git a/src/hooks/dhcp/ping_check/tests/config_cache_unittests.cc b/src/hooks/dhcp/ping_check/tests/config_cache_unittests.cc +new file mode 100644 +index 0000000000..f4e48d6591 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/tests/config_cache_unittests.cc +@@ -0,0 +1,245 @@ ++// Copyright (C) 2023-2025 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 This file contains tests which verify the subnet ping-check ++/// configuration cache. ++ ++#include <config.h> ++#include <config_cache.h> ++#include <dhcpsrv/cfgmgr.h> ++#include <hooks/callout_manager.h> ++#include <hooks/hooks.h> ++#include <testutils/gtest_utils.h> ++#include <testutils/multi_threading_utils.h> ++ ++#include <boost/date_time/posix_time/posix_time.hpp> ++#include <gtest/gtest.h> ++#include <sstream> ++ ++using namespace std; ++using namespace isc; ++using namespace isc::data; ++using namespace isc::dhcp; ++using namespace isc::hooks; ++using namespace isc::ping_check; ++using namespace isc::test; ++using namespace boost::posix_time; ++ ++namespace { ++ ++/// @brief ConfigCache derivation that allows flush time to be modified. ++class TestConfigCache : public ConfigCache { ++public: ++ /// @brief Constructor ++ TestConfigCache() { ++ } ++ ++ /// @brief Destructor ++ virtual ~TestConfigCache() { ++ } ++ ++ /// @brief Adjusts the last flush time by the given amount. ++ /// ++ /// @param offset signed value in seconds to add to cache's last ++ /// flush time value ++ void tweakLastFlushTime(int offset) { ++ setModificationTime(getLastFlushTime() + seconds(offset)); ++ } ++}; ++ ++/// @brief Test fixture for testing ConfigCache. ++class ConfigCacheTest : public ::testing::Test { ++public: ++ /// @brief Constructor ++ ConfigCacheTest() { ++ isc::util::MultiThreadingMgr::instance().setMode(false); ++ } ++ ++ /// @brief Destructor ++ virtual ~ConfigCacheTest() { ++ } ++ ++ /// @brief Verifies construction of a ConfigCache. ++ void testConstruction() { ++ // We use a BaseStampedElement to get the current time to ensure we ++ // are using the same time perspective (currently local) as StampedElements do. ++ BaseStampedElement now; ++ ptime start_time = now.getModificationTime(); ++ ++ // Create a new cache. ++ TestConfigCache configs; ++ EXPECT_EQ(configs.size(), 0); ++ ++ // Verify that last_flush_time_ has been set and that the ++ // cache has no entries. ++ ptime last_flush_time = configs.getLastFlushTime(); ++ EXPECT_GE(last_flush_time, start_time); ++ ++ // Verify that looking for an entry in an empty cache ++ // gracefully finds nothing. ++ PingCheckConfigPtr fetched_config; ++ EXPECT_FALSE(configs.findConfig(999, fetched_config)); ++ EXPECT_FALSE(fetched_config); ++ } ++ ++ /// @brief Verifies that invalid user-context config is rejected gracefully. ++ void testInvalidConfig() { ++ // Create a new cache. ++ TestConfigCache configs; ++ EXPECT_EQ(configs.size(), 0); ++ ++ // An invalid keyword should fail. ++ std::string json = ++ R"({ ++ "ping-check" : { ++ "bogus" : 777 ++ } ++ })"; ++ ++ ConstElementPtr user_context; ++ ASSERT_NO_THROW_LOG(user_context = Element::fromJSON(json)); ++ ++ ASSERT_THROW_MSG(configs.parseAndCacheConfig(1, user_context), DhcpConfigError, ++ "spurious 'bogus' parameter"); ++ ++ EXPECT_EQ(configs.size(), 0); ++ } ++ ++ /// @brief Verifies that valid user-context supplied config are cached correctly. ++ void testValidConfig() { ++ // Create a new cache. ++ TestConfigCache configs; ++ EXPECT_EQ(configs.size(), 0); ++ ++ // A valid config should get cached. ++ std::string json = ++ R"({ ++ "ping-check" : { ++ "enable-ping-check" : false, ++ "min-ping-requests" : 2, ++ "reply-timeout" : 375, ++ "ping-cltt-secs" : 120, ++ "ping-channel-threads" : 6 ++ } ++ })"; ++ ++ ConstElementPtr user_context; ++ ASSERT_NO_THROW_LOG(user_context = Element::fromJSON(json)); ++ ++ // Verify that we cache a valid config. ++ PingCheckConfigPtr config; ++ ASSERT_NO_THROW_LOG(config = configs.parseAndCacheConfig(1, user_context)); ++ ASSERT_TRUE(config); ++ EXPECT_EQ(configs.size(), 1); ++ ++ // Verify we can retrieve the cached config. ++ PingCheckConfigPtr fetched_config; ++ ASSERT_TRUE(configs.findConfig(1, fetched_config)); ++ EXPECT_EQ(fetched_config, config); ++ } ++ ++ /// @brief Verifies that an empty config pointer can be cached. ++ void testConfigCacheEmptyConfig() { ++ // Create a new cache. ++ TestConfigCache configs; ++ EXPECT_EQ(configs.size(), 0); ++ ++ // Verify that we can cache an empty config pointer. ++ PingCheckConfigPtr no_config; ++ ASSERT_NO_THROW_LOG(configs.cacheConfig(1, no_config)); ++ EXPECT_EQ(configs.size(), 1); ++ ++ // Verify we can retrieve the cached empty config pointer. ++ PingCheckConfigPtr fetched_config; ++ ASSERT_TRUE(configs.findConfig(1, fetched_config)); ++ ASSERT_FALSE(fetched_config); ++ } ++ ++ /// @brief Verifies that the cache can be cleared correctly. ++ void testFlushCache() { ++ // Create a new cache. ++ TestConfigCache configs; ++ EXPECT_EQ(configs.size(), 0); ++ ++ ptime last_flush_time = configs.getLastFlushTime(); ++ ++ // Now let's wind the clock back on last_flush_time. ++ configs.tweakLastFlushTime(-1000); ++ EXPECT_LT(configs.getLastFlushTime(), last_flush_time); ++ last_flush_time = configs.getLastFlushTime(); ++ ++ // Make a simple valid config. ++ std::string json = ++ R"({ ++ "ping-check": { ++ "enable-ping-check" : true ++ } ++ })"; ++ ++ ConstElementPtr user_context; ++ ASSERT_NO_THROW_LOG(user_context = Element::fromJSON(json)); ++ ++ for (int id = 1; id < 5; ++id) { ++ PingCheckConfigPtr config; ++ ASSERT_NO_THROW_LOG(config = configs.parseAndCacheConfig(id, user_context)); ++ ASSERT_TRUE(config); ++ EXPECT_EQ(configs.size(), id); ++ } ++ ++ // Verify we can explicitly clear the cache. Should be no entries ++ // and last_flush_time should be updated. ++ configs.flush(); ++ EXPECT_GT(configs.getLastFlushTime(), last_flush_time); ++ EXPECT_EQ(configs.size(), 0); ++ } ++}; ++ ++TEST_F(ConfigCacheTest, construction) { ++ testConstruction(); ++} ++ ++TEST_F(ConfigCacheTest, constructionMultiThreading) { ++ MultiThreadingTest mt; ++ testConstruction(); ++} ++ ++TEST_F(ConfigCacheTest, invalidConfig) { ++ testInvalidConfig(); ++} ++ ++TEST_F(ConfigCacheTest, invalidConfigMultiThreading) { ++ MultiThreadingTest mt; ++ testInvalidConfig(); ++} ++ ++TEST_F(ConfigCacheTest, validConfig) { ++ testValidConfig(); ++} ++ ++TEST_F(ConfigCacheTest, validConfigMultiThreading) { ++ MultiThreadingTest mt; ++ testValidConfig(); ++} ++ ++TEST_F(ConfigCacheTest, configCacheEmptyConfig) { ++ testConfigCacheEmptyConfig(); ++} ++ ++TEST_F(ConfigCacheTest, configCacheEmptyConfigMultiThreading) { ++ MultiThreadingTest mt; ++ testConfigCacheEmptyConfig(); ++} ++ ++TEST_F(ConfigCacheTest, flushCache) { ++ testFlushCache(); ++} ++ ++TEST_F(ConfigCacheTest, flushCacheMultiThreading) { ++ MultiThreadingTest mt; ++ testFlushCache(); ++} ++ ++} // end of anonymous namespace +diff --git a/src/hooks/dhcp/ping_check/tests/icmp_endpoint_unittests.cc b/src/hooks/dhcp/ping_check/tests/icmp_endpoint_unittests.cc +new file mode 100644 +index 0000000000..e9ed8dcb9b +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/tests/icmp_endpoint_unittests.cc +@@ -0,0 +1,44 @@ ++// Copyright (C) 2023-2025 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/asio_wrapper.h> ++#include <asiolink/io_address.h> ++#include <icmp_endpoint.h> ++ ++#include <gtest/gtest.h> ++ ++#include <string> ++ ++using namespace isc::asiolink; ++using namespace isc::ping_check; ++using namespace std; ++ ++// This test checks that the endpoint can manage its own internal ++// boost::asio::ip::icmp::endpoint object for IPv4. ++TEST(ICMPEndpointTest, v4Address) { ++ const string test_address("192.0.2.1"); ++ ++ IOAddress address(test_address); ++ ICMPEndpoint endpoint(address); ++ ++ EXPECT_TRUE(address == endpoint.getAddress()); ++ EXPECT_EQ(static_cast<short>(IPPROTO_ICMP), endpoint.getProtocol()); ++ EXPECT_EQ(AF_INET, endpoint.getFamily()); ++} ++ ++// This test checks that the endpoint can manage its own internal ++// boost::asio::ip::icmp::endpoint object for IPv6. ++TEST(ICMPEndpointTest, v6Address) { ++ const string test_address("2001:db8::1235"); ++ ++ IOAddress address(test_address); ++ ICMPEndpoint endpoint(address); ++ ++ EXPECT_TRUE(address == endpoint.getAddress()); ++ EXPECT_EQ(static_cast<short>(IPPROTO_ICMPV6), endpoint.getProtocol()); ++ EXPECT_EQ(AF_INET6, endpoint.getFamily()); ++} +diff --git a/src/hooks/dhcp/ping_check/tests/icmp_msg_unittests.cc b/src/hooks/dhcp/ping_check/tests/icmp_msg_unittests.cc +new file mode 100644 +index 0000000000..36c7056840 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/tests/icmp_msg_unittests.cc +@@ -0,0 +1,172 @@ ++// Copyright (C) 2023-2025 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 This file contains tests which exercise the ICMPMsg class. ++ ++#include <config.h> ++#include <icmp_msg.h> ++#include <asiolink/io_address.h> ++#include <testutils/gtest_utils.h> ++#include <util/str.h> ++ ++#include <gtest/gtest.h> ++#include <list> ++ ++using namespace std; ++using namespace isc; ++using namespace isc::asiolink; ++using namespace isc::ping_check; ++ ++namespace { ++ ++// Verifies accessors. ++TEST(ICMPMsgTest, basics) { ++ ICMPMsgPtr msg(new ICMPMsg()); ++ ++ msg->setType(ICMPMsg::ECHO_REPLY); ++ EXPECT_EQ(ICMPMsg::ECHO_REPLY, msg->getType()); ++ ++ msg->setCode(77); ++ EXPECT_EQ(77, msg->getCode()); ++ ++ msg->setChecksum(0x8899); ++ EXPECT_EQ(0x8899, msg->getChecksum()); ++ ++ msg->setId(0x1122); ++ EXPECT_EQ(0x1122, msg->getId()); ++ ++ msg->setSequence(0x3344); ++ EXPECT_EQ(0x3344, msg->getSequence()); ++ ++ msg->setSource(IOAddress("192.0.2.1")); ++ EXPECT_EQ(IOAddress("192.0.2.1"), msg->getSource()); ++ ++ msg->setDestination(IOAddress("192.0.2.2")); ++ EXPECT_EQ(IOAddress("192.0.2.2"), msg->getDestination()); ++ ++ std::vector<uint8_t> payload{ 0x55, 0x66, 0x77, 0x88, 0x99 }; ++ msg->setPayload(payload.data(), payload.size()); ++ EXPECT_EQ(payload, msg->getPayload()); ++} ++ ++// Verifies that a valid ECHO REPLY message can be unpacked. ++TEST(ICMPMsgTest, unpackValidEchoReply) { ++ // Create wire data for a valid ECHO REPLY. ++ std::string echo_reply = ++ "45:00:00:30:73:8a:00:00:40:01:a0:ff:b2:10:01:19:b2:10:01:0a:" ++ "00:00:33:11:55:66:77:88:" ++ "00:00:00:00:00:00:00:00:" ++ "00:00:00:00:00:00:00:00:" ++ "00:00:00:00"; ++ ++ std::vector<uint8_t> wire_data; ++ ASSERT_NO_THROW_LOG(util::str::decodeSeparatedHexString(echo_reply, ":", wire_data)); ++ ++ // Unpack the wire data. ++ ICMPMsgPtr msg; ++ ASSERT_NO_THROW_LOG(msg = ICMPMsg::unpack(wire_data.data(), wire_data.size())); ++ ASSERT_TRUE(msg); ++ ++ // Verify the reply contents. ++ EXPECT_EQ(ICMPMsg::ECHO_REPLY, msg->getType()); ++ EXPECT_EQ(0, msg->getCode()); ++ EXPECT_EQ(0x3311, msg->getChecksum()); ++ EXPECT_EQ(0x5566, msg->getId()); ++ EXPECT_EQ(0x7788, msg->getSequence()); ++ EXPECT_EQ(IOAddress("178.16.1.25"), msg->getSource()); ++ EXPECT_EQ(IOAddress("178.16.1.10"), msg->getDestination()); ++ ++ std::vector<uint8_t> payload{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; ++ EXPECT_EQ(payload, msg->getPayload()); ++} ++ ++// Verifies that a valid DESTINATION UNREACHABLE message can be unpacked. ++TEST(ICMPMsgTest, unpackValidUnreachable) { ++ // Valid destination unreachable message. Payload is the original ++ // ECHO request. ++ std::string unreachable = ++ "45:c0:00:4c:31:b3:00:00:40:01:e2:09:b2:10:01:0a:b2:10:01:0a:" ++ "03:01:fc:fe:00:00:00:00:" ++ "45:00:00:30:e3:e2:40:00:40:01:f0:5c:" ++ "b2:10:01:0a:b2:10:01:63:08:00:2b:11:" ++ "55:66:77:88:00:00:00:00:00:00:00:00:" ++ "00:00:00:00:00:00:00:00:00:00:00:00"; ++ ++ // Create the wire data. ++ std::vector<uint8_t> wire_data; ++ ASSERT_NO_THROW_LOG(util::str::decodeSeparatedHexString(unreachable, ":", wire_data)); ++ ++ // Unpack the outer message. ++ ICMPMsgPtr msg; ++ ASSERT_NO_THROW_LOG(msg = ICMPMsg::unpack(wire_data.data(), wire_data.size())); ++ ASSERT_TRUE(msg); ++ ++ // Verify its contents. ++ EXPECT_EQ(ICMPMsg::TARGET_UNREACHABLE, msg->getType()); ++ EXPECT_EQ(1, msg->getCode()); ++ EXPECT_EQ(0xfcfe, msg->getChecksum()); ++ EXPECT_EQ(0, msg->getId()); ++ EXPECT_EQ(0, msg->getSequence()); ++ EXPECT_EQ(IOAddress("178.16.1.10"), msg->getSource()); ++ EXPECT_EQ(IOAddress("178.16.1.10"), msg->getDestination()); ++ ++ // Now unpack the original ECHO from the outer message payload. ++ std::vector<uint8_t> payload(wire_data.begin() + 28, wire_data.end()); ++ EXPECT_EQ(payload, msg->getPayload()); ++ ++ ICMPMsgPtr payload_msg; ++ ASSERT_NO_THROW_LOG(payload_msg = ICMPMsg::unpack(payload.data(), payload.size())); ++ ASSERT_TRUE(payload_msg); ++ ++ // Verify the original ECHO contents. ++ EXPECT_EQ(ICMPMsg::ECHO_REQUEST, payload_msg->getType()); ++ EXPECT_EQ(0, payload_msg->getCode()); ++ EXPECT_EQ(0x2b11, payload_msg->getChecksum()); ++ EXPECT_EQ(0x5566, payload_msg->getId()); ++ EXPECT_EQ(0x7788, payload_msg->getSequence()); ++ EXPECT_EQ(IOAddress("178.16.1.10"), payload_msg->getSource()); ++ EXPECT_EQ(IOAddress("178.16.1.99"), payload_msg->getDestination()); ++} ++ ++// Verifies the malformed packets are detected. ++TEST(ICMPMsgTest, unpackInValidPackets) { ++ // Contains a test scenario. ++ struct Scenario { ++ // Wire data to submit to unpack. ++ std::string wire_data_; ++ // Expected exception message. ++ std::string error_msg_; ++ }; ++ ++ // List of scenarios to test. ++ std::list<Scenario> scenarios = { ++ { ++ // Truncated IP header ++ "45:c0:00:4c:31:b3:00:00:40:01:e2:09:b2", ++ "ICMPMsg::unpack - truncated ip header, length: 13" ++ }, ++ { ++ // Truncated packet ++ "45:c0:00:4c:31:b3:00:00:40:01:e2:09:b2:10:01:0a:b2:10:01:0a:" ++ "03:01:fc:fe:00:00:00:00:" ++ "45:00:00:30:e3:e2:40:00:40:01:f0:5c", ++ "ICMPMsg::truncated packet? length: 40, hlen: 20" ++ } ++ ++ }; ++ ++ // Iterate over scenarios. ++ for (auto const& scenario : scenarios) { ++ // Create the wire data. ++ std::vector<uint8_t> wire_data; ++ ASSERT_NO_THROW_LOG(util::str::decodeSeparatedHexString(scenario.wire_data_, ":", wire_data)); ++ ASSERT_THROW_MSG(ICMPMsg::unpack(wire_data.data(), wire_data.size()), BadValue, scenario.error_msg_); ++ } ++} ++ ++/// @todo YOU NEED some round trip tests that test packing! ++ ++} // end of anonymous namespace +diff --git a/src/hooks/dhcp/ping_check/tests/icmp_socket_unittests.cc b/src/hooks/dhcp/ping_check/tests/icmp_socket_unittests.cc +new file mode 100644 +index 0000000000..2394b360ca +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/tests/icmp_socket_unittests.cc +@@ -0,0 +1,380 @@ ++// Copyright (C) 2023-2025 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 Test of ICMPSocket ++/// ++/// Tests the functionality of a ICMPSocket by working through an open-send- ++/// receive-close sequence and checking that the asynchronous notifications ++/// work. ++ ++#include <config.h> ++#include <asiolink/asio_wrapper.h> ++#include <asiolink/interval_timer.h> ++#include <asiolink/io_address.h> ++#include <asiolink/io_service.h> ++#include <icmp_socket.h> ++#include <icmp_msg.h> ++#include <exceptions/exceptions.h> ++#include <util/buffer.h> ++#include <testutils/gtest_utils.h> ++ ++#include <boost/shared_ptr.hpp> ++#include <boost/enable_shared_from_this.hpp> ++#include <boost/date_time/posix_time/posix_time.hpp> ++#include <gtest/gtest.h> ++ ++#include <string> ++#include <arpa/inet.h> ++#include <netinet/in.h> ++#include <sys/types.h> ++#include <sys/socket.h> ++#include <algorithm> ++#include <cstdlib> ++#include <cstddef> ++#include <list> ++#include <vector> ++#include <unistd.h> ++ ++#include <netinet/ip.h> ++#include <netinet/ip_icmp.h> ++ ++using namespace isc; ++using namespace boost::asio; ++using namespace boost::posix_time; ++using namespace isc::asiolink; ++using namespace isc::ping_check; ++using namespace isc::util; ++using namespace std; ++ ++namespace ph = std::placeholders; ++ ++namespace { ++ ++/// @brief Test timeout (ms). ++const long TEST_TIMEOUT = 10000; ++ ++/// @brief Type of the function implementing a callback invoked by the ++/// @c SocketCallback functor. ++typedef std::function<void(boost::system::error_code ec, size_t length)> ++ SocketCallbackFunction; ++ ++/// @brief Callback class for socket IO operations ++/// ++/// An instance of this object is passed to the asynchronous I/O functions ++/// and the operator() method is called when when an asynchronous I/O ++/// completes. The arguments to the completion callback are stored for later ++/// retrieval. ++class SocketCallback { ++public: ++ ++ /// @brief Structure that houses callback invocation data. ++ struct PrivateData { ++ PrivateData() : ++ error_code_(), length_(0), called_(false), name_("") ++ {} ++ ++ boost::system::error_code error_code_; ///< Completion error code ++ size_t length_; ///< Number of bytes transferred ++ bool called_; ///< Set true when callback called ++ std::string name_; ///< Which of the objects this is ++ }; ++ ++ /// @brief Constructor ++ /// ++ /// Constructs the object. It also creates the data member pointed to by ++ /// a shared pointer. When used as a callback object, this is copied as it ++ /// is passed into the asynchronous function. This means that there are two ++ /// objects and inspecting the one we passed in does not tell us anything. ++ /// ++ /// Therefore we use a boost::shared_ptr. When the object is copied, the ++ /// shared pointer is copied, which leaves both objects pointing to the same ++ /// data. ++ /// ++ /// @param which Which of the two callback objects this is ++ explicit SocketCallback(const std::string& which) : data_(new PrivateData()) ++ { ++ setName(which); ++ } ++ ++ /// @brief Destructor ++ /// ++ /// No code needed, destroying the shared pointer destroys the private data. ++ virtual ~SocketCallback() ++ {} ++ ++ /// @brief Clears the current values of invocation data members. ++ void clear() { ++ setCode(0); ++ setLength(0); ++ setCalled(false); ++ } ++ ++ /// @brief Callback Function ++ /// ++ /// Called when an asynchronous I/O completes, this stores the ++ /// completion error code and the number of bytes transferred. ++ /// ++ /// @param ec I/O completion error code passed to callback function. ++ /// @param length Number of bytes transferred ++ virtual void operator()(boost::system::error_code ec, size_t length = 0) { ++ data_->error_code_ = ec; ++ setLength(length); ++ setCalled(true); ++ } ++ ++ /// @brief Get I/O completion error code ++ int getCode() { ++ return (data_->error_code_.value()); ++ } ++ ++ /// @brief Set I/O completion code ++ /// ++ /// @param code New value of completion code ++ void setCode(int code) { ++ data_->error_code_ = boost::system::error_code(code, boost::system::error_code().category()); ++ } ++ ++ /// @brief Get number of bytes transferred in I/O ++ size_t getLength() const { ++ return (data_->length_); ++ } ++ ++ /// @brief Set number of bytes transferred in I/O ++ /// ++ /// @param length New value of length parameter ++ void setLength(size_t length) { ++ data_->length_ = length; ++ } ++ ++ /// @brief Get flag to say when callback was called ++ bool getCalled() const { ++ return (data_->called_); ++ } ++ ++ /// @brief Set flag to say when callback was called ++ /// ++ /// @param called New value of called parameter ++ void setCalled(bool called) { ++ data_->called_ = called; ++ } ++ ++ /// @brief Return instance of callback name ++ std::string getName() const { ++ return (data_->name_); ++ } ++ ++ /// @brief Set callback name ++ /// ++ /// @param name New value of the callback name ++ void setName(const std::string& name) { ++ data_->name_ = name; ++ } ++ ++private: ++ boost::shared_ptr<PrivateData> data_; ///< Pointer to private data ++}; ++ ++/// @brief Socket and pointer types for sending and receiving ICMP echos. ++typedef ICMPSocket<SocketCallback> PingSocket; ++typedef boost::shared_ptr<PingSocket> PingSocketPtr; ++ ++/// @brief Simple test fixture for testing ICMPSocket. ++class ICMPSocketTest : public ::testing::Test { ++public: ++ /// @brief Constructor. ++ ICMPSocketTest() ++ : io_service_(new IOService()), test_timer_(io_service_) { ++ test_timer_.setup(std::bind(&ICMPSocketTest::timeoutHandler, this, true), ++ TEST_TIMEOUT, IntervalTimer::ONE_SHOT); ++ } ++ ++ /// @brief Destructor. ++ virtual ~ICMPSocketTest() { ++ test_timer_.cancel(); ++ io_service_->stopAndPoll(); ++ } ++ ++ /// @brief Indicates if current user is not root ++ /// ++ /// @return True if neither the uid or the effective ++ /// uid is root. ++ static bool notRoot() { ++ return (getuid() != 0 && geteuid() != 0); ++ } ++ ++ /// @brief Callback function invoke upon test timeout. ++ /// ++ /// It stops the IO service and reports test timeout. ++ /// ++ /// @param fail_on_timeout Specifies if test failure should be reported. ++ void timeoutHandler(const bool fail_on_timeout) { ++ if (fail_on_timeout) { ++ ADD_FAILURE() << "Timeout occurred while running the test!"; ++ } ++ io_service_->stop(); ++ } ++ ++ /// @brief IOService instance used by thread pools. ++ IOServicePtr io_service_; ++ ++ /// @brief Asynchronous timer service to detect timeouts. ++ IntervalTimer test_timer_; ++ ++ /// @brief Returns pointer to the first byte of the input buffer. ++ /// ++ /// @throw InvalidOperation if called when the buffer is empty. ++ uint8_t* getInputBufData() { ++ if (input_buf_.empty()) { ++ isc_throw(InvalidOperation, "TcpConnection::getInputBufData() - cannot access empty buffer"); ++ } ++ ++ return (input_buf_.data()); ++ } ++ ++ /// @brief Returns input buffer size. ++ size_t getInputBufSize() const { ++ return (input_buf_.size()); ++ } ++ ++ /// @brief Set the capacity of the input buffer ++ /// ++ /// @param buf_size maximum number of bytes allowed in the buffer ++ void resizeInputBuf(size_t buf_size) { ++ input_buf_.resize(buf_size); ++ } ++ ++ /// @brief Buffer for a single socket read. ++ std::vector<uint8_t> input_buf_; ++}; ++ ++ ++// Verifies that an ICMP socket can be opened and closed. ++TEST_F(ICMPSocketTest, openClose) { ++ SKIP_IF(notRoot()); ++ ++ // For open the endpoint is only used to determine protocol, the address is irrelevant. ++ ICMPEndpoint ping_to_endpoint(IOAddress::IPV4_ZERO_ADDRESS()); ++ ++ PingSocket socket(io_service_); ++ SocketCallback socket_cb("open"); ++ ++ // Verify the socket is closed. ++ ASSERT_FALSE(socket.isOpen()); ++ ++ // Open the socket. ++ ASSERT_NO_THROW_LOG(socket.open(&ping_to_endpoint, socket_cb)); ++ ++ // Verify the socket is open. ++ ASSERT_TRUE(socket.isOpen()); ++ // Since open() is synchronous the callback should not have been invoked. ++ ASSERT_FALSE(socket_cb.getCalled()); ++ ++ // Opening an already open should be harmless. ++ ASSERT_NO_THROW_LOG(socket.open(&ping_to_endpoint, socket_cb)); ++ ASSERT_TRUE(socket.isOpen()); ++ ++ // Close the socket. ++ ASSERT_NO_THROW_LOG(socket.close()); ++ ASSERT_FALSE(socket.isOpen()); ++ ++ // Closing a closed socket should be harmless. ++ ASSERT_NO_THROW_LOG(socket.close()); ++ ASSERT_FALSE(socket.isOpen()); ++} ++ ++// Verifies that an ICMP socket can send and receive ICMP messages. ++TEST_F(ICMPSocketTest, sendReceive) { ++ SKIP_IF(notRoot()); ++ ++ PingSocket socket(io_service_); ++ ++ // For open the endpoint is only used to determine protocol, the address is irrelevant. ++ ICMPEndpoint endpoint(IOAddress::IPV4_ZERO_ADDRESS()); ++ ++ // Open the socket. ++ SocketCallback open_cb("open"); ++ ASSERT_NO_THROW_LOG(socket.open(&endpoint, open_cb)); ++ ++ // Build a ping. ++ struct icmp echo; ++ memset(&echo, 0, sizeof(echo)); ++ echo.icmp_type = ICMPMsg::ECHO_REQUEST; ++ echo.icmp_id = htons(0x1122); ++ echo.icmp_seq = htons(0x3344); ++ echo.icmp_cksum = htons(~(socket.calcChecksum((const uint8_t*)&echo, sizeof(echo)))); ++ ++ // Send it to the loopback. ++ IOAddress ping_to_addr("127.0.0.1"); ++ SocketCallback send_cb("send"); ++ ICMPEndpoint ping_to_endpoint(ping_to_addr); ++ ASSERT_NO_THROW_LOG(socket.asyncSend(&echo, sizeof(echo), &ping_to_endpoint, send_cb)); ++ ++ // Run the send handler. ++ io_service_->runOne(); ++ ++ // Callback should have been invoked without an error code. ++ ASSERT_TRUE(send_cb.getCalled()); ++ ASSERT_EQ(0, send_cb.getCode()); ++ // Verify we sent the whole message. ++ ASSERT_EQ(send_cb.getLength(), sizeof(echo)); ++ ++ // Call asyncReceive until we get our reply. ++ resizeInputBuf(1500); ++ ICMPEndpoint reply_endpoint; ++ SocketCallback receive_cb("receive"); ++ ++ // We need two receives when pinging loop back, only one with a real address. ++ size_t pass = 0; ++ do { ++ receive_cb.clear(); ++ memset(getInputBufData(), 0x00, getInputBufSize()); ++ ASSERT_NO_THROW(socket.asyncReceive(static_cast<void*>(getInputBufData()), ++ getInputBufSize(), 0, &reply_endpoint, receive_cb)); ++ ++ // Run the read handler. ++ io_service_->runOne(); ++ } while (++pass < 2 && (!receive_cb.getCalled())); ++ ++ // Callback should have been invoked without an error code. ++ ASSERT_TRUE(receive_cb.getCalled()); ++ ASSERT_EQ(0, receive_cb.getCode()); ++ ++ // Verify the reply came from the target address. ++ EXPECT_EQ(ping_to_addr.toText(), reply_endpoint.getAddress().toText()); ++ ++ // Verify we got at least enough data for an IP header. ++ size_t bytes_received = receive_cb.getLength(); ++ ASSERT_GE(bytes_received, sizeof(struct ip)); ++ ++ // Build the reply from data ++ uint8_t* icbuf = getInputBufData(); ++ ++ // Find the IP header length... ++ struct ip* ip_header = (struct ip*)(icbuf); ++ auto hlen = (ip_header->ip_hl << 2); ++ ++ // Make sure we received enough data. ++ ASSERT_TRUE(bytes_received >= (hlen + sizeof(struct icmp))) ++ << "received packet too short to be ICMP"; ++ ++ // Verify the message type. ++ struct icmp* reply = (struct icmp*)(icbuf + hlen); ++ auto msg_type = reply->icmp_type; ++ ASSERT_EQ(ICMPMsg::ECHO_REPLY, msg_type); ++ ++ // Verify the id and sequence values. ++ auto id = ntohs(reply->icmp_hun.ih_idseq.icd_id); ++ EXPECT_EQ(0x1122, id); ++ ++ auto sequence = ntohs(reply->icmp_hun.ih_idseq.icd_seq); ++ EXPECT_EQ(0x3344, sequence); ++ ++ // Close the socket. ++ ASSERT_NO_THROW_LOG(socket.close()); ++ ASSERT_FALSE(socket.isOpen()); ++} ++ ++} +diff --git a/src/hooks/dhcp/ping_check/tests/meson.build b/src/hooks/dhcp/ping_check/tests/meson.build +new file mode 100644 +index 0000000000..8beca7813e +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/tests/meson.build +@@ -0,0 +1,21 @@ ++if not TESTS_OPT.enabled() ++ subdir_done() ++endif ++ ++dhcp_ping_check_tests = executable( ++ 'dhcp-ping-check-tests', ++ 'config_cache_unittests.cc', ++ 'icmp_endpoint_unittests.cc', ++ 'icmp_msg_unittests.cc', ++ 'icmp_socket_unittests.cc', ++ 'ping_channel_unittests.cc', ++ 'ping_check_config_unittests.cc', ++ 'ping_check_mgr_unittests.cc', ++ 'ping_context_store_unittests.cc', ++ 'ping_context_unittests.cc', ++ 'run_unittests.cc', ++ dependencies: [CRYPTO_DEP, GTEST_DEP], ++ include_directories: [include_directories('.'), include_directories('..')] + INCLUDES, ++ link_with: [dhcp_ping_check_archive] + LIBS_BUILT_SO_FAR, ++) ++test('dhcp-ping-check-tests', dhcp_ping_check_tests, protocol: 'gtest') +diff --git a/src/hooks/dhcp/ping_check/tests/ping_channel_unittests.cc b/src/hooks/dhcp/ping_check/tests/ping_channel_unittests.cc +new file mode 100644 +index 0000000000..4c57a2e500 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/tests/ping_channel_unittests.cc +@@ -0,0 +1,821 @@ ++// Copyright (C) 2023-2025 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 This file contains tests which exercise the PingChannel class. ++ ++#include <config.h> ++ ++#include <ping_channel.h> ++#include <ping_test_utils.h> ++#include <asiolink/interval_timer.h> ++#include <asiolink/io_service_thread_pool.h> ++#include <dhcp/iface_mgr.h> ++#include <util/multi_threading_mgr.h> ++#include <testutils/multi_threading_utils.h> ++#include <testutils/gtest_utils.h> ++#include <gtest/gtest.h> ++ ++#include <boost/multi_index/indexed_by.hpp> ++#include <boost/multi_index/member.hpp> ++#include <boost/multi_index/mem_fun.hpp> ++#include <boost/multi_index/hashed_index.hpp> ++#include <boost/multi_index/ordered_index.hpp> ++#include <boost/multi_index_container.hpp> ++#include <boost/multi_index/composite_key.hpp> ++ ++#include <queue> ++#include <list> ++#include <thread> ++#include <mutex> ++ ++using namespace std; ++using namespace isc; ++using namespace isc::asiolink; ++using namespace isc::dhcp; ++using namespace isc::ping_check; ++using namespace isc::util; ++using namespace isc::test; ++using namespace boost::asio::error; ++ ++namespace ph = std::placeholders; ++ ++namespace { ++ ++/// @brief Tag for index by address. ++struct AddressIdSequenceIndexTag { }; ++ ++/// @brief A multi index container holding pointers ICMPMsgPtr ++/// ++/// The message may be accessed using the following index(es): ++/// - using an IPv4 address, id, and sequence number ++typedef boost::multi_index_container< ++ // It holds pointers to ICMPMsg objects. ++ ICMPMsgPtr, ++ boost::multi_index::indexed_by< ++ // Specification of the first index starts here. ++ // This index sorts PingContexts by IPv4 addresses represented as ++ // IOAddress objects. ++ // Specification of the first index starts here. ++ boost::multi_index::ordered_unique< ++ boost::multi_index::tag<AddressIdSequenceIndexTag>, ++ boost::multi_index::composite_key< ++ ICMPMsg, ++ // The boolean value specifying if context is waiting for a reply ++ boost::multi_index::const_mem_fun<ICMPMsg, const IOAddress&, ++ &ICMPMsg::getSource>, ++ boost::multi_index::const_mem_fun<ICMPMsg, uint16_t, ++ &ICMPMsg::getId>, ++ boost::multi_index::const_mem_fun<ICMPMsg, uint16_t, ++ &ICMPMsg::getSequence> ++ > ++ > ++ > ++> ReplyContainer; ++ ++/// @brief Single-threaded test fixture for exercising a PingChannel. ++class PingChannelTest : public IOServiceTest { ++public: ++ /// @brief Constructor ++ PingChannelTest() : mutex_(new mutex()), stopped_(false) { ++ MultiThreadingMgr::instance().setMode(false); ++ }; ++ ++ /// @brief Destructor ++ virtual ~PingChannelTest() { ++ stopped_ = true; ++ if (channel_) { ++ channel_->close(); ++ } ++ if (ios_pool_) { ++ ios_pool_->getIOService()->stopAndPoll(); ++ ios_pool_->stop(); ++ } ++ ios_pool_.reset(); ++ test_timer_.cancel(); ++ test_io_service_->stopAndPoll(); ++ MultiThreadingMgr::instance().setMode(false); ++ } ++ ++ /// @brief Called prior to test destruction. ++ /// Ensure we stop the pool in the even a test failed in an unexpected ++ /// manner that left it running. Otherwise we can get false TSAN complaints. ++ virtual void TearDown() { ++ // Stop the thread pool (if one). ++ if (ios_pool_) { ++ ios_pool_->stop(); ++ } ++ } ++ ++ /// @brief Initializes the IOServiceThreadPool ++ /// ++ /// @param num_threads number of threads in the pool ++ /// @param defer_start enables deferred start of the pool's IOService ++ void initThreadPool(size_t num_threads = 1, bool defer_start = false) { ++ ios_pool_.reset(new IoServiceThreadPool(IOServicePtr(), num_threads, defer_start)); ++ }; ++ ++ /// @brief Callback to invoke to fetch the next ping target. ++ /// ++ /// Fetches the next entry from the front of the send queue (if one). Checks for ++ /// test completion before returning. ++ /// ++ /// @param[out] next upon return it will contain the next target address. Contents are ++ /// only meaningful if the function returns true. ++ /// ++ /// @return True another target address exists, false otherwise. ++ virtual bool nextToSend(IOAddress& next) { ++ if (stopped_) { ++ return (false); ++ } ++ MultiThreadingLock lock(*mutex_); ++ bool use_next = true; ++ if (send_queue_.empty()) { ++ use_next = false; ++ } else { ++ next = send_queue_.front(); ++ } ++ ++ stopIfDone(); ++ return (use_next); ++ } ++ ++ /// @brief Callback to invoke when an ECHO write has completed. ++ /// ++ /// Ensures the completed echo matches the front of the send queue and then ++ /// pops it from the front of the queue. Checks for test completion before ++ /// returning. ++ /// ++ /// @param echo ICMP echo message that as sent ++ virtual void echoSent(ICMPMsgPtr& echo, bool send_failed) { ++ if (stopped_) { ++ return; ++ } ++ MultiThreadingLock lock(*mutex_); ++ ASSERT_EQ(echo->getDestination(), send_queue_.front()) << "send queue mismatch"; ++ send_queue_.pop(); ++ if (!send_failed) { ++ echos_sent_.push_back(echo); ++ } ++ stopIfDone(); ++ } ++ ++ /// @brief Callback to invoke when an ICMP reply has been received. ++ /// ++ /// Stores the reply if it is an ECHO REPLY message. We check to the ++ /// do avoid storing our outbound ECHO REQUESTs when testing with loop back ++ /// address. Checks for test completion before returning. ++ /// ++ /// @param reply ICMP message that was received ++ virtual void replyReceived(ICMPMsgPtr& reply) { ++ if (stopped_) { ++ return; ++ } ++ MultiThreadingLock lock(*mutex_); ++ if (reply->getType() == ICMPMsg::ECHO_REPLY) { ++ // If loopback routing is enabled, Insert the original destination address ++ // as the reply's source address. ++ if (channel_->route_loopback_) { ++ IOAddress address = channel_->loopback_map_.find(reply->getSequence()); ++ if (address != IOAddress::IPV4_ZERO_ADDRESS()) { ++ reply->setSource(address); ++ } ++ } ++ ++ replies_received_.push_back(reply); ++ storeReply(reply); ++ } ++ ++ stopIfDone(); ++ } ++ ++ /// @brief Tests that a channel can send and receive, reliably ++ /// in either single or multi-threaded mode. ++ /// ++ /// The test queues the given number of requests, beginning with ++ /// address 127.0.0.1 and incrementing the address through the number ++ /// of targets. It then opens the channel and initiates reading and ++ /// and writing, running until the test completes or times out. ++ /// It expects to receive a reply for every request. ++ /// ++ /// @param num_threads number of threads in the thread pool. If 0, ++ /// the channel will be single-threaded, sharing the test's IOService, ++ /// otherwise the channel will be driven by an IOServiceThreadPool with ++ /// the given number of threads. ++ /// @param num_targets number of target IP addresses to ping. Must not ++ /// be greater than 253. ++ /// @param set_error_trigger optional function that sets the error trigger ++ /// condition. ++ void sendReceiveTest(size_t num_threads, size_t num_targets = 25, ++ const std::function<void()>& set_error_trigger = [](){}); ++ ++ /// @brief Tests for graceful behavior when a channel encounters a read ++ /// or write error, in either single or multi-threaded mode. ++ /// ++ /// The test runs in two passes. The first pass sends and receives until ++ /// the error trigger occurs. The error should induce a graceful cessation ++ /// of operations. After verifying expected state of affairs, the second pass ++ /// is begun by re-opening the channel and resuming operations until the test ++ /// completes or times out. ++ /// ++ /// @param set_error_trigger function that sets the error trigger condition ++ /// @param num_threads number of threads in the thread pool. If 0, ++ /// the channel will be single-threaded, sharing the test's IOService, ++ /// otherwise the channel will be driven by an IOServiceThreadPool with ++ /// the given number of threads. ++ /// @param num_targets number of target IP addresses to ping. Must not ++ /// be greater than 253. ++ void ioErrorTest(const std::function<void()>& set_error_trigger, ++ size_t num_threads, size_t num_targets = 10); ++ ++ /// @brief Adds a reply to reply store. ++ /// ++ /// Fails if a reply for the same address, id, and sequence number is already ++ /// in the store. Must be used in a thread-safe context. ++ /// ++ /// @param reply reply to store ++ void storeReply(ICMPMsgPtr& reply) { ++ auto retpair = replies_map_.insert(reply); ++ ASSERT_TRUE(retpair.second) ++ << "failed to insert reply for: " << reply->getSource() ++ << ", id: " << reply->getId() << ", sequence: " << reply->getSequence(); ++ } ++ ++ /// @brief Fetches a reply from the store that matches a given ECHO ++ /// ++ /// Must be used in a thread-safe context. ++ /// ++ /// @param echo echo for whom a reply is sought ++ /// ++ /// @return The matching reply if found, otherwise an empty ICMPMsgPtr. ++ ICMPMsgPtr findReply(const ICMPMsgPtr& echo) { ++ auto const& index = replies_map_.get<AddressIdSequenceIndexTag>(); ++ auto key = boost::make_tuple(echo->getDestination(), echo->getId(), echo->getSequence()); ++ auto iter = index.find(key); ++ return (iter == index.end() ? ICMPMsgPtr() : *iter); ++ } ++ ++ /// @brief Channel instance. ++ TestablePingChannelPtr channel_; ++ ++ /// @brief IoServiceThreadPool instance ++ IoServiceThreadPoolPtr ios_pool_; ++ ++ /// @brief The mutex used to protect internal state. ++ const boost::scoped_ptr<std::mutex> mutex_; ++ ++ /// @brief Queue of IOAddresses for which to send ECHO REQUESTs. ++ std::queue<IOAddress> send_queue_; ++ ++ /// @brief List of ECHO REQUESTs that have been successfully sent in the order ++ /// they were sent. ++ std::list<ICMPMsgPtr> echos_sent_; ++ ++ /// @brief List of ECHO REPLYs that have been successfully received in the ++ /// order they were received. ++ std::list<ICMPMsgPtr> replies_received_; ++ ++ /// @brief Map of ECHO REPLYs received, indexed by source IP, id, and sequence number. ++ ReplyContainer replies_map_; ++ ++ /// @brief Flag which indicates that the manager has been stopped. ++ bool stopped_; ++}; ++ ++void ++PingChannelTest::sendReceiveTest(size_t num_threads, size_t num_targets /* = 25 */, ++ const std::function<void()>& set_error_trigger) { ++ stopped_ = false; ++ ++ // Clear state. ++ send_queue_ = {}; ++ echos_sent_.clear(); ++ replies_received_.clear(); ++ replies_map_.clear(); ++ ++ SKIP_IF(notRoot()); ++ ++ ASSERT_TRUE(num_targets < 253); ++ auto channel_ios = test_io_service_; ++ if (num_threads) { ++ // Enable MT mode. ++ util::MultiThreadingMgr::instance().setMode(true); ++ ++ // Initialize the thread pool to num_threads, defer start. ++ ASSERT_NO_THROW_LOG(initThreadPool(num_threads, true)); ++ ASSERT_TRUE(ios_pool_->isStopped()); ++ channel_ios = ios_pool_->getIOService(); ++ } ++ ++ // Create the channel instance with the appropriate io_service. ++ ASSERT_NO_THROW_LOG(channel_.reset(new TestablePingChannel( ++ channel_ios, ++ std::bind(&PingChannelTest::nextToSend, this, ph::_1), ++ std::bind(&PingChannelTest::echoSent, this, ph::_1, ph::_2), ++ std::bind(&PingChannelTest::replyReceived, this, ph::_1) ++ ))); ++ ++ // Create the callback to check test completion criteria. ++ // It returns true if we have sent out all the echos and received ++ // all the replies. ++ test_done_cb_ = [this]() { ++ return (send_queue_.empty() && (echos_sent_.size() == replies_received_.size())); ++ }; ++ ++ // Fill the send queue with num_target addresses to ping. ++ IOAddress target("127.0.0.1"); ++ for (auto i = 0; i < num_targets; ++i) { ++ send_queue_.push(target); ++ target = IOAddress::increase(target); ++ } ++ ++ (set_error_trigger)(); ++ ++ // Open the channel. ++ ASSERT_NO_THROW_LOG(channel_->open()); ++ ASSERT_TRUE(channel_->isOpen()); ++ ++ if (num_threads) { ++ ios_pool_->run(); ++ } ++ ++ // Initiate reading and writing. ++ ASSERT_NO_THROW_LOG(channel_->startRead()); ++ ASSERT_NO_THROW_LOG(channel_->startSend()); ++ ++ // Run the main thread's IOService until we complete or timeout. ++ ASSERT_NO_THROW_LOG(runIOService(1000)); ++ ++ if (ios_pool_) { ++ // Stop the thread pool. ++ ASSERT_NO_THROW_LOG(ios_pool_->stop()); ++ ASSERT_TRUE(ios_pool_->isStopped()); ++ } ++ ++ // Send queue should be empty. ++ EXPECT_TRUE(send_queue_.empty()); ++ ++ // Should have as many replies as echos. ++ EXPECT_EQ(echos_sent_.size(), replies_received_.size()); ++ ++ // Should have a reply for every echo. ++ for (auto const& echo : echos_sent_) { ++ ICMPMsgPtr reply = findReply(echo); ++ EXPECT_TRUE(reply) << "no reply found for:" << echo->getDestination() ++ << ", id:" << echo->getId() << ", sequence: " << echo->getSequence(); ++ } ++ ++ stopped_ = true; ++ if (channel_) { ++ channel_->close(); ++ } ++ if (ios_pool_) { ++ ios_pool_->getIOService()->stopAndPoll(); ++ ios_pool_->stop(); ++ } ++ ios_pool_.reset(); ++ test_timer_.cancel(); ++ test_io_service_->stopAndPoll(); ++ MultiThreadingMgr::instance().setMode(false); ++} ++ ++void ++PingChannelTest::ioErrorTest(const std::function<void()>& set_error_trigger, ++ size_t num_threads, size_t num_targets) { ++ ASSERT_TRUE(num_targets < 253); ++ SKIP_IF(notRoot()); ++ ++ ASSERT_TRUE(replies_received_.empty()); ++ ++ /// If it's an MT test create the thread pool. ++ auto channel_ios = test_io_service_; ++ if (num_threads) { ++ // Enable MT mode. ++ util::MultiThreadingMgr::instance().setMode(true); ++ ++ // Initialize the thread pool to num_threads, defer start. ++ ASSERT_NO_THROW_LOG(initThreadPool(num_threads, true)); ++ ASSERT_TRUE(ios_pool_->isStopped()); ++ channel_ios = ios_pool_->getIOService(); ++ } ++ ++ // Set local shutdown called flag to false. ++ bool shutdown_cb_called = false; ++ ++ // Create the channel instance with the appropriate io_service. ++ ASSERT_NO_THROW_LOG(channel_.reset(new TestablePingChannel( ++ channel_ios, ++ std::bind(&PingChannelTest::nextToSend, this, ph::_1), ++ std::bind(&PingChannelTest::echoSent, this, ph::_1, ph::_2), ++ std::bind(&PingChannelTest::replyReceived, this, ph::_1), ++ ([this, &shutdown_cb_called]() { ++ shutdown_cb_called = true; ++ test_io_service_->stop(); ++ }) ++ ))); ++ ++ // Set the test_done_cb_ to always return false (i.e. test is not ++ // done). ++ test_done_cb_ = []() { ++ return (false); ++ }; ++ ++ // Fill the send queue with target addresses to ping. ++ IOAddress target("127.0.0.1"); ++ for (auto i = 0; i < (num_targets / 2); ++i) { ++ send_queue_.push(target); ++ target = IOAddress::increase(target); ++ } ++ ++ // Set the error trigger. ++ (set_error_trigger)(); ++ ++ // FIRST PASS ++ ++ // Open the channel. ++ ASSERT_NO_THROW_LOG(channel_->open()); ++ ASSERT_TRUE(channel_->isOpen()); ++ ++ if (num_threads) { ++ ios_pool_->run(); ++ } ++ ++ // Initiate reading and writing. ++ ASSERT_NO_THROW_LOG(channel_->startRead()); ++ ASSERT_NO_THROW_LOG(channel_->startSend()); ++ ++ // Run the main thread's IOService until we stop or timeout. ++ ASSERT_NO_THROW_LOG(runIOService(1000)); ++ ++ // Shutdown callback should have been invoked, the channel should be closed, ++ // but the pool should still be running. ++ ASSERT_TRUE(shutdown_cb_called); ++ ASSERT_FALSE(channel_->isOpen()); ++ ++ if (ios_pool_) { ++ ASSERT_TRUE(ios_pool_->isRunning()); ++ ++ // Pause the thread pool. ++ ASSERT_NO_THROW_LOG(ios_pool_->pause()); ++ ASSERT_TRUE(ios_pool_->isPaused()); ++ } ++ ++ // Save how many echos sent and replies received during the first pass. ++ auto first_pass_echo_count = echos_sent_.size(); ++ auto first_pass_reply_count = replies_received_.size(); ++ ++ // Should have sent some but not all. ++ EXPECT_LE(first_pass_echo_count, num_targets); ++ ++ // SECOND PASS ++ ++ // Modify the test done callback to check test completion criteria. ++ // It returns true if we have sent out all the echos and received ++ // all the replies. ++ test_done_cb_ = [this, &first_pass_reply_count]() { ++ return (send_queue_.empty() && (replies_received_.size() > first_pass_reply_count)); ++ }; ++ ++ // Fill the send queue with target addresses to ping. ++ for (auto i = 0; i < (num_targets / 2); ++i) { ++ send_queue_.push(target); ++ target = IOAddress::increase(target); ++ } ++ ++ // Resume running the thread pool (if one). ++ if (ios_pool_) { ++ ASSERT_NO_THROW_LOG(ios_pool_->run()); ++ ASSERT_TRUE(ios_pool_->isRunning()); ++ } ++ ++ // Resume reopening the channel and restarting IO operations. ++ ASSERT_NO_THROW_LOG(channel_->open()); ++ ASSERT_TRUE(channel_->isOpen()); ++ ASSERT_NO_THROW_LOG(channel_->startRead()); ++ ASSERT_NO_THROW_LOG(channel_->startSend()); ++ ++ // Run the main thread's IOService until we complete or timeout. ++ ASSERT_NO_THROW_LOG(runIOService(1000)); ++ ++ // Stop the thread pool (if one). ++ if (ios_pool_) { ++ ASSERT_NO_THROW_LOG(ios_pool_->stop()); ++ ASSERT_TRUE(ios_pool_->isStopped()); ++ } ++ ++ // Send queue should be empty. ++ EXPECT_TRUE(send_queue_.empty()); ++ ++ // Should have sent as many echos as we queued. ++ EXPECT_EQ(echos_sent_.size(), num_targets); ++ ++ // Should have more replies than we had, but likely not all. ++ EXPECT_GE(replies_received_.size(), first_pass_reply_count); ++} ++ ++// Verifies PingChannel open and close operations. ++TEST_F(PingChannelTest, openCloseST) { ++ SKIP_IF(notRoot()); ++ ++ // Create the channel instance. ++ ASSERT_NO_THROW_LOG(channel_.reset(new TestablePingChannel( ++ test_io_service_, ++ std::bind(&PingChannelTest::nextToSend, this, ph::_1), ++ std::bind(&PingChannelTest::echoSent, this, ph::_1, ph::_2), ++ std::bind(&PingChannelTest::replyReceived, this, ph::_1) ++ ))); ++ ++ ASSERT_TRUE(channel_); ++ ++ ASSERT_TRUE(channel_->getSingleThreaded()); ++ ++ // Verify it is not open. ++ ASSERT_FALSE(channel_->isOpen()); ++ ++ EXPECT_FALSE(channel_->getWatchSocket()); ++ EXPECT_EQ(channel_->getRegisteredWriteFd(), -1); ++ EXPECT_EQ(channel_->getRegisteredReadFd(), -1); ++ ++ // Verify that invoking close is harmless. ++ ASSERT_NO_THROW_LOG(channel_->close()); ++ ++ // Attempt to open the channel. ++ ASSERT_NO_THROW_LOG(channel_->open()); ++ ++ // PingChannel::open() is synchronous and while it has a callback ++ // it should never be invoked. Run the service to make sure. ++ ASSERT_NO_THROW_LOG(runIOService(1000)); ++ ++ // Verify the channel is open. ++ ASSERT_TRUE(channel_->isOpen()); ++ ++ // Verify the WatchSocket was created and that its fd and that of the ++ // PingSocket are both registered with IfaceMgr. ++ ASSERT_TRUE(channel_->getWatchSocket()); ++ int registered_write_fd = channel_->getRegisteredWriteFd(); ++ EXPECT_EQ(registered_write_fd, channel_->getWatchSocket()->getSelectFd()); ++ EXPECT_TRUE(IfaceMgr::instance().isExternalSocket(registered_write_fd)); ++ int registered_read_fd = channel_->getRegisteredReadFd(); ++ EXPECT_EQ(registered_read_fd, channel_->getPingSocket()->getNative()); ++ EXPECT_TRUE(IfaceMgr::instance().isExternalSocket(registered_read_fd)); ++ ++ // A subsequent open should be harmless. ++ ASSERT_NO_THROW_LOG(channel_->open()); ++ ++ // Closing the socket should work. ++ ASSERT_NO_THROW_LOG(channel_->close()); ++ ++ // Verify watch socket is gone, registered fds are reset, and prior ++ // registered fds are no longer registered. ++ EXPECT_FALSE(channel_->getWatchSocket()); ++ EXPECT_EQ(channel_->getRegisteredWriteFd(), -1); ++ EXPECT_FALSE(IfaceMgr::instance().isExternalSocket(registered_write_fd)); ++ EXPECT_EQ(channel_->getRegisteredReadFd(), -1); ++ EXPECT_FALSE(IfaceMgr::instance().isExternalSocket(registered_read_fd)); ++ ++ // Verify it is not open. ++ ASSERT_FALSE(channel_->isOpen()); ++} ++ ++// Verifies PingChannel open and close operations. ++TEST_F(PingChannelTest, openCloseMT) { ++ SKIP_IF(notRoot()); ++ MultiThreadingTest mt; ++ ++ // Create the channel instance. ++ ASSERT_NO_THROW_LOG(channel_.reset(new TestablePingChannel( ++ test_io_service_, ++ std::bind(&PingChannelTest::nextToSend, this, ph::_1), ++ std::bind(&PingChannelTest::echoSent, this, ph::_1, ph::_2), ++ std::bind(&PingChannelTest::replyReceived, this, ph::_1) ++ ))); ++ ++ ASSERT_TRUE(channel_); ++ ++ ASSERT_FALSE(channel_->getSingleThreaded()); ++ ++ // Verify it is not open. ++ ASSERT_FALSE(channel_->isOpen()); ++ ++ // Verify that invoking close is harmless. ++ ASSERT_NO_THROW_LOG(channel_->close()); ++ ++ // Attempt to open the channel. ++ ASSERT_NO_THROW_LOG(channel_->open()); ++ ++ // PingChannel::open() is synchronous and while it has a callback ++ // it should never be invoked. Run the service to make sure. ++ ASSERT_NO_THROW_LOG(runIOService(1000)); ++ ++ // Verify the channel is open. ++ ASSERT_TRUE(channel_->isOpen()); ++ ++ // Verify that single-threaded members are not set. ++ EXPECT_FALSE(channel_->getWatchSocket()); ++ EXPECT_EQ(channel_->getRegisteredWriteFd(), -1); ++ EXPECT_EQ(channel_->getRegisteredReadFd(), -1); ++ ++ // A subsequent open should be harmless. ++ ASSERT_NO_THROW_LOG(channel_->open()); ++ ++ // Closing the socket should work. ++ ASSERT_NO_THROW_LOG(channel_->close()); ++ ++ // Verify it is not open. ++ ASSERT_FALSE(channel_->isOpen()); ++} ++ ++// Verifies that a PingChannel can perpetuate sending requests and receiving ++// replies when driven by a single-threaded IOService. ++TEST_F(PingChannelTest, sendReceiveST) { ++ sendReceiveTest(0); ++} ++ ++// Verifies that a PingChannel can perpetuate sending requests and receiving ++// replies when driven by a multi-threaded IOServiceThreadPool 3 threads ++TEST_F(PingChannelTest, sendReceiveMT) { ++ // Use a thread pool with 3 threads. ++ sendReceiveTest(3); ++} ++ ++// Verifies that an exception throw from asyncRead triggers graceful channel ++// shutdown and that operations can be resumed with a single-threaded channel. ++TEST_F(PingChannelTest, readExceptionErrorST) { ++ ioErrorTest( ++ [this]() { ++ channel_->throw_on_read_number_ = 5; ++ }, 0); ++} ++ ++// Verifies that an exception throw from asyncRead triggers graceful channel ++// shutdown and that operations can be resumed with a multi-threaded channel. ++TEST_F(PingChannelTest, readExceptionErrorMT) { ++ // Use a thread pool with 3 threads. ++ ioErrorTest( ++ [this]() { ++ channel_->throw_on_read_number_ = 5; ++ }, 3, 20); ++} ++ ++// Verifies that a fatal error code passed into socketReadCallback triggers graceful channel ++// shutdown and that operations can be resumed with a single-threaded channel. ++TEST_F(PingChannelTest, readFatalErrorST) { ++ ioErrorTest( ++ [this]() { ++ channel_->ec_on_read_number_ = 3; ++ // See boost/asio/error.hpp for error codes ++ channel_->read_error_ec_ = make_error_code(fault); ++ }, 0); ++} ++ ++// Verifies that a fatal error code passed into socketReadCallback triggers graceful channel ++// shutdown and that operations can be resumed with a single-threaded channel. ++TEST_F(PingChannelTest, readFatalErrorMT) { ++ ioErrorTest( ++ [this]() { ++ channel_->ec_on_read_number_ = 3; ++ // See boost/asio/error.hpp for error codes ++ channel_->read_error_ec_ = make_error_code(fault); ++ }, 4); ++} ++ ++// Verifies that a non-fatal, EWOULDBLOCK error passed into socketReadCallback does ++// not disrupt reading for a single-threaded channel. ++TEST_F(PingChannelTest, readAgainErrorST) { ++ sendReceiveTest(0, 10, ++ [this]() { ++ channel_->ec_on_read_number_ = 4; ++ // See boost/asio/error.hpp for error codes ++ channel_->read_error_ec_ = make_error_code(would_block); ++ }); ++} ++ ++// Verifies that a non-fatal, EWOULDBLOCK error passed into socketReadCallback does ++// not disrupt reading for a multi-threaded channel. ++TEST_F(PingChannelTest, readAgainErrorMT) { ++ sendReceiveTest(3, 10, ++ [this]() { ++ channel_->ec_on_read_number_ = 4; ++ // See boost/asio/error.hpp for error codes ++ channel_->read_error_ec_ = make_error_code(would_block); ++ }); ++} ++ ++// Verifies that an exception throw from asyncRead triggers graceful channel ++// shutdown and that operations can be resumed with a single-threaded channel. ++TEST_F(PingChannelTest, writeExceptionErrorST) { ++ ioErrorTest( ++ [this]() { ++ channel_->throw_on_write_number_ = 5; ++ }, 0); ++} ++ ++// Verifies that an exception throw from asyncRead triggers graceful channel ++// shutdown and that operations can be resumed with a multi-threaded channel. ++TEST_F(PingChannelTest, writeExceptionErrorMT) { ++ // Use a thread pool with 3 threads. ++ ioErrorTest( ++ [this]() { ++ channel_->throw_on_write_number_ = 5; ++ }, 3); ++} ++ ++// Verifies that a fatal error code passed into socketReadCallback triggers graceful channel ++// shutdown and that operations can be resumed with a single-threaded channel. ++TEST_F(PingChannelTest, writeFatalErrorST) { ++ ioErrorTest( ++ [this]() { ++ channel_->ec_on_write_number_ = 3; ++ // See boost/asio/error.hpp for error codes ++ channel_->write_error_ec_ = make_error_code(fault); ++ }, 0); ++} ++ ++// Verifies that a fatal error code passed into socketReadCallback triggers graceful channel ++// shutdown and that operations can be resumed with a single-threaded channel. ++TEST_F(PingChannelTest, writeFatalErrorMT) { ++ ioErrorTest( ++ [this]() { ++ channel_->ec_on_write_number_ = 3; ++ // See boost/asio/error.hpp for error codes ++ channel_->write_error_ec_ = make_error_code(fault); ++ }, 4); ++} ++ ++// Verifies that a non-fatal, EWOULDBLOCK error passed into socketWriteCallback does ++// not disrupt writing for a single-threaded channel. ++TEST_F(PingChannelTest, writeAgainErrorST) { ++ sendReceiveTest(0, 10, ++ [this]() { ++ channel_->ec_on_write_number_ = 6; ++ // See boost/asio/error.hpp for error codes ++ channel_->write_error_ec_ = make_error_code(would_block); ++ }); ++} ++ ++// Verifies that a non-fatal, EWOULDBLOCK error passed into socketWriteCallback ++// does not disrupt writing for a multi-threaded channel. ++TEST_F(PingChannelTest, writeAgainErrorMT) { ++ sendReceiveTest(3, 10, ++ [this]() { ++ channel_->ec_on_write_number_ = 6; ++ // See boost/asio/error.hpp for error codes ++ channel_->write_error_ec_ = make_error_code(would_block); ++ }); ++} ++ ++// Verify the recoverable write errors do not disrupt writing for a ++// single-threaded channel. ++TEST_F(PingChannelTest, writeSendFailedErrorST) { ++ SKIP_IF(notRoot()); ++ ++ std::list<boost::asio::error::basic_errors> errors = { ++ boost::asio::error::network_unreachable, ++ boost::asio::error::host_unreachable, ++ boost::asio::error::network_down, ++ boost::asio::error::no_buffer_space, ++ boost::asio::error::access_denied ++ }; ++ ++ for (auto const& error : errors) { ++ sendReceiveTest(0, 10, ++ [this, error]() { ++ channel_->ec_on_write_number_ = 6; ++ // See boost/asio/error.hpp for error codes ++ channel_->write_error_ec_ = make_error_code(error); ++ }); ++ ++ // Sanity check, we should have sent one less than we targeted. ++ EXPECT_EQ(echos_sent_.size(), 9); ++ } ++} ++ ++// Verify the recoverable write errors do not disrupt writing for a ++// multi-threaded channel. ++TEST_F(PingChannelTest, writeSendFailedErrorMT) { ++ SKIP_IF(notRoot()); ++ ++ std::list<boost::asio::error::basic_errors> errors = { ++ boost::asio::error::network_unreachable, ++ boost::asio::error::host_unreachable, ++ boost::asio::error::network_down, ++ boost::asio::error::no_buffer_space, ++ boost::asio::error::access_denied ++ }; ++ ++ for (auto const& error : errors) { ++ sendReceiveTest(3, 10, ++ [this, error]() { ++ channel_->ec_on_write_number_ = 6; ++ // See boost/asio/error.hpp for error codes ++ channel_->write_error_ec_ = make_error_code(error); ++ }); ++ ++ // Sanity check, we should have sent one less than we targeted. ++ EXPECT_EQ(echos_sent_.size(), 9); ++ } ++} ++ ++} // end of anonymous namespace +diff --git a/src/hooks/dhcp/ping_check/tests/ping_check_config_unittests.cc b/src/hooks/dhcp/ping_check/tests/ping_check_config_unittests.cc +new file mode 100644 +index 0000000000..a831a0efab +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/tests/ping_check_config_unittests.cc +@@ -0,0 +1,287 @@ ++// Copyright (C) 2023-2025 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 This file contains tests which exercise the PingCheckConfig class. ++ ++#include <config.h> ++#include <ping_check_config.h> ++#include <testutils/gtest_utils.h> ++ ++#include <gtest/gtest.h> ++#include <list> ++ ++using namespace std; ++using namespace isc; ++using namespace isc::data; ++using namespace isc::ping_check; ++ ++namespace { ++ ++// Verifies PingCheckConfig constructors and accessors. ++TEST(PingCheckConfigTest, basics) { ++ PingCheckConfig config; ++ ++ // Verify initial values. ++ EXPECT_TRUE(config.getEnablePingCheck()); ++ EXPECT_EQ(1, config.getMinPingRequests()); ++ EXPECT_EQ(100, config.getReplyTimeout()); ++ EXPECT_EQ(60, config.getPingClttSecs()); ++ EXPECT_EQ(0, config.getPingChannelThreads()); ++ ++ // Verify accessors. ++ EXPECT_NO_THROW_LOG(config.setEnablePingCheck(false)); ++ EXPECT_FALSE(config.getEnablePingCheck()); ++ ++ EXPECT_NO_THROW_LOG(config.setMinPingRequests(4)); ++ EXPECT_EQ(4, config.getMinPingRequests()); ++ ++ EXPECT_NO_THROW_LOG(config.setReplyTimeout(250)); ++ EXPECT_EQ(250, config.getReplyTimeout()); ++ ++ EXPECT_NO_THROW_LOG(config.setPingClttSecs(120)); ++ EXPECT_EQ(120, config.getPingClttSecs()); ++ ++ EXPECT_NO_THROW_LOG(config.setPingChannelThreads(6)); ++ EXPECT_EQ(6, config.getPingChannelThreads()); ++ ++ // Verify copy construction. ++ PingCheckConfig config2(config); ++ EXPECT_FALSE(config2.getEnablePingCheck()); ++ EXPECT_EQ(4, config2.getMinPingRequests()); ++ EXPECT_EQ(250, config2.getReplyTimeout()); ++ EXPECT_EQ(120, config2.getPingClttSecs()); ++ EXPECT_EQ(6, config2.getPingChannelThreads()); ++} ++ ++// Exercises PingCheckConfig parameter parsing with valid configuration ++// permutations. ++TEST(PingCheckConfigTest, parseValidScenarios) { ++ // Describes a test scenario. ++ struct Scenario { ++ int line_; // Scenario line number ++ std::string json_; // JSON configuration to parse ++ bool exp_enable_ping_check_; // Expected value for enable-ping-check ++ uint32_t exp_min_ping_requests_; // Expected value for min-ping-requests ++ uint32_t exp_reply_timeout_; // Expected value for reply-timeout ++ uint32_t exp_ping_cltt_secs_; // Expected value for ping-cltt-secs ++ size_t exp_num_threads_; // Expected value for ping-channel-threads ++ }; ++ ++ // List of test scenarios to run. ++ list<Scenario> scenarios = { ++ { ++ // Empty map ++ __LINE__, ++ R"({ })", ++ true, 1, 100, 60, 0 ++ }, ++ { ++ // Only enable-ping-check", ++ __LINE__, ++ R"({ "enable-ping-check" : false })", ++ false, 1, 100, 60, 0 ++ }, ++ { ++ // Only min-ping-requests", ++ __LINE__, ++ R"({ "min-ping-requests" : 3 })", ++ true, 3, 100, 60, 0 ++ }, ++ { ++ // Only reply-timeout", ++ __LINE__, ++ R"({ "reply-timeout" : 250 })", ++ true, 1, 250, 60, 0 ++ }, ++ { ++ // Only ping-cltt-secs", ++ __LINE__, ++ R"({ "ping-cltt-secs" : 77 })", ++ true, 1, 100, 77, 0 ++ }, ++ { ++ // Only ping-channel-threads", ++ __LINE__, ++ R"({ "ping-channel-threads" : 5 })", ++ true, 1, 100, 60, 5 ++ }, ++ { ++ // All parameters", ++ __LINE__, ++ R"( ++ { ++ "enable-ping-check" : false, ++ "min-ping-requests" : 2, ++ "reply-timeout" : 375, ++ "ping-cltt-secs" : 120, ++ "ping-channel-threads" : 6 ++ })", ++ false, 2, 375, 120, 6 ++ }, ++ }; ++ ++ // Iterate over the scenarios. ++ for (auto const& scenario : scenarios) { ++ stringstream oss; ++ oss << "scenario at line: " << scenario.line_; ++ SCOPED_TRACE(oss.str()); ++ ++ // Convert JSON texts to Element map. ++ ConstElementPtr json_elements; ++ ASSERT_NO_THROW_LOG(json_elements = Element::fromJSON(scenario.json_)); ++ ++ // Parsing elements should succeed. ++ PingCheckConfig config; ++ ASSERT_NO_THROW_LOG(config.parse(json_elements)); ++ ++ // Verify expected values. ++ EXPECT_EQ(scenario.exp_enable_ping_check_, config.getEnablePingCheck()); ++ EXPECT_EQ(scenario.exp_min_ping_requests_, config.getMinPingRequests()); ++ EXPECT_EQ(scenario.exp_reply_timeout_, config.getReplyTimeout()); ++ EXPECT_EQ(scenario.exp_ping_cltt_secs_, config.getPingClttSecs()); ++ EXPECT_EQ(scenario.exp_num_threads_, config.getPingChannelThreads()); ++ } ++} ++ ++// Exercises PingCheckConfig parameter parsing with invalid configuration ++// permutations. ++TEST(PingCheckConfigTest, parseInvalidScenarios) { ++ // Describes a test scenario. ++ struct Scenario { ++ int line_; // Scenario line number ++ string json_; // JSON configuration to parse ++ string exp_message_; // Expected exception message ++ }; ++ ++ // List of test scenarios to run. Most scenario supply ++ // all valid parameters except one in error. This allows ++ // us to verify that no values are changed if any are in error. ++ list<Scenario> scenarios = { ++ { ++ __LINE__, ++ R"( ++ { ++ "enable-ping-check" : false, ++ "min-ping-requests" : 3, ++ "reply-timeout" : 250, ++ "ping-cltt-secs" : 90, ++ "ping-channel-threads" : 4, ++ "bogus" : false ++ })", ++ "spurious 'bogus' parameter" ++ }, ++ { ++ __LINE__, ++ R"( ++ { ++ "enable-ping-check" : "not bool", ++ "min-ping-requests" : 3, ++ "reply-timeout" : 250, ++ "ping-cltt-secs" : 90, ++ "ping-channel-threads" : 4 ++ })", ++ "'enable-ping-check' parameter is not a boolean" ++ }, ++ { ++ __LINE__, ++ R"( ++ { ++ "enable-ping-check" : false, ++ "min-ping-requests" : 0, ++ "reply-timeout" : 250, ++ "ping-cltt-secs" : 90, ++ "ping-channel-threads" : 4 ++ })", ++ "invalid min-ping-requests: '0', must be greater than 0" ++ }, ++ { ++ __LINE__, ++ R"( ++ { ++ "enable-ping-check" : false, ++ "min-ping-requests" : -2, ++ "reply-timeout" : 250, ++ "ping-cltt-secs" : 90, ++ "ping-channel-threads" : 4 ++ })", ++ "invalid min-ping-requests: '-2', must be greater than 0" ++ }, ++ { ++ __LINE__, ++ R"( ++ { ++ "enable-ping-check" : false, ++ "min-ping-requests" : 1, ++ "reply-timeout" : 0, ++ "ping-cltt-secs" : 90, ++ "ping-channel-threads" : 4 ++ })", ++ "invalid reply-timeout: '0', must be greater than 0" ++ }, ++ { ++ __LINE__, ++ R"( ++ { ++ "enable-ping-check" : false, ++ "min-ping-requests" : 1, ++ "reply-timeout" : -77, ++ "ping-cltt-secs" : 90, ++ "ping-channel-threads" : 4 ++ })", ++ "invalid reply-timeout: '-77', must be greater than 0" ++ }, ++ { ++ __LINE__, ++ R"( ++ { ++ "enable-ping-check" : false, ++ "min-ping-requests" : 1, ++ "reply-timeout" : 250, ++ "ping-cltt-secs" : -3, ++ "ping-channel-threads" : 4 ++ })", ++ "invalid ping-cltt-secs: '-3', cannot be less than 0" ++ }, ++ { ++ __LINE__, ++ R"( ++ { ++ "enable-ping-check" : false, ++ "min-ping-requests" : 1, ++ "reply-timeout" : 250, ++ "ping-cltt-secs" : 90, ++ "ping-channel-threads" : -1 ++ })", ++ "invalid ping-channel-threads: '-1', cannot be less than 0" ++ } ++ }; ++ ++ // Iterate over the scenarios. ++ PingCheckConfig default_config; ++ for (auto const& scenario : scenarios) { ++ stringstream oss; ++ oss << "scenario at line: " << scenario.line_; ++ SCOPED_TRACE(oss.str()); ++ ++ // Convert JSON text to a map of parameters. ++ ConstElementPtr json_elements; ++ ASSERT_NO_THROW_LOG(json_elements = Element::fromJSON(scenario.json_)); ++ ++ // Parsing parameters should throw. ++ PingCheckConfig config; ++ ASSERT_THROW_MSG(config.parse(json_elements), dhcp::DhcpConfigError, ++ scenario.exp_message_); ++ ++ // Original values should be intact. ++ EXPECT_EQ(default_config.getEnablePingCheck(), config.getEnablePingCheck()); ++ EXPECT_EQ(default_config.getMinPingRequests(), config.getMinPingRequests()); ++ EXPECT_EQ(default_config.getReplyTimeout(), config.getReplyTimeout()); ++ EXPECT_EQ(default_config.getPingClttSecs(), config.getPingClttSecs()); ++ EXPECT_EQ(default_config.getPingChannelThreads(), config.getPingChannelThreads()); ++ } ++} ++ ++} // end of anonymous namespace +diff --git a/src/hooks/dhcp/ping_check/tests/ping_check_mgr_unittests.cc b/src/hooks/dhcp/ping_check/tests/ping_check_mgr_unittests.cc +new file mode 100644 +index 0000000000..ded13b085c +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/tests/ping_check_mgr_unittests.cc +@@ -0,0 +1,1878 @@ ++// Copyright (C) 2023-2025 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 This file contains tests which exercise the PingCheckMgr class. ++#include <config.h> ++ ++#include <ping_check_mgr.h> ++#include <ping_test_utils.h> ++#include <cc/data.h> ++#include <dhcp/pkt4.h> ++#include <dhcpsrv/cfgmgr.h> ++#include <dhcpsrv/lease.h> ++#include <hooks/hooks_manager.h> ++#include <util/chrono_time_utils.h> ++#include <testutils/gtest_utils.h> ++#include <testutils/multi_threading_utils.h> ++ ++#include <gtest/gtest.h> ++#include <mutex> ++#include <chrono> ++ ++using namespace std; ++using namespace isc; ++using namespace isc::data; ++using namespace isc::dhcp; ++using namespace isc::util; ++using namespace isc::asiolink; ++using namespace isc::ping_check; ++using namespace isc::hooks; ++using namespace isc::test; ++using namespace std::chrono; ++using namespace boost::asio::error; ++ ++namespace ph = std::placeholders; ++ ++namespace { ++ ++// Sanity check the basics for production class, PingCheckMgr, single-threaded mode. ++TEST(PingCheckMgr, basicsST) { ++ SKIP_IF(IOServiceTest::notRoot()); ++ MultiThreadingMgr::instance().setMode(false); ++ ++ // Create a multi-threaded manager. ++ IOServicePtr main_ios(new IOService()); ++ PingCheckMgrPtr mgr; ++ ASSERT_NO_THROW_LOG(mgr.reset(new PingCheckMgr(0))); ++ ASSERT_TRUE(mgr); ++ mgr->setIOService(main_ios); ++ ++ // Sanity check the global configuration. More robust tests are done ++ // elsewhere. ++ auto& config = mgr->getGlobalConfig(); ++ EXPECT_TRUE(config->getEnablePingCheck()); ++ EXPECT_EQ(1, config->getMinPingRequests()); ++ EXPECT_EQ(100, config->getReplyTimeout()); ++ EXPECT_EQ(60, config->getPingClttSecs()); ++ EXPECT_EQ(0, config->getPingChannelThreads()); ++ ++ // Verify we report as stopped. ++ EXPECT_FALSE(mgr->isRunning()); ++ EXPECT_TRUE(mgr->isStopped()); ++ EXPECT_FALSE(mgr->isPaused()); ++ ++ // Starting it should be OK. ++ ASSERT_NO_THROW_LOG(mgr->start()); ++ ++ // Verify we report as running. ++ EXPECT_TRUE(mgr->isRunning()); ++ EXPECT_FALSE(mgr->isStopped()); ++ EXPECT_FALSE(mgr->isPaused()); ++ ++ // Pausing it should be harmless. ++ ASSERT_NO_THROW_LOG(mgr->pause()); ++ ++ // Verify we report as running. ++ EXPECT_TRUE(mgr->isRunning()); ++ EXPECT_FALSE(mgr->isStopped()); ++ EXPECT_FALSE(mgr->isPaused()); ++ ++ // Resuming it should be harmless. ++ ASSERT_NO_THROW_LOG(mgr->resume()); ++ ++ // Verify we report as running. ++ EXPECT_TRUE(mgr->isRunning()); ++ EXPECT_FALSE(mgr->isStopped()); ++ EXPECT_FALSE(mgr->isPaused()); ++ ++ // Stopping it should be fine ++ ASSERT_NO_THROW_LOG(mgr->stop()); ++ ++ // Verify we report as stopped. ++ EXPECT_FALSE(mgr->isRunning()); ++ EXPECT_TRUE(mgr->isStopped()); ++ EXPECT_FALSE(mgr->isPaused()); ++ ++ // Re-starting it should be OK. ++ ASSERT_NO_THROW_LOG(mgr->start()); ++ ++ // Verify we report as running. ++ EXPECT_TRUE(mgr->isRunning()); ++ EXPECT_FALSE(mgr->isStopped()); ++ EXPECT_FALSE(mgr->isPaused()); ++ ++ // Calling destructor when its running should be OK. ++ ASSERT_NO_THROW_LOG(mgr.reset()); ++ ++ main_ios->stopAndPoll(); ++} ++ ++// Sanity check the basics for production class, PingCheckMgr. Bulk of testing ++// is done with test derivation, TestPingCheckMgr. ++TEST(PingCheckMgr, basicsMT) { ++ SKIP_IF(IOServiceTest::notRoot()); ++ MultiThreadingTest mt; ++ ++ // Create a multi-threaded manager. ++ IOServicePtr main_ios(new IOService()); ++ PingCheckMgrPtr mgr; ++ ASSERT_NO_THROW_LOG(mgr.reset(new PingCheckMgr(3))); ++ ASSERT_TRUE(mgr); ++ mgr->setIOService(main_ios); ++ ++ // Sanity check the global configuration. More robust tests are done ++ // elsewhere. ++ auto& config = mgr->getGlobalConfig(); ++ EXPECT_TRUE(config->getEnablePingCheck()); ++ EXPECT_EQ(1, config->getMinPingRequests()); ++ EXPECT_EQ(100, config->getReplyTimeout()); ++ EXPECT_EQ(60, config->getPingClttSecs()); ++ EXPECT_EQ(3, config->getPingChannelThreads()); ++ ++ // It should not be running yet. ++ EXPECT_FALSE(mgr->isRunning()); ++ EXPECT_TRUE(mgr->isStopped()); ++ EXPECT_FALSE(mgr->isPaused()); ++ ++ // Starting it should be OK. ++ ASSERT_NO_THROW_LOG(mgr->start()); ++ ++ // Verify it's running. ++ EXPECT_TRUE(mgr->isRunning()); ++ EXPECT_FALSE(mgr->isStopped()); ++ EXPECT_FALSE(mgr->isPaused()); ++ ++ // Pausing it should be fine. ++ ASSERT_NO_THROW_LOG(mgr->pause()); ++ ++ // Verify it's paused. ++ EXPECT_FALSE(mgr->isRunning()); ++ EXPECT_FALSE(mgr->isStopped()); ++ EXPECT_TRUE(mgr->isPaused()); ++ ++ // Resuming it should be fine. ++ ASSERT_NO_THROW_LOG(mgr->resume()); ++ ++ // Verify it's running. ++ EXPECT_TRUE(mgr->isRunning()); ++ EXPECT_FALSE(mgr->isStopped()); ++ EXPECT_FALSE(mgr->isPaused()); ++ ++ // Stopping it should be fine ++ ASSERT_NO_THROW_LOG(mgr->stop()); ++ ++ // It should not be running. ++ EXPECT_FALSE(mgr->isRunning()); ++ EXPECT_TRUE(mgr->isStopped()); ++ EXPECT_FALSE(mgr->isPaused()); ++ ++ // Re-starting it should be OK. ++ ASSERT_NO_THROW_LOG(mgr->start()); ++ ++ // Verify it's running. ++ EXPECT_TRUE(mgr->isRunning()); ++ EXPECT_FALSE(mgr->isStopped()); ++ EXPECT_FALSE(mgr->isPaused()); ++ ++ // Calling destructor when its running should be OK. ++ ASSERT_NO_THROW_LOG(mgr.reset()); ++} ++ ++// Verify basic behavior of PingCheckMgr::configure(). ++TEST(PingCheckMgr, configure) { ++ // Create a manager. ++ IOServicePtr main_ios(new IOService()); ++ PingCheckMgrPtr mgr; ++ ASSERT_NO_THROW_LOG(mgr.reset(new PingCheckMgr())); ++ ASSERT_TRUE(mgr); ++ ++ // Verify initial global configuration. ++ auto& default_config = mgr->getGlobalConfig(); ++ EXPECT_TRUE(default_config->getEnablePingCheck()); ++ EXPECT_EQ(1, default_config->getMinPingRequests()); ++ EXPECT_EQ(100, default_config->getReplyTimeout()); ++ EXPECT_EQ(60, default_config->getPingClttSecs()); ++ EXPECT_EQ(0, default_config->getPingChannelThreads()); ++ ++ //Create a valid configuration. ++ std::string valid_json_cfg = ++ R"({ ++ "enable-ping-check" : false, ++ "min-ping-requests" : 2, ++ "reply-timeout" : 250, ++ "ping-cltt-secs" : 90, ++ "ping-channel-threads" : 3 ++ })"; ++ ++ auto parameters = Element::fromJSON(valid_json_cfg); ++ ++ // Parse it. ++ ASSERT_NO_THROW_LOG(mgr->configure(parameters)); ++ ++ // Verify updated global configuration. ++ auto& config = mgr->getGlobalConfig(); ++ ASSERT_TRUE(config); ++ EXPECT_FALSE(config->getEnablePingCheck()); ++ EXPECT_EQ(2, config->getMinPingRequests()); ++ EXPECT_EQ(250, config->getReplyTimeout()); ++ EXPECT_EQ(90, config->getPingClttSecs()); ++ EXPECT_EQ(3, config->getPingChannelThreads()); ++ ++ // Create an invalid configuration. ++ std::string invalid_json_cfg = ++ R"({ ++ "enable-ping-check" : true, ++ "min-ping-requests" : 4, ++ "reply-timeout" : 500, ++ "ping-cltt-secs" : 45, ++ "ping-channel-threads" : 6, ++ "bogus" : 0 ++ })"; ++ ++ parameters = Element::fromJSON(invalid_json_cfg); ++ ++ // Parsing it should throw. ++ ASSERT_THROW_MSG(mgr->configure(parameters), DhcpConfigError, "spurious 'bogus' parameter"); ++ ++ // Verify configuration values were left unchanged. ++ auto& final_config = mgr->getGlobalConfig(); ++ ASSERT_TRUE(final_config); ++ EXPECT_EQ(final_config->getEnablePingCheck(), config->getEnablePingCheck()); ++ EXPECT_EQ(final_config->getMinPingRequests(), config->getMinPingRequests()); ++ EXPECT_EQ(final_config->getReplyTimeout(), config->getReplyTimeout()); ++ EXPECT_EQ(final_config->getPingClttSecs(), config->getPingClttSecs()); ++ EXPECT_EQ(final_config->getPingChannelThreads(), config->getPingChannelThreads()); ++} ++ ++/// @brief Defines a callback to invoke at the bottom of sendCompleted() ++typedef std::function<void(const ICMPMsgPtr& echo, bool send_failed)> SendCompletedCallback; ++ ++/// @brief Defines a callback to invoke at the bottom of replyReceived() ++typedef std::function<void(const ICMPMsgPtr& reply)> ReplyReceivedCallback; ++ ++/// @brief Testable derivation of PingCheckMgr ++/// ++/// Uses a TestablePingChannel to facilitate more robust testing. ++class TestablePingCheckMgr : public PingCheckMgr { ++public: ++ /// @brief Constructor. ++ /// ++ /// @param num_threads number of threads to use in the thread pool (0 means follow ++ /// core thread pool size) ++ /// @param min_echos minimum number of ECHO REQUESTs sent without replies ++ /// received required to declare an address free to offer. Defaults to 1, ++ /// must be greater than zero. ++ /// @param reply_timeout maximum number of milliseconds to wait for an ++ /// ECHO REPLY after an ECHO REQUEST has been sent. Defaults to 100, ++ TestablePingCheckMgr(uint32_t num_threads, uint32_t min_echos = 1, ++ uint32_t reply_timeout = 100) ++ : PingCheckMgr(num_threads, min_echos, reply_timeout), ++ post_send_completed_cb_(SendCompletedCallback()), ++ post_reply_received_cb_(ReplyReceivedCallback()) { ++ } ++ ++ /// @brief Destructor. ++ virtual ~TestablePingCheckMgr() { ++ post_send_completed_cb_ = SendCompletedCallback(); ++ post_reply_received_cb_ = ReplyReceivedCallback(); ++ if (getIOService()) { ++ getIOService()->stopAndPoll(); ++ } ++ } ++ ++ /// @brief Fetch the current channel instance. ++ /// ++ /// @return pointer to the TestablePingChannel instance (or an empty pointer). ++ TestablePingChannelPtr getChannel() { ++ return (boost::dynamic_pointer_cast<TestablePingChannel>(channel_)); ++ } ++ ++ /// @brief Fetches the manager's context store. ++ /// ++ /// @return Pointer to the PingContextStore. ++ PingContextStorePtr getStore() { ++ return (store_); ++ } ++ ++ /// @brief Fetches the expiration timer's current interval (in milliseconds). ++ /// ++ /// @return current interval as long or 0L if the timer is not currently ++ /// running or does not exist. ++ long getExpirationTimerInterval() { ++ if (expiration_timer_) { ++ return (expiration_timer_->getInterval()); ++ } ++ ++ return (0); ++ } ++ ++protected: ++ /// @brief Creates a TestablePingChannel instance. ++ /// ++ /// This override the base case creator. ++ /// ++ /// @param io_service IOService that will drive the channel. ++ /// @return pointer to the newly created channel. ++ virtual PingChannelPtr createChannel(asiolink::IOServicePtr io_service) { ++ return (TestablePingChannelPtr( ++ new TestablePingChannel(io_service, ++ std::bind(&PingCheckMgr::nextToSend, this, ph::_1), ++ std::bind(&TestablePingCheckMgr::sendCompleted, ++ this, ph::_1, ph::_2), ++ std::bind(&TestablePingCheckMgr::replyReceived, this, ph::_1), ++ std::bind(&PingCheckMgr::channelShutdown, this)))); ++ } ++ ++public: ++ /// @brief Fetches the current size of the parking lot. ++ /// ++ /// @return size_t containing the number of entries parked. ++ size_t parkingLotSize() const { ++ auto const& parking_lot = ServerHooks::getServerHooks().getParkingLotPtr("lease4_offer"); ++ return (parking_lot->size()); ++ } ++ ++ /// @brief Callback passed to PingChannel to invoke when an ECHO REQUEST ++ /// send has completed. ++ /// ++ /// -# Invokes the base class implementation ++ /// -# Invokes an optional callback ++ /// ++ /// @param echo ICMP echo message that is sent. ++ /// @param send_failed True if the send completed with a non-fatal error, ++ /// false otherwise. ++ virtual void sendCompleted(const ICMPMsgPtr& echo, bool send_failed) { ++ // Call the production callback. ++ PingCheckMgr::sendCompleted(echo, send_failed); ++ ++ // Invoke the post check, if one. ++ if (post_send_completed_cb_) { ++ (post_send_completed_cb_)(echo, send_failed); ++ } ++ } ++ ++ /// @brief Callback invoked by the channel to process received ICMP messages. ++ /// ++ /// -# Invokes the base class implementation ++ /// -# Pauses the test IOService thread and returns if the parking lot is empty ++ /// -# Invokes an option callback passing in the reply received ++ /// ++ /// @param reply pointer to the ICMP message received. ++ virtual void replyReceived(const ICMPMsgPtr& reply) { ++ if (reply->getType() == ICMPMsg::ECHO_REQUEST) { ++ return; ++ } ++ ++ // If we're routing loopback messages, look up the original address based ++ // on the sequence number and use it as the reply's source address. ++ if (getChannel()->route_loopback_) { ++ IOAddress address = getChannel()->loopback_map_.find(reply->getSequence()); ++ if (address != IOAddress::IPV4_ZERO_ADDRESS()) { ++ reply->setSource(address); ++ } ++ } ++ ++ // Call the production callback. ++ PingCheckMgr::replyReceived(reply); ++ ++ // Invoke the post check, if one. ++ if (post_reply_received_cb_) { ++ (post_reply_received_cb_)(reply); ++ } ++ } ++ ++ /// @brief Fetches the thread pool (if it exists). ++ /// ++ /// @return pointer to theIoServiceThreadPool. Will be empty ++ /// in ST mode or if the manager has not been started. ++ asiolink::IoServiceThreadPoolPtr getThreadPool() { ++ return (thread_pool_); ++ } ++ ++ /// @brief Sets the network_state object. ++ /// ++ /// @param network_state pointer to a NetworkState instance. ++ void setNetworkState(NetworkStatePtr network_state) { ++ network_state_ = network_state; ++ } ++ ++ /// @brief Callback to invoke at the bottom of sendCompleted(). ++ SendCompletedCallback post_send_completed_cb_; ++ ++ /// @brief Callback to invoke at the bottom of replyReceived(). ++ ReplyReceivedCallback post_reply_received_cb_; ++}; ++ ++/// @brief Defines a shared pointer to a PingCheckMgr. ++typedef boost::shared_ptr<TestablePingCheckMgr> TestablePingCheckMgrPtr; ++ ++/// @brief Holds a lease and its associated query. ++struct LeaseQueryPair { ++public: ++ /// @brief Constructor. ++ /// ++ /// @param lease pointer to the lease. ++ /// @param query pointer to the query. ++ LeaseQueryPair(Lease4Ptr lease, Pkt4Ptr query) : lease_(lease), query_(query) { ++ }; ++ ++ /// @brief Pointer to the lease. ++ Lease4Ptr lease_; ++ ++ /// @brief Pointer to the query. ++ Pkt4Ptr query_; ++}; ++ ++/// @brief Container of leases and their associated queries. ++typedef std::vector<LeaseQueryPair> LeaseQueryPairs; ++ ++/// @brief Test fixture for exercising PingCheckMgr. ++/// ++/// Uses a TestablePingCheckMgr instance for all tests and ++/// provides numerous helper functions. ++class PingCheckMgrTest : public IOServiceTest { ++public: ++ /// @brief Constructor. ++ PingCheckMgrTest() : mgr_(), lease_query_pairs_(), mutex_(new mutex()), ++ test_start_time_(PingContext::now()), unparked_(0) { ++ MultiThreadingMgr::instance().setMode(false); ++ }; ++ ++ /// @brief Destructor. ++ virtual ~PingCheckMgrTest() { ++ test_timer_.cancel(); ++ test_io_service_->stopAndPoll(); ++ MultiThreadingMgr::instance().setMode(false); ++ } ++ ++ /// @brief Pretest setup. ++ /// ++ /// Registers the hook point and creates its parking lot. ++ virtual void SetUp() { ++ HooksManager::registerHook("lease4_offer"); ++ parking_lot_ = boost::make_shared<ParkingLotHandle>( ++ ServerHooks::getServerHooks().getParkingLotPtr("lease4_offer")); ++ } ++ ++ /// @brief Ensure we stop cleanly. ++ virtual void TearDown() { ++ if (mgr_) { ++ mgr_->stop(); ++ } ++ ++ HooksManager::clearParkingLots(); ++ } ++ ++ /// @brief Creates the test's manager instance. ++ /// ++ /// @param num_threads number of threads in the thread pool. ++ /// @param min_echos minimum number of echos per ping check. ++ /// @param reply_timeout reply timeout per ping. ++ /// @param start_and_pause when false, the manager is only created, ++ /// when true it is created, started and then paused. This allows ++ /// manipulation of context store contents while the threads are doing ++ /// no work. ++ void createMgr(uint32_t num_threads, ++ uint32_t min_echos = 1, ++ uint32_t reply_timeout = 100, ++ bool start_and_pause = false) { ++ ASSERT_NO_THROW_LOG( ++ mgr_.reset(new TestablePingCheckMgr(num_threads, min_echos, reply_timeout))); ++ ASSERT_TRUE(mgr_); ++ mgr_->setIOService(test_io_service_); ++ ++ if (start_and_pause) { ++ ASSERT_NO_THROW_LOG(mgr_->start()); ++ ++ if (!MultiThreadingMgr::instance().getMode()) { ++ ASSERT_FALSE(mgr_->getThreadPool()); ++ } else { ++ ASSERT_TRUE(mgr_->getThreadPool()); ++ ASSERT_NO_THROW_LOG(mgr_->pause()); ++ ASSERT_TRUE(mgr_->isPaused()); ++ } ++ } ++ } ++ ++ /// @brief Add a new lease and query pair to the test's list of lease query pairs. ++ /// ++ /// Creates a bare-bones DHCPv4 lease and DHCPDISCOVER, wraps them in a ++ /// LeaseQueryPair and adds the pair to the end of the test's internal ++ /// list of pairs, lease_query_pairs_. ++ /// ++ /// @param target IOAddress of the lease. ++ /// @param transid transaction id of the query. ++ /// ++ /// @return A copy of the newly created pair. ++ LeaseQueryPair makeLeaseQueryPair(IOAddress target, uint16_t transid) { ++ // Make a lease and query pair ++ Lease4Ptr lease(new Lease4()); ++ lease->addr_ = IOAddress(target); ++ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, transid)); ++ LeaseQueryPair lqp(lease, query); ++ lease_query_pairs_.push_back(lqp); ++ return (lqp); ++ } ++ ++ /// @brief Start ping checks for a given number of targets. ++ /// ++ /// The function first creates and parks the given number of targets, and ++ /// then starts a ping check for each of them. Parking them all first ++ /// establishes the number of ping checks expected to be conducted during ++ /// the test prior to actually starting any of them. This avoids the ++ /// parking lot from becoming empty part way through the test. ++ /// ++ /// It unpark callback lambda increments the unparked_ counter and then ++ /// pushes the unparked lease/query pair to either the list of frees ++ /// or list of declines. ++ /// ++ /// @param num_targets number of target ip addresses to ping check. ++ /// @param start_address starting target address. Defaults to 127.0.0.1. ++ /// ++ /// @return last target address started. ++ IOAddress startTargets(size_t num_targets, IOAddress start_address = IOAddress("127.0.0.1")) { ++ IOAddress target = start_address; ++ for (auto i = 0; i < num_targets; ++i) { ++ auto lqp = makeLeaseQueryPair(IOAddress(target), i+1); ++ HooksManager::park("lease4_offer", lqp.query_, ++ [this, lqp]() { ++ MultiThreadingLock lock(*mutex_); ++ ++unparked_; ++ auto handle = lqp.query_->getCalloutHandle(); ++ bool offer_address_in_use; ++ handle->getArgument("offer_address_in_use", offer_address_in_use); ++ offer_address_in_use ? declines_.push_back(lqp) : frees_.push_back(lqp); ++ }); ++ ++ try { ++ mgr_->startPing(lqp.lease_, lqp.query_, parking_lot_); ++ } catch (const std::exception& ex) { ++ ADD_FAILURE() << "startPing threw: " << ex.what(); ++ } ++ ++ target = IOAddress::increase(target); ++ } ++ ++ return(target); ++ } ++ ++ /// @brief Fetches the context, by lease address, from the store for a ++ /// given lease query pair. ++ /// ++ /// @param lqp LeaseQueryPair for which the context is desired. ++ /// @return pointer to the found context or an empty pointer. ++ PingContextPtr getContext(const LeaseQueryPair& lqp) { ++ return (getContext(lqp.lease_->addr_)); ++ } ++ ++ /// @brief Fetches the context, by lease address, from the store for address. ++ /// ++ /// @param address lease ip address for which the context is desired. ++ /// @return pointer to the found context or an empty pointer. ++ PingContextPtr getContext(const IOAddress& address) { ++ return (mgr_->getStore()->getContextByAddress(address)); ++ } ++ ++ /// @brief Updates a context in the store. ++ /// ++ /// @param context context to update. ++ void updateContext(PingContextPtr& context) { ++ ASSERT_NO_THROW_LOG(mgr_->getStore()->updateContext(context)); ++ } ++ ++ /// @brief Tests equality of two timestamps within a given tolerance. ++ /// ++ /// The two time stamps are considered equal if the absolute value of their ++ /// difference is between 0 and the specified tolerance (inclusive). ++ /// ++ /// @param lhs first TimeStamp to compare. ++ /// @param rhs second TimeStamp to compare. ++ /// @param tolerance margin of difference allowed for equality in milliseconds. ++ /// Defaults to 10. ++ /// ++ /// @return True if the time stamps are "equal", false otherwise. ++ bool fuzzyEqual(const TimeStamp& lhs, const TimeStamp& rhs, long tolerance = 10) { ++ auto diff = abs(duration_cast<milliseconds>(lhs - rhs).count()); ++ return (diff >= 0 && diff <= tolerance); ++ } ++ ++ /// @brief Tests equality of two longs within a given tolerance. ++ /// ++ /// The two values are considered equal if the absolute value of their ++ /// difference is between 0 and the specified tolerance (inclusive). ++ /// ++ /// @param lhs first value to compare. ++ /// @param rhs second value to compare. ++ /// @param tolerance margin of difference allowed for equality in milliseconds. ++ /// Defaults to 10. ++ /// ++ /// @return True if the time values are "equal", false otherwise. ++ bool fuzzyEqual(const long& lhs, const long& rhs, long tolerance = 10) { ++ auto diff = abs(lhs - rhs); ++ return (diff >= 0 && diff <= tolerance); ++ } ++ ++ /// @brief Creates an ECHO REQUEST message from a given address. ++ /// ++ /// @param target ip address to use as the echo's destination address. ++ /// @return Pointer to the new message. ++ ICMPMsgPtr makeEchoRequest(const IOAddress& target) { ++ ICMPMsgPtr msg(new ICMPMsg()); ++ msg->setType(ICMPMsg::ECHO_REQUEST); ++ msg->setDestination(IOAddress(target)); ++ msg->setSource(IOAddress("127.0.0.1")); ++ return (msg); ++ } ++ ++ /// @brief Creates an ECHO_REPLY message from a given address. ++ /// ++ /// @param from ip address to use as the reply's source address. ++ /// @return Pointer to the new message. ++ ICMPMsgPtr makeEchoReply(const IOAddress& from) { ++ ICMPMsgPtr msg(new ICMPMsg()); ++ msg->setType(ICMPMsg::ECHO_REPLY); ++ msg->setSource(IOAddress(from)); ++ msg->setDestination(IOAddress("127.0.0.1")); ++ return (msg); ++ } ++ ++ /// @brief Creates an TARGET_UNREACHABLE message from a given address. ++ /// ++ /// @param target ip address to use as the reply's source address. ++ /// @return Pointer to the new message. ++ ICMPMsgPtr makeUnreachable(const IOAddress& target) { ++ // Make the TARGET_UNREACHABLE message first. ++ ICMPMsgPtr msg(new ICMPMsg()); ++ msg->setType(ICMPMsg::TARGET_UNREACHABLE); ++ msg->setSource(IOAddress("127.0.0.1")); ++ msg->setDestination(IOAddress("127.0.0.1")); ++ ++ // Now embed the ping target's "original" echo into the unreachable ++ // message's payload. This includes the IP header followed by the ++ // ECHO REQUEST. First make the IP header and add it to the payload. ++ // We only set values we care about. ++ struct ip ip_header; ++ memset((void *)(&ip_header), 0x00, sizeof(struct ip)); ++ ip_header.ip_v = 4; ++ ip_header.ip_hl = 5; /* shift left twice = 20 */ ++ ip_header.ip_len = 48; /* ip_header + echo length */ ++ ip_header.ip_dst.s_addr = htonl(target.toUint32()); ++ ip_header.ip_src.s_addr = htonl(msg->getSource().toUint32()); ++ msg->setPayload((const uint8_t*)(&ip_header), sizeof(struct ip)); ++ ++ // Now make the ECHO_REQUEST, pack it and add that to the payload. ++ ICMPMsgPtr echo = makeEchoRequest(target); ++ ICMPPtr packed_echo = echo->pack(); ++ msg->setPayload((const uint8_t*)(packed_echo.get()), sizeof(struct icmp)); ++ ++ return (msg); ++ } ++ ++ /// @brief Compares a LeaseQueryPair collection to the internal collection ++ /// of pairs created (see makeLeaseQueryPairs()). ++ /// ++ /// @param test_collection Collection of pairs to compare against those in ++ /// the creation collection. ++ void compareLeaseQueryPairs(LeaseQueryPairs& test_collection) { ++ // We should have as many in the test_collection as we have creation ++ // collection. ++ ASSERT_EQ(test_collection.size(), lease_query_pairs_.size()); ++ ++ // Order is not guaranteed so we sort both lists then compare. ++ std::sort(test_collection.begin(), test_collection.end(), ++ [](LeaseQueryPair const& a, LeaseQueryPair const& b) ++ { return (a.lease_->addr_ < b.lease_->addr_); }); ++ ++ std::sort(lease_query_pairs_.begin(), lease_query_pairs_.end(), ++ [](LeaseQueryPair const& a, LeaseQueryPair const& b) ++ { return (a.lease_->addr_ < b.lease_->addr_); }); ++ ++ auto dpi = test_collection.begin(); ++ for (auto const& lqpi : lease_query_pairs_) { ++ ASSERT_EQ((*dpi).lease_->addr_, lqpi.lease_->addr_); ++ ++dpi; ++ } ++ } ++ ++ /// @brief Exercises the operational basics: create, start, and stop ++ /// for TestablePingCheckMgr. ++ /// ++ /// @param num_threads number of threads in the thread pool. ++ void testOperationalBasics(size_t num_threads) { ++ SKIP_IF(notRoot()); ++ ++ // Create manager with the given number of threads. ++ ASSERT_NO_THROW_LOG(createMgr(num_threads)); ++ ASSERT_TRUE(mgr_); ++ ++ // Should not be running. ++ EXPECT_FALSE(mgr_->isRunning()); ++ EXPECT_TRUE(mgr_->isStopped()); ++ EXPECT_FALSE(mgr_->isPaused()); ++ ++ // Channel should not yet exist. ++ ASSERT_FALSE(mgr_->getChannel()); ++ ++ // Start the manager. ++ ASSERT_NO_THROW_LOG(mgr_->start()); ++ ++ // Thread pool should exist in MT mode only. ++ if (MultiThreadingMgr::instance().getMode()) { ++ ASSERT_TRUE(mgr_->getThreadPool()); ++ } else { ++ ASSERT_FALSE(mgr_->getThreadPool()); ++ } ++ ++ // Should be running. ++ EXPECT_TRUE(mgr_->isRunning()); ++ EXPECT_FALSE(mgr_->isStopped()); ++ EXPECT_FALSE(mgr_->isPaused()); ++ ++ // Channel should exist and be open. ++ auto channel = mgr_->getChannel(); ++ ASSERT_TRUE(channel); ++ ASSERT_TRUE(channel->isOpen()); ++ ++ // Context store should exist and be empty. ++ auto store = mgr_->getStore(); ++ ASSERT_TRUE(store); ++ auto pings = store->getAll(); ++ ASSERT_EQ(0, pings->size()); ++ ++ // Destruction should be graceful. ++ ASSERT_NO_THROW_LOG(mgr_.reset()); ++ } ++ ++ /// @brief Verifies that startPing() creates a new context in the store and ++ /// it can be fetched with the nextToSend() callback. ++ void testStartPing() { ++ SKIP_IF(notRoot()); ++ ++ // Create manager with thread-pool size of 3, min_echos 2, reply_timeout 250 ms. ++ // ST mode should ingore requested thread number. ++ ASSERT_NO_THROW_LOG(createMgr(3, 2, 250)); ++ ASSERT_TRUE(mgr_); ++ ++ // Make a lease and query pair ++ auto lqp1 = makeLeaseQueryPair(IOAddress("127.0.0.101"), 101); ++ ++ // Channel isn't open, startPing should throw. ++ ASSERT_THROW_MSG(mgr_->startPing(lqp1.lease_, lqp1.query_, parking_lot_), InvalidOperation, ++ "PingCheckMgr::startPing() - channel isn't open"); ++ ++ // Start the manager. This will open the channel. ++ ASSERT_NO_THROW_LOG(mgr_->start()); ++ ASSERT_TRUE(mgr_->isRunning()); ++ ++ if (mgr_->getThreadPool()) { ++ // Pause the manager so startPing() will succeed but no events will occur. ++ // This should let us add contexts that sit in WAITING_TO_SEND state. ++ ASSERT_NO_THROW_LOG(mgr_->pause()); ++ ASSERT_TRUE(mgr_->isPaused()); ++ } ++ ++ // Call startPing() again. It should work. ++ ASSERT_NO_THROW_LOG(mgr_->startPing(lqp1.lease_, lqp1.query_, parking_lot_)); ++ ++ // Calling startPing() on the same lease should fail, duplicates not allowed. ++ ASSERT_THROW_MSG(mgr_->startPing(lqp1.lease_, lqp1.query_, parking_lot_), DuplicateContext, ++ "PingContextStore::addContex: context already exists for: 127.0.0.101"); ++ ++ // Our context should be present. ++ auto const& store = mgr_->getStore(); ++ auto pings = store->getAll(); ++ ASSERT_EQ(1, pings->size()); ++ PingContextPtr context1; ++ ASSERT_NO_THROW_LOG(context1 = store->getContextByAddress(lqp1.lease_->addr_)); ++ ASSERT_TRUE(context1); ++ ++ // Verify the context's state. ++ EXPECT_EQ(2, context1->getMinEchos()); ++ EXPECT_EQ(250, context1->getReplyTimeout()); ++ EXPECT_EQ(0, context1->getEchosSent()); ++ EXPECT_EQ(PingContext::EMPTY_TIME(), context1->getLastEchoSentTime()); ++ EXPECT_LE(test_start_time_, context1->getSendWaitStart()); ++ EXPECT_EQ(PingContext::EMPTY_TIME(), context1->getNextExpiry()); ++ EXPECT_LE(test_start_time_, context1->getCreatedTime()); ++ EXPECT_EQ(lqp1.lease_, context1->getLease()); ++ EXPECT_EQ(lqp1.query_, context1->getQuery()); ++ EXPECT_EQ(PingContext::WAITING_TO_SEND, context1->getState()); ++ ++ // Sleep a bit to make sure there's a difference in context times. ++ usleep(5); ++ ++ // Make a second lease and query pair ++ auto lqp2 = makeLeaseQueryPair(IOAddress("127.0.0.102"), 102); ++ ++ // Start a ping for lease2. ++ ASSERT_NO_THROW_LOG(mgr_->startPing(lqp2.lease_, lqp2.query_, parking_lot_)); ++ ++ // Both contexts should be present. ++ pings = store->getAll(); ++ ASSERT_EQ(2, pings->size()); ++ ++ // Fetch the second context by address. ++ PingContextPtr context2; ++ ASSERT_NO_THROW_LOG(context2 = store->getContextByAddress(lqp2.lease_->addr_)); ++ ASSERT_TRUE(context2); ++ ++ // Verify the second context's state. ++ EXPECT_EQ(2, context2->getMinEchos()); ++ EXPECT_EQ(250, context2->getReplyTimeout()); ++ EXPECT_EQ(0, context2->getEchosSent()); ++ EXPECT_EQ(PingContext::EMPTY_TIME(), context2->getLastEchoSentTime()); ++ // Its send_wait_start_time_ should be more recent than context1. ++ EXPECT_LE(context1->getSendWaitStart(), context2->getSendWaitStart()); ++ EXPECT_EQ(PingContext::EMPTY_TIME(), context2->getNextExpiry()); ++ // Its created_time_ should be more recent than context1. ++ EXPECT_LE(context1->getCreatedTime(), context2->getCreatedTime()); ++ EXPECT_EQ(lqp2.lease_, context2->getLease()); ++ EXPECT_EQ(lqp2.query_, context2->getQuery()); ++ EXPECT_EQ(PingContext::WAITING_TO_SEND, context2->getState()); ++ } ++ ++ /// @brief Exercises PingCheckMgr::nextToSend(). ++ void testNextToSend() { ++ SKIP_IF(notRoot()); ++ ++ // Create a paused manager. 3 threads, 2 echos, 250 ms timeout. ++ // ST mode should ingore requested thread number. ++ createMgr(3, 2, 250, true); ++ ++ // Calling nextToSend() should return false. ++ IOAddress next("0.0.0.0"); ++ ASSERT_FALSE(mgr_->nextToSend(next)); ++ ++ // Now let's start 3 contexts. ++ size_t num_targets = 3; ++ IOAddress target("127.0.0.1"); ++ for (auto i = 0; i < num_targets; ++i) { ++ auto lqp = makeLeaseQueryPair(IOAddress(target), i+1); ++ ++ // Call startPing(). ++ ASSERT_NO_THROW_LOG(mgr_->startPing(lqp.lease_, lqp.query_, parking_lot_)); ++ target = IOAddress::increase(target); ++ ++ PingContextPtr context = getContext(lqp); ++ ASSERT_TRUE(context); ++ ++ // Verify the context's initial state is correct. ++ EXPECT_EQ(0, context->getEchosSent()); ++ EXPECT_EQ(PingContext::EMPTY_TIME(), context->getLastEchoSentTime()); ++ EXPECT_LE(test_start_time_, context->getSendWaitStart()); ++ EXPECT_EQ(PingContext::EMPTY_TIME(), context->getNextExpiry()); ++ EXPECT_LE(test_start_time_, context->getCreatedTime()); ++ EXPECT_EQ(PingContext::WAITING_TO_SEND, context->getState()); ++ ++ // Sleep a few before we add the next one to ensure ordering by ++ // time is consistent. ++ usleep(5); ++ } ++ ++ // Consecutive calls to nextToSend() should return target addresses ++ // in the order they were created. ++ for (auto const& lqp : lease_query_pairs_) { ++ // Next to send should return the next address to send. ++ ASSERT_TRUE(mgr_->nextToSend(next)); ++ ++ // It should match the lease as created. ++ ASSERT_EQ(next, lqp.lease_->addr_); ++ ++ // Fetch the corresponding context. ++ PingContextPtr context = getContext(next); ++ ASSERT_TRUE(context); ++ ++ // Verify the state has properly moved to SENDING. ++ EXPECT_EQ(0, context->getEchosSent()); ++ EXPECT_EQ(PingContext::EMPTY_TIME(), context->getLastEchoSentTime()); ++ EXPECT_EQ(PingContext::EMPTY_TIME(), context->getNextExpiry()); ++ EXPECT_EQ(PingContext::SENDING, context->getState()); ++ } ++ ++ // A final call to nextToSend should return false. ++ ASSERT_FALSE(mgr_->nextToSend(next)); ++ } ++ ++ /// @brief Exercises PingCheckMgr::setNextExpiration. ++ void testSetNextExpiration() { ++ SKIP_IF(notRoot()); ++ ++ // Create a paused manager. 3 threads, 2 echos, 500 ms timeout. ++ // ST mode should ingore requested thread number. ++ createMgr(3, 2, 500, true); ++ ++ // Should not have an expiration time, timer should not be running. ++ ASSERT_EQ(PingContext::EMPTY_TIME(), mgr_->getNextExpiry()); ++ ASSERT_EQ(mgr_->getExpirationTimerInterval(), 0); ++ ++ // Now let's start 3 contexts. ++ size_t num_targets = 3; ++ IOAddress target("127.0.0.1"); ++ for (auto i = 0; i < num_targets; ++i) { ++ auto lqp = makeLeaseQueryPair(IOAddress(target), i+1); ++ ++ // Call startPing(). ++ ASSERT_NO_THROW_LOG(mgr_->startPing(lqp.lease_, lqp.query_, parking_lot_)); ++ target = IOAddress::increase(target); ++ } ++ ++ // Still should not have an expiration time nor running timer. ++ ASSERT_EQ(PingContext::EMPTY_TIME(), mgr_->getNextExpiry()); ++ EXPECT_EQ(mgr_->getExpirationTimerInterval(), 0); ++ ++ // Simulate a completed send for the second context. ++ PingContextPtr context2; ++ context2 = getContext(lease_query_pairs_[1]); ++ ASSERT_TRUE(context2); ++ context2->beginWaitingForReply(test_start_time_ - milliseconds(50)); ++ updateContext(context2); ++ ++ // Call setNextExpiration(). ++ ASSERT_NO_THROW_LOG(mgr_->setNextExpiration()); ++ ++ // Refresh the context. ++ context2 = getContext(lease_query_pairs_[1]); ++ ++ // Verify the mgr has the same next expiration as the context and ++ // that the expiration timer is running. Allow for some fudge in ++ // the checks. ++ auto original_mgr_expiry = mgr_->getNextExpiry(); ++ EXPECT_TRUE(fuzzyEqual(original_mgr_expiry, context2->getNextExpiry())); ++ ++ auto original_interval = mgr_->getExpirationTimerInterval(); ++ EXPECT_TRUE(fuzzyEqual(original_interval, 450)); ++ ++ // Simulate a completed send for the third context. ++ PingContextPtr context3; ++ context3 = getContext(lease_query_pairs_[2]); ++ ASSERT_TRUE(context3); ++ context3->beginWaitingForReply(); ++ updateContext(context3); ++ ++ // Call setNextExpiration(). ++ ASSERT_NO_THROW_LOG(mgr_->setNextExpiration()); ++ ++ // Refresh the context. ++ context3 = getContext(lease_query_pairs_[2]); ++ ++ // Context3 should have a later expiration than context2. ++ EXPECT_LT(context2->getNextExpiry(), context3->getNextExpiry()); ++ ++ // Expiration and timer should still match the original values based on ++ // the second context. ++ EXPECT_TRUE(fuzzyEqual(mgr_->getNextExpiry(), original_mgr_expiry)); ++ EXPECT_EQ(mgr_->getExpirationTimerInterval(), original_interval); ++ ++ // Simulate a completed send for the first context but use a smaller ++ // timeout and back date it. ++ PingContextPtr context1; ++ context1 = getContext(lease_query_pairs_[0]); ++ ASSERT_TRUE(context1); ++ context1->setReplyTimeout(50); ++ context1->beginWaitingForReply(test_start_time_ - milliseconds(1)); ++ updateContext(context1); ++ ++ // Call setNextExpiration(). ++ ASSERT_NO_THROW_LOG(mgr_->setNextExpiration()); ++ ++ // Refresh the context. ++ context1 = getContext(lease_query_pairs_[0]); ++ ++ // Context1 should have a earlier expiration than context2. ++ EXPECT_LT(context1->getNextExpiry(), context2->getNextExpiry()); ++ // Timer interval should be based on context1. ++ EXPECT_TRUE(fuzzyEqual(mgr_->getExpirationTimerInterval(), 50, 20)) ++ << " interval: " << mgr_->getExpirationTimerInterval(); ++ ++ // Move all contexts to TARGET_FREE. This should leave none ++ // still waiting. ++ context1->setState(PingContext::TARGET_FREE); ++ updateContext(context1); ++ context2->setState(PingContext::TARGET_FREE); ++ updateContext(context2); ++ context3->setState(PingContext::TARGET_FREE); ++ updateContext(context3); ++ ++ // Call setNextExpiration(). ++ ASSERT_NO_THROW_LOG(mgr_->setNextExpiration()); ++ ++ // Should not have an expiration time, timer should not be running. ++ ASSERT_EQ(PingContext::EMPTY_TIME(), mgr_->getNextExpiry()); ++ ASSERT_EQ(mgr_->getExpirationTimerInterval(), 0); ++ } ++ ++ /// @brief Exercises PingCheckMgr::sendCompleted. ++ void testSendCompleted() { ++ SKIP_IF(notRoot()); ++ ++ // Create a paused manager. 3 threads, 2 echos, 500 ms timeout. ++ // ST mode should ingore requested thread number. ++ createMgr(3, 2, 500, true); ++ ++ // Start a ping for an address so we have a context. ++ IOAddress target("127.0.0.2"); ++ auto lqp = makeLeaseQueryPair(IOAddress(target), 102); ++ ++ // Call startPing(). ++ ASSERT_NO_THROW_LOG(mgr_->startPing(lqp.lease_, lqp.query_, parking_lot_)); ++ ++ // Simulate a completed send for the context. ++ PingContextPtr context; ++ context = getContext(lqp); ++ ASSERT_TRUE(context); ++ ++ // Make an ECHO REQUEST packet based on context. ++ ICMPMsgPtr echo_request = makeEchoRequest(context->getLease()->addr_); ++ ++ // Invoke sendCompleted() with fabricated request. Should succeed. ++ ASSERT_NO_THROW_LOG(mgr_->sendCompleted(echo_request, false)); ++ ++ // Refresh the context. ++ context = getContext(context->getLease()->addr_); ++ ++ EXPECT_EQ(PingContext::WAITING_FOR_REPLY, context->getState()); ++ EXPECT_EQ(1, context->getEchosSent()); ++ EXPECT_GE(context->getLastEchoSentTime(), test_start_time_); ++ ++ // Verify the mgr has the same next expiration as the context and ++ // that the expiration timer is running. Allow for some fudge in ++ // the checks. ++ EXPECT_GT(context->getNextExpiry(), test_start_time_); ++ EXPECT_TRUE(fuzzyEqual(mgr_->getNextExpiry(), context->getNextExpiry())); ++ EXPECT_TRUE(fuzzyEqual(mgr_->getExpirationTimerInterval(), 500)); ++ ++ // Make an ECHO REQUEST packet for an address that has no context. ++ echo_request = makeEchoRequest(IOAddress("192.168.0.1")); ++ ++ // Invoking sendCompleted() with request for a non-existent address be harmless. ++ ASSERT_NO_THROW_LOG(mgr_->sendCompleted(echo_request, false)); ++ ++ // Invoking sendCompleted() with an invalid message type should be harmless. ++ echo_request->setType(ICMPMsg::ECHO_REPLY); ++ ASSERT_NO_THROW_LOG(mgr_->sendCompleted(ICMPMsgPtr(), false)); ++ ++ // Invoking sendCompleted() with an empty message should be harmless. ++ echo_request.reset(); ++ ASSERT_NO_THROW_LOG(mgr_->sendCompleted(ICMPMsgPtr(), false)); ++ ++ // Verify expiration values should not have not been altered. ++ EXPECT_TRUE(fuzzyEqual(mgr_->getNextExpiry(), context->getNextExpiry())); ++ EXPECT_TRUE(fuzzyEqual(mgr_->getExpirationTimerInterval(), 500)); ++ } ++ ++ /// @brief Exercises PingCheckMgr::replyReceived() for ECHO REPLYs. Note this ++ /// also exercises handleEchoReply(). ++ void testReplyReceivedForEchoReply() { ++ SKIP_IF(notRoot()); ++ ++ // Create a paused manager. 3 threads, 2 echos, 500 ms timeout. ++ // ST mode should ingore requested thread number. ++ createMgr(3, 2, 500, true); ++ ++ // Install a post reply received callback to stop the test if we're done. ++ mgr_->post_reply_received_cb_ = ++ [this](const ICMPMsgPtr& /* reply */) { ++ MultiThreadingLock lock(*mutex_); ++ if (mgr_->parkingLotSize() == 0) { ++ stopTestService(); ++ return; ++ } ++ }; ++ ++ // Turn off loopback routing. ++ mgr_->getChannel()->route_loopback_ = false; ++ ++ // Start a ping for an address so we have a context. ++ startTargets(1); ++ auto lqp = lease_query_pairs_[0]; ++ ++ // Simulate a completed send for the context. ++ PingContextPtr context; ++ context = getContext(lqp); ++ ASSERT_TRUE(context); ++ ++ // Make an ECHO REQUEST packet based on context and invoke sendCompleted(). ++ ICMPMsgPtr echo_request = makeEchoRequest(context->getLease()->addr_); ++ ASSERT_NO_THROW_LOG(mgr_->sendCompleted(echo_request, false)); ++ ++ // Should still have one parked query. ++ EXPECT_EQ(1, mgr_->parkingLotSize()); ++ ++ // Verify the expiration timer is running. ++ EXPECT_TRUE(fuzzyEqual(mgr_->getExpirationTimerInterval(), 500)); ++ ++ // Make an ECHO REPLY packet based on context and invoke replyReceived(). ++ ICMPMsgPtr echo_reply = makeEchoReply(context->getLease()->addr_); ++ ASSERT_NO_THROW_LOG(mgr_->replyReceived(echo_reply)); ++ ++ // Verify the expiration timer is no longer running. ++ EXPECT_EQ(mgr_->getExpirationTimerInterval(), 0); ++ ++ // The context should no longer be in the store. ++ EXPECT_FALSE(getContext(lqp)); ++ ++ // We should have dropped the query from the lot rather than unparking it. ++ EXPECT_EQ(mgr_->parkingLotSize(), 0); ++ EXPECT_EQ(unparked_, 1); ++ ++ // We should have one decline that matches our lease query pair. ++ compareLeaseQueryPairs(declines_); ++ ++ // Make an ECHO REPLY packet for an address that has no context. ++ echo_reply = makeEchoReply(IOAddress("192.168.0.1")); ++ ++ // Invoke replyReceived() for a reply with no matching context, ++ // it should not throw. ++ ASSERT_NO_THROW_LOG(mgr_->PingCheckMgr::replyReceived(echo_reply)); ++ ++ // Invoke replyReceived() an empty message, it should not throw. ++ // (Bypass test implementation for this check). ++ echo_reply.reset(); ++ ASSERT_NO_THROW_LOG(mgr_->PingCheckMgr::replyReceived(echo_reply)); ++ } ++ ++ /// @brief Exercises PingCheckMgr::replyReceived() for UNREACHABLEs. Note this ++ /// also exercises handleTargetUnreachable(). ++ void testReplyReceivedForTargetUnreachable() { ++ SKIP_IF(notRoot()); ++ ++ // Create a paused manager. 3 threads, 2 echos, 500 ms timeout. ++ // ST mode should ingore requested thread number. ++ createMgr(3, 2, 500, true); ++ ++ // Install a post reply received callback to stop the test if we're done. ++ mgr_->post_reply_received_cb_ = ++ [this](const ICMPMsgPtr& /* reply */) { ++ MultiThreadingLock lock(*mutex_); ++ if (mgr_->parkingLotSize() == 0) { ++ stopTestService(); ++ return; ++ } ++ }; ++ ++ // Turn off loopback routing. ++ mgr_->getChannel()->route_loopback_ = false; ++ ++ // Start a ping for an address so we have a context. ++ startTargets(1); ++ auto lqp = lease_query_pairs_[0]; ++ ++ // Simulate a completed send for the context. ++ PingContextPtr context; ++ context = getContext(lqp); ++ ASSERT_TRUE(context); ++ ++ // Make an ECHO REQUEST packet based on context and invoke sendCompleted(). ++ ICMPMsgPtr echo_request = makeEchoRequest(context->getLease()->addr_); ++ ASSERT_NO_THROW_LOG(mgr_->sendCompleted(echo_request, false)); ++ ++ // Should still have one parked query. ++ EXPECT_EQ(1, mgr_->parkingLotSize()); ++ ++ // Verify the expiration timer is running. ++ EXPECT_TRUE(fuzzyEqual(mgr_->getExpirationTimerInterval(), 500)); ++ ++ // Make an ECHO REPLY packet based on context and invoke replyReceived(). ++ ICMPMsgPtr unreachable = makeUnreachable(context->getLease()->addr_); ++ ASSERT_NO_THROW_LOG(mgr_->replyReceived(unreachable)); ++ ++ // Verify the expiration timer is no longer running. ++ EXPECT_EQ(mgr_->getExpirationTimerInterval(), 0); ++ ++ // The context should no longer be in the store. ++ EXPECT_FALSE(getContext(lqp)); ++ ++ // We should have unparked the query from the lot. ++ EXPECT_EQ(mgr_->parkingLotSize(), 0); ++ EXPECT_EQ(unparked_, 1); ++ ++ // We should have one free that matches our lease query pair. ++ compareLeaseQueryPairs(frees_); ++ ++ // Invoke replyReceived() for an unreachable with no matching context, ++ // it should not throw. ++ unreachable = makeUnreachable(IOAddress("192.168.0.1")); ++ ASSERT_NO_THROW_LOG(mgr_->replyReceived(unreachable)); ++ } ++ ++ /// @brief Verifies expiration processing by invoking expirationTimedout(). ++ /// This also exercises processExpiredSince(), doNextEcho(), finishFree(), ++ /// and setNextExpiration(). ++ void testExpirationProcessing() { ++ SKIP_IF(notRoot()); ++ ++ // Create a paused manager. 3 threads, 1 echos, 250 ms timeout. ++ // ST mode should ingore requested thread number. ++ createMgr(3, 1, 250, true); ++ ++ // Start four ping checks, then stage them so: ++ // ++ // First context is WAITING_TO_SEND, no expiry. ++ // Second context is WAITING_FOR_REPLY, has expired and has ++ // exhausted min_echos_. ++ // Third context is WAITING_FOR_REPLY, has expired but has ++ // not exhausted min_echos_. ++ // Fourth context is WAITING_FOR_REPLY but has not yet expired. ++ // ++ size_t num_targets = 4; ++ ++ // Start the desired number of targets with an unpark callback ++ // that increments the unparked count. ++ startTargets(num_targets); ++ ++ // Now establish the desired state for each context. ++ // First context is in WAITING_TO_SEND, no expiry. ++ PingContextPtr context1 = getContext(lease_query_pairs_[0]); ++ ASSERT_TRUE(context1); ++ EXPECT_EQ(context1->getState(), PingContext::WAITING_TO_SEND); ++ ++ // Second context setup: expired and has exhausted min_echos_ ++ PingContextPtr context2 = getContext(lease_query_pairs_[1]); ++ ASSERT_TRUE(context2); ++ context2->beginWaitingForReply(test_start_time_ - milliseconds(500)); ++ updateContext(context2); ++ ++ // Third context setup: expired but has not exhausted min_echos_ ++ PingContextPtr context3 = getContext(lease_query_pairs_[2]); ++ ASSERT_TRUE(context3); ++ context3->setMinEchos(2); ++ context3->beginWaitingForReply(test_start_time_ - milliseconds(500)); ++ updateContext(context3); ++ ++ // Fourth context setup: has not yet expired ++ PingContextPtr context4 = getContext(lease_query_pairs_[3]); ++ ASSERT_TRUE(context4); ++ context4->beginWaitingForReply(test_start_time_); ++ updateContext(context4); ++ ++ // Now invoke expirationTimedout(). ++ ASSERT_NO_THROW_LOG(mgr_->expirationTimedOut()); ++ ++ // Verify the contexts are in the expected states. ++ // Context1 should still be WAITING_TO_SEND. ++ context1 = getContext(lease_query_pairs_[0]); ++ ASSERT_TRUE(context1); ++ EXPECT_EQ(context1->getState(), PingContext::WAITING_TO_SEND); ++ ++ // Context2 should be gone by unparking and its address freed. ++ IOAddress address = lease_query_pairs_[1].lease_->addr_; ++ context2 = getContext(address); ++ ASSERT_FALSE(context2); ++ EXPECT_EQ(unparked_, 1); ++ ASSERT_EQ(frees_.size(), 1); ++ EXPECT_EQ(frees_[0].lease_->addr_, address); ++ ++ // Context3 should be in WAITING_TO_SEND. ++ context3 = getContext(lease_query_pairs_[2]); ++ ASSERT_TRUE(context3); ++ EXPECT_EQ(context3->getState(), PingContext::WAITING_TO_SEND); ++ ++ // Context4 should still be WAITING_FOR_REPLY. ++ context4 = getContext(lease_query_pairs_[3]); ++ ASSERT_TRUE(context4); ++ EXPECT_EQ(context4->getState(), PingContext::WAITING_FOR_REPLY); ++ ++ // Manager's next_expiry_ should be based on context4? ++ EXPECT_TRUE(fuzzyEqual(mgr_->getNextExpiry(), context4->getNextExpiry())); ++ } ++ ++ /// @brief Generates a number of ping checks to local loop back addresses. ++ /// ++ /// Pings should all result in ECHO_REPLYs that get "declined". Declined ++ /// addresses are added to a list. Test completion is gated by the parking ++ /// lot becoming empty or test times out. ++ void testMultiplePingsWithReply() { ++ SKIP_IF(notRoot()); ++ ++ // Create manager with thread-pool size of 3, min_echos 1, ++ // reply_timeout 1000 milliseconds. Larger time out for this test ++ // avoids sporadic expirations which leads to unaccounted for UNPARKs. ++ // ST mode should ingore requested thread number. ++ ASSERT_NO_THROW_LOG(createMgr(3, 1, 1000)); ++ ASSERT_TRUE(mgr_); ++ ++ // Install a post reply received callback to stop the test if we're done. ++ int num_targets = 25; ++ mgr_->post_reply_received_cb_ = ++ [this, num_targets](const ICMPMsgPtr& /* reply */) { ++ MultiThreadingLock lock(*mutex_); ++ if (unparked_ == num_targets) { ++ stopTestService(); ++ return; ++ } ++ }; ++ ++ // Start the manager. This will open the channel. ++ ASSERT_NO_THROW_LOG(mgr_->start()); ++ ASSERT_TRUE(mgr_->isRunning()); ++ ++ // Start the ping checks. ++ startTargets(num_targets); ++ ++ // Run the main thread's IOService until we complete or timeout. ++ ASSERT_NO_THROW_LOG(runIOService()); ++ ++ // Stop the thread pool. ++ ASSERT_NO_THROW_LOG(mgr_->stop()); ++ ASSERT_TRUE(mgr_->isStopped()); ++ ++ // Calling nextToSend() should return false. ++ IOAddress next("0.0.0.0"); ++ ASSERT_FALSE(mgr_->nextToSend(next)); ++ ++ // We should have as many declines as we have pairs created. ++ compareLeaseQueryPairs(declines_); ++ } ++ ++ /// @brief Generates a large number of ping checks to local loop back addresses. ++ /// ++ /// A pause is induced approximately halfway through the number of replies ++ /// at which point the manager is paused and then resumed. This is intended ++ /// to demonstrate the ability to pause and resume the manager gracefully. ++ /// The pings should all result in ECHO_REPLYs that get "declined". Declined ++ /// addresses are added to a list. Test completion is gated by the parking ++ /// lot becoming empty or test times out. ++ void testMultiplePingsWithReplyAndPause() { ++ SKIP_IF(notRoot()); ++ ++ // Create manager with thread-pool size of 3, min_echos 1, ++ // reply_timeout 1000 milliseconds. Larger time out for this test ++ // avoids sporadic expirations which leads to unaccounted for UNPARKs. ++ // ST mode should ingore requested thread number. ++ ASSERT_NO_THROW_LOG(createMgr(3, 1, 1000)); ++ ASSERT_TRUE(mgr_); ++ ++ // Generate ping checks to the desired number of targets. ++ // Set up the pause callback to pause at half the number of ++ // expected replies. ++ size_t num_targets = 24; ++ size_t reply_cnt = 0; ++ size_t pause_at = num_targets / 2; ++ bool test_paused = false; ++ ++ // Install post reply callback to stop the test thread when we reach ++ // the pause count. ++ mgr_->post_reply_received_cb_ = ++ [this, &reply_cnt, &test_paused, &pause_at](const ICMPMsgPtr& reply) { ++ MultiThreadingLock lock(*mutex_); ++ if (reply->getType() == ICMPMsg::ECHO_REPLY) { ++ ++reply_cnt; ++ if (pause_at && (reply_cnt >= pause_at)) { ++ test_paused = true; ++ stopTestService(); ++ pause_at = 0; ++ } ++ } ++ }; ++ ++ // Start the manager. This will open the channel. ++ ASSERT_NO_THROW_LOG(mgr_->start()); ++ ASSERT_TRUE(mgr_->isRunning()); ++ ASSERT_NO_THROW_LOG(mgr_->pause()); ++ ++ // Start 1/2 desired number of ping checks. ++ startTargets(num_targets / 2); ++ ++ // Run the main thread's IOService until we pause or timeout. ++ ASSERT_NO_THROW_LOG(mgr_->resume()); ++ ASSERT_TRUE(mgr_->isRunning()); ++ ASSERT_NO_THROW_LOG(runIOService()); ++ ++ // Manager should still be running. Pause it. ++ ASSERT_TRUE(mgr_->isRunning()); ++ if (mgr_->getThreadPool()) { ++ ASSERT_NO_THROW_LOG(mgr_->pause()); ++ ASSERT_TRUE(mgr_->isPaused()); ++ } ++ ++ // Verify that the pause callback is why we stopped, that we ++ // received at least as many as we should have before pause ++ // and that we have more work to do. The test is a range as ++ // pausing does not happen exactly at the same point from test ++ // run to test run. ++ ASSERT_TRUE(test_paused); ++ ASSERT_TRUE((reply_cnt >= pause_at) && (reply_cnt < num_targets)) ++ << "reply_cnt " << reply_cnt ++ << ", pause_at " << pause_at ++ << ", num_targets " << num_targets; ++ ++ mgr_->post_reply_received_cb_ = ++ [this, num_targets](const ICMPMsgPtr& /* reply */) { ++ MultiThreadingLock lock(*mutex_); ++ if (unparked_ == num_targets) { ++ stopTestService(); ++ return; ++ } ++ }; ++ ++ // Start second batch of targets. ++ startTargets(num_targets / 2, IOAddress("127.0.0.15")); ++ ++ ASSERT_NO_THROW_LOG(mgr_->resume()); ++ ASSERT_TRUE(mgr_->isRunning()); ++ ++ // Restart the main thread's IOService until we complete or timeout. ++ ASSERT_NO_THROW_LOG(runIOService()); ++ ++ ASSERT_NO_THROW_LOG(mgr_->stop()); ++ ASSERT_TRUE(mgr_->isStopped()); ++ ++ // Calling nextToSend() should return false. ++ IOAddress next("0.0.0.0"); ++ ASSERT_FALSE(mgr_->nextToSend(next)); ++ ++ // We should have as many declines as we have pairs created. ++ compareLeaseQueryPairs(declines_); ++ } ++ ++ /// @brief Verifies that a recoverable error completion in sendCompleted() results ++ /// in the target address being free to use. In other words, it should have ++ /// the same outcome as the receiving a TARGET_UNREACHABLE reply from the OS. ++ void testSendCompletedSendFailed() { ++ SKIP_IF(notRoot()); ++ ++ // Create manager with thread-pool size of 3, min_echos 1, ++ // reply_timeout 250 milliseconds. ++ // ST mode should ingore requested thread number. ++ ASSERT_NO_THROW_LOG(createMgr(3, 1, 250)); ++ ASSERT_TRUE(mgr_); ++ ++ // Install a post send completed callback to stop the test if we're done. ++ mgr_->post_send_completed_cb_ = ++ [this](const ICMPMsgPtr& /* echo */, bool send_failed) { ++ MultiThreadingLock lock(*mutex_); ++ if (send_failed) { ++ stopTestService(); ++ } ++ }; ++ ++ // Start the manager. ++ ASSERT_NO_THROW_LOG(mgr_->start()); ++ ++ // Set the test channel to complete the first send with a network_unreachable ++ // error. This saves us from trying to determine an address in the test ++ // environment that would cause it. ++ mgr_->getChannel()->ec_on_write_number_ = 1; ++ mgr_->getChannel()->write_error_ec_ = make_error_code(network_unreachable); ++ ++ // Start a ping for one target. ++ startTargets(1); ++ auto lqp = lease_query_pairs_[0]; ++ ++ // Run the main thread's IOService until we complete or timeout. ++ ASSERT_NO_THROW_LOG(runIOService()); ++ ++ // Verify the expiration timer is no longer running. ++ EXPECT_EQ(mgr_->getExpirationTimerInterval(), 0); ++ ++ // The context should no longer be in the store. ++ EXPECT_FALSE(getContext(lqp)); ++ ++ // We should have unparked the query from the lot. ++ EXPECT_EQ(mgr_->parkingLotSize(), 0); ++ EXPECT_EQ(unparked_, 1); ++ ++ // We should have one free that matches our lease query pair. ++ compareLeaseQueryPairs(frees_); ++ } ++ ++ /// @brief Exercises shouldPing(). ++ void testShouldPingTest() { ++ SKIP_IF(notRoot()); ++ ++ // Create manager with thread-pool size of 3, min_echos 1, ++ // reply_timeout 250 milliseconds. ++ // ST mode should ingore requested thread number. ++ ASSERT_NO_THROW_LOG(createMgr(3, 1, 250)); ++ ASSERT_TRUE(mgr_); ++ ++ // Make a default config. ++ PingCheckConfigPtr config(new PingCheckConfig()); ++ ++ // Make a lease query pair. ++ auto lqp1 = makeLeaseQueryPair(IOAddress("127.0.0.2"), 111); ++ const uint8_t id1[] = { 0x31, 0x32, 0x33, 0x34 }; ++ ClientIdPtr cid1(new ClientId(id1, sizeof(id1))); ++ lqp1.lease_->client_id_ = cid1; ++ ++ Lease4Ptr empty_lease; ++ CalloutHandle::CalloutNextStep status; ++ ++ // Ping checking enabled, no old lease, channel doesn't exist, should return CONTINUE. ++ ASSERT_TRUE(config->getEnablePingCheck()); ++ ASSERT_NO_THROW_LOG(status = mgr_->shouldPing(lqp1.lease_, lqp1.query_, empty_lease, config)); ++ EXPECT_EQ(status, CalloutHandle::NEXT_STEP_CONTINUE); ++ ++ // Start the manager, then pause it. This lets us start pings without ++ // them changing state. ++ ASSERT_NO_THROW_LOG(mgr_->start()); ++ ASSERT_NO_THROW_LOG(mgr_->pause()); ++ ++ // Ping checking disabled, no old lease, should return CONTINUE. ++ config->setEnablePingCheck(false); ++ ASSERT_NO_THROW_LOG(status = mgr_->shouldPing(lqp1.lease_, lqp1.query_, empty_lease, config)); ++ EXPECT_EQ(status, CalloutHandle::NEXT_STEP_CONTINUE); ++ ++ // Ping checking enabled, no old lease, should return PARK. ++ config->setEnablePingCheck(true); ++ ASSERT_NO_THROW_LOG(status = mgr_->shouldPing(lqp1.lease_, lqp1.query_, empty_lease, config)); ++ EXPECT_EQ(status, CalloutHandle::NEXT_STEP_PARK); ++ ++ // Make an old lease based on the first lease. ++ time_t now = time(0); ++ Lease4Ptr old_lease(new Lease4(*(lqp1.lease_))); ++ ++ // Prior lease belonging to the same client with cltt greater than ping-cltt-secs ++ // should return PARK. ++ old_lease->cltt_ = now - config->getPingClttSecs() * 2; ++ ASSERT_NO_THROW_LOG(status = mgr_->shouldPing(lqp1.lease_, lqp1.query_, old_lease, config)); ++ EXPECT_EQ(status, CalloutHandle::NEXT_STEP_PARK); ++ ++ // Prior lease belonging to the same client but with cltt less than ping-cltt-secs ++ // should return CONTINUE. ++ old_lease->cltt_ = now - config->getPingClttSecs() / 2; ++ ASSERT_NO_THROW_LOG(status = mgr_->shouldPing(lqp1.lease_, lqp1.query_, old_lease, config)); ++ EXPECT_EQ(status, CalloutHandle::NEXT_STEP_CONTINUE); ++ ++ // Prior lease belonging to a different client, should return PARK. ++ const uint8_t id2[] = { 0x35, 0x36, 0x37, 0x34 }; ++ old_lease->client_id_.reset(new ClientId(id2, sizeof(id2))); ++ ASSERT_NO_THROW_LOG(status = mgr_->shouldPing(lqp1.lease_, lqp1.query_, old_lease, config)); ++ EXPECT_EQ(status, CalloutHandle::NEXT_STEP_PARK); ++ ++ // Now let's start a ping for the lease-query pair. ++ ASSERT_NO_THROW_LOG(mgr_->startPing(lqp1.lease_, lqp1.query_, parking_lot_)); ++ ++ // Make a second lease query pair. Same address, different client. ++ auto lqp2 = makeLeaseQueryPair(IOAddress("127.0.0.2"), 333); ++ lqp2.lease_->client_id_ = old_lease->client_id_; ++ ++ // Trying to start a ping for an address already being checked should return DROP. ++ ASSERT_NO_THROW_LOG(status = mgr_->shouldPing(lqp2.lease_, lqp2.query_, empty_lease, config)); ++ EXPECT_EQ(status, CalloutHandle::NEXT_STEP_DROP); ++ ++ // Stop the mgr. ++ ASSERT_NO_THROW(mgr_->stop()); ++ ++ // Ping checking enabled, no old lease, channel isn't open, should return CONTINUE. ++ ASSERT_TRUE(config->getEnablePingCheck()); ++ ASSERT_NO_THROW_LOG(status = mgr_->shouldPing(lqp1.lease_, lqp1.query_, empty_lease, config)); ++ EXPECT_EQ(status, CalloutHandle::NEXT_STEP_CONTINUE); ++ } ++ ++ /// @brief Exercise's getScopedConfig(). ++ void testGetScopedConfig() { ++ CfgMgr::instance().setFamily(AF_INET); ++ ++ // Start with empty cache, any subnet that hasn't been seen should get parsed ++ // and, if valid, added to the cache. ++ CfgMgr& cfg_mgr = CfgMgr::instance(); ++ CfgSubnets4Ptr subnets = cfg_mgr.getStagingCfg()->getCfgSubnets4(); ++ ++ // Subnet 1 has no ping-check config. Should return global config. ++ ElementPtr user_context = Element::createMap(); ++ Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.1.0"), 24, 30, 40, 60, 1)); ++ subnet->setContext(user_context); ++ subnets->add(subnet); ++ ++ // Subnet 2 has invalid ping-check content. Should return global config. ++ std::string invalid_json_cfg = ++ R"({ ++ "ping-check": { ++ "enable-ping-check" : true, ++ "bogus-key-word" : true ++ } ++ })"; ++ ++ user_context = Element::fromJSON(invalid_json_cfg); ++ subnet.reset(new Subnet4(IOAddress("192.0.2.0"), 24, 30, 40, 60, 2)); ++ subnet->setContext(user_context); ++ subnets->add(subnet); ++ ++ // Subnet 3 has valid ping check. Should return subnet config ++ std::string valid_json_cfg = ++ R"({ ++ "ping-check": { ++ "enable-ping-check" : true, ++ "min-ping-requests" : 13 ++ } ++ })"; ++ ++ user_context = Element::fromJSON(valid_json_cfg); ++ subnet.reset(new Subnet4(IOAddress("192.0.3.0"), 24, 30, 40, 60, 3)); ++ subnet->setContext(user_context); ++ subnets->add(subnet); ++ ++ // Commit the subnet configuration. ++ cfg_mgr.commit(); ++ ++ // Create manager with thread-pool size of 3, min_echos 2, reply_timeout 250 ms. ++ ASSERT_NO_THROW_LOG(createMgr(3, 2, 250)); ++ ASSERT_TRUE(mgr_); ++ ++ Lease4Ptr lease(new Lease4()); ++ PingCheckConfigPtr config; ++ ++ // Should get the global configuration for subnet 1. ++ lease->addr_ = IOAddress("192.0.1.1"); ++ lease->subnet_id_ = 1; ++ ASSERT_NO_THROW_LOG(config = mgr_->getScopedConfig(lease)); ++ ASSERT_TRUE(config); ++ ASSERT_EQ(config, mgr_->getGlobalConfig()); ++ ++ // Should get the global configuration for subnet 2. ++ lease->addr_ = IOAddress("192.0.2.1"); ++ lease->subnet_id_ = 2; ++ ASSERT_NO_THROW_LOG(config = mgr_->getScopedConfig(lease)); ++ ASSERT_TRUE(config); ++ ASSERT_EQ(config, mgr_->getGlobalConfig()); ++ ++ // Should get subnet configuration for subnet 3. ++ lease->addr_ = IOAddress("192.0.3.1"); ++ lease->subnet_id_ = 3; ++ ASSERT_NO_THROW_LOG(config = mgr_->getScopedConfig(lease)); ++ ASSERT_TRUE(config); ++ ASSERT_NE(config, mgr_->getGlobalConfig()); ++ EXPECT_EQ(config->getMinPingRequests(), 13); ++ } ++ ++ /// @brief Exercises checkSuspended(). ++ /// ++ /// This is intended to verify that ping checking is suspended and resumed based ++ /// on the DHCP service state, not to verify every place that checkSuspended() ++ /// is called. ++ void testCheckSuspended() { ++ SKIP_IF(notRoot()); ++ ++ // Create manager with thread-pool size of 3, min_echos 1, ++ // reply_timeout 250 milliseconds. ++ ASSERT_NO_THROW_LOG(createMgr(3, 1, 250)); ++ ASSERT_TRUE(mgr_); ++ ++ // Make a default config. ++ PingCheckConfigPtr config(new PingCheckConfig()); ++ ++ // Give the manager a NetworkState instance. ++ NetworkStatePtr network_state(new NetworkState()); ++ mgr_->setNetworkState(network_state); ++ ++ // Verify that ping checking is not suspended. ++ ASSERT_FALSE(mgr_->checkSuspended()); ++ ++ // Start the manager, then pause it. This lets us start pings without ++ // them changing state. ++ ASSERT_NO_THROW_LOG(mgr_->start()); ++ ASSERT_NO_THROW_LOG(mgr_->pause()); ++ ++ // Verfify the ping store is empty. ++ auto store = mgr_->getStore(); ++ ASSERT_TRUE(store); ++ auto pings = store->getAll(); ++ ASSERT_EQ(0, pings->size()); ++ ++ // Make a lease query pair. ++ auto lqp1 = makeLeaseQueryPair(IOAddress("127.0.0.2"), 111); ++ const uint8_t id1[] = { 0x31, 0x32, 0x33, 0x34 }; ++ ClientIdPtr cid1(new ClientId(id1, sizeof(id1))); ++ lqp1.lease_->client_id_ = cid1; ++ ++ // Now let's try to start a ping for the lease-query pair. It should work. ++ ASSERT_NO_THROW_LOG(mgr_->startPing(lqp1.lease_, lqp1.query_, parking_lot_)); ++ ++ // Verify we have an entry in the store. ++ pings = store->getAll(); ++ ASSERT_EQ(1, pings->size()); ++ ++ // Disable the DHCP service. ++ network_state->disableService(NetworkState::USER_COMMAND); ++ ++ // Make a second lease query pair. Different address, different client. ++ auto lqp2 = makeLeaseQueryPair(IOAddress("127.0.0.3"), 333); ++ const uint8_t id2[] = { 0x31, 0x32, 0x33, 0x35 }; ++ ClientIdPtr cid2(new ClientId(id1, sizeof(id2))); ++ lqp2.lease_->client_id_ = cid2; ++ ++ // Try to start a ping. We should not be able to do it. ++ ASSERT_THROW_MSG(mgr_->startPing(lqp2.lease_, lqp2.query_, parking_lot_), ++ InvalidOperation, ++ "PingCheckMgr::startPing() - DHCP service is suspended!"); ++ ++ // Store should be empty, having been flushed by suspension detection. ++ pings = store->getAll(); ++ ASSERT_EQ(0, pings->size()); ++ ++ // Ping checking should report as suspended. ++ ASSERT_TRUE(mgr_->checkSuspended()); ++ ++ // Re-enable the DHCP service. ++ network_state->enableService(NetworkState::USER_COMMAND); ++ ++ // Suspension checking should lift the suspension and we should once again ++ // be able to start a new ping check. ++ ASSERT_NO_THROW_LOG(mgr_->startPing(lqp2.lease_, lqp2.query_, parking_lot_)); ++ ++ // Store should have one check in it. ++ pings = store->getAll(); ++ ASSERT_EQ(1, pings->size()); ++ ++ // Ping checking should report as not suspended. ++ ASSERT_FALSE(mgr_->checkSuspended()); ++ } ++ ++ /// @brief Manager instance. ++ TestablePingCheckMgrPtr mgr_; ++ ++ /// @brief List of lease/query pairs used during the test, in the order ++ /// they were created. ++ LeaseQueryPairs lease_query_pairs_; ++ ++ /// @brief The mutex used to protect internal state. ++ const boost::scoped_ptr<std::mutex> mutex_; ++ ++ /// @brief Marks the start time of a test. ++ TimeStamp test_start_time_; ++ ++ /// @brief Parking lot where the associated query is parked. ++ /// If empty parking is not being employed. ++ ParkingLotHandlePtr parking_lot_; ++ ++ /// @brief Number of queries unparked during a test. ++ size_t unparked_; ++ ++ /// @brief List of leases that were found to be in-use during a test. ++ LeaseQueryPairs declines_; ++ ++ /// @brief List of leases that were found to be free to use during a test. ++ LeaseQueryPairs frees_; ++}; ++ ++TEST_F(PingCheckMgrTest, operationalBasicsST) { ++ testOperationalBasics(0); ++} ++ ++TEST_F(PingCheckMgrTest, operationalBasicsMT) { ++ MultiThreadingTest mt; ++ testOperationalBasics(3); ++} ++ ++TEST_F(PingCheckMgrTest, startPingST) { ++ testStartPing(); ++} ++ ++TEST_F(PingCheckMgrTest, startPingMT) { ++ MultiThreadingTest mt; ++ testStartPing(); ++} ++ ++TEST_F(PingCheckMgrTest, nextToSendST) { ++ testNextToSend(); ++} ++ ++TEST_F(PingCheckMgrTest, nextToSendMT) { ++ MultiThreadingTest mt; ++ testNextToSend(); ++} ++ ++TEST_F(PingCheckMgrTest, setNextExpirationST) { ++ testSetNextExpiration(); ++} ++ ++TEST_F(PingCheckMgrTest, setNextExpirationMT) { ++ MultiThreadingTest mt; ++ testSetNextExpiration(); ++} ++ ++TEST_F(PingCheckMgrTest, sendCompletedST) { ++ testSendCompleted(); ++} ++ ++TEST_F(PingCheckMgrTest, sendCompletedMT) { ++ MultiThreadingTest mt; ++ testSendCompleted(); ++} ++ ++TEST_F(PingCheckMgrTest, replyReceivedForEchoReplyST) { ++ testReplyReceivedForEchoReply(); ++} ++ ++TEST_F(PingCheckMgrTest, replyReceivedForEchoReplyMT) { ++ MultiThreadingTest mt; ++ testReplyReceivedForEchoReply(); ++} ++ ++TEST_F(PingCheckMgrTest, replyReceivedForTargetUnreachableST) { ++ testReplyReceivedForTargetUnreachable(); ++} ++ ++TEST_F(PingCheckMgrTest, replyReceivedForTargetUnreachableMT) { ++ MultiThreadingTest mt; ++ testReplyReceivedForTargetUnreachable(); ++} ++ ++TEST_F(PingCheckMgrTest, expirationProcessingST) { ++ testExpirationProcessing(); ++} ++ ++TEST_F(PingCheckMgrTest, expirationProcessingMT) { ++ MultiThreadingTest mt; ++ testExpirationProcessing(); ++} ++ ++TEST_F(PingCheckMgrTest, multiplePingsWithReplyST) { ++ testMultiplePingsWithReply(); ++} ++ ++TEST_F(PingCheckMgrTest, multiplePingsWithReplyMT) { ++ MultiThreadingTest mt; ++ testMultiplePingsWithReply(); ++} ++ ++TEST_F(PingCheckMgrTest, multiplePingsWithReplyAndPauseST) { ++ testMultiplePingsWithReplyAndPause(); ++} ++ ++TEST_F(PingCheckMgrTest, multiplePingsWithReplyAndPauseMT) { ++ MultiThreadingTest mt; ++ testMultiplePingsWithReplyAndPause(); ++} ++ ++TEST_F(PingCheckMgrTest, sendCompletedSendFailedST) { ++ testSendCompletedSendFailed(); ++} ++ ++TEST_F(PingCheckMgrTest, sendCompletedSendFailedMT) { ++ MultiThreadingTest mt; ++ testSendCompletedSendFailed(); ++} ++ ++TEST_F(PingCheckMgrTest, shouldPingST) { ++ testShouldPingTest(); ++} ++ ++TEST_F(PingCheckMgrTest, shouldPingMT) { ++ MultiThreadingTest mt; ++ testShouldPingTest(); ++} ++ ++TEST_F(PingCheckMgrTest, getScopedConfigST) { ++ testGetScopedConfig(); ++} ++ ++TEST_F(PingCheckMgrTest, getScopedConfigMT) { ++ MultiThreadingTest mt; ++ testGetScopedConfig(); ++} ++ ++TEST_F(PingCheckMgrTest, checkSuspendedST) { ++ testCheckSuspended(); ++} ++ ++TEST_F(PingCheckMgrTest, checkSuspendedMT) { ++ MultiThreadingTest mt; ++ testCheckSuspended(); ++} ++ ++} // end of anonymous namespace +diff --git a/src/hooks/dhcp/ping_check/tests/ping_context_store_unittests.cc b/src/hooks/dhcp/ping_check/tests/ping_context_store_unittests.cc +new file mode 100644 +index 0000000000..3a8854eb0e +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/tests/ping_context_store_unittests.cc +@@ -0,0 +1,467 @@ ++// Copyright (C) 2023-2025 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 This file contains tests which exercise the PingContextStore class. ++ ++#include <config.h> ++#include <ping_context_store.h> ++#include <asiolink/io_address.h> ++#include <testutils/gtest_utils.h> ++#include <testutils/multi_threading_utils.h> ++ ++#include <gtest/gtest.h> ++#include <sstream> ++ ++using namespace std; ++using namespace isc; ++using namespace isc::asiolink; ++using namespace isc::dhcp; ++using namespace isc::ping_check; ++using namespace isc::test; ++using namespace std::chrono; ++ ++namespace { ++ ++/// @brief Text fixture class for @c PingContextStore ++/// ++/// In order to facilitate single and multi threaded testing, ++/// individual tests are implemented as methods that are called ++/// from within TEST_F bodies rather than in TEST_F bodies. ++class PingContextStoreTest : public ::testing::Test { ++public: ++ ++ /// @brief Constructor ++ PingContextStoreTest() { ++ } ++ ++ /// @brief Destructor ++ virtual ~PingContextStoreTest() = default; ++ ++ /// @brief Verifies that contexts can be added to the store given valid leases and queries. ++ /// Also verifies that they can be fetched by address. ++ void addContextTest() { ++ PingContextStore store; ++ PingContextPtr context; ++ ++ // Add three contexts, one for each lease/query. ++ auto now = PingContext::now(); ++ for (int i = 0; i < leases_.size(); ++i) { ++ ASSERT_NO_THROW_LOG(context = store.addContext(leases_[i], queries_[i], 2, 300)); ++ ASSERT_TRUE(context); ++ EXPECT_EQ(leases_[i], context->getLease()); ++ EXPECT_EQ(queries_[i], context->getQuery()); ++ ++ // Check initial values. ++ EXPECT_EQ(PingContext::WAITING_TO_SEND, context->getState()); ++ EXPECT_LE(now, context->getSendWaitStart()); ++ EXPECT_EQ(2, context->getMinEchos()); ++ EXPECT_EQ(300, context->getReplyTimeout()); ++ } ++ ++ // Make sure they can be fetched by address and by query individually. ++ for (int i = 0; i < leases_.size(); ++i) { ++ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[i]->addr_)); ++ ASSERT_TRUE(context); ++ EXPECT_EQ(leases_[i], context->getLease()); ++ ++ ASSERT_NO_THROW_LOG(context = store.getContextByQuery(queries_[i])); ++ ASSERT_TRUE(context); ++ EXPECT_EQ(queries_[i], context->getQuery()); ++ } ++ } ++ ++ /// @brief Verifies that the store only allows once entry per IP address. ++ void addContextDuplicateTest() { ++ PingContextStore store; ++ PingContextPtr context; ++ ++ ASSERT_NO_THROW_LOG(context = store.addContext(leases_[0], queries_[0], 1, 100)); ++ ASSERT_TRUE(context); ++ ASSERT_THROW_MSG(store.addContext(leases_[0], queries_[0], 1, 100), DuplicateContext, ++ "PingContextStore::addContex: context already exists for: 192.0.2.1"); ++ } ++ ++ /// @brief Verify that addContext fails given invalid input. ++ void addContextInvalidTest() { ++ PingContextStore store; ++ ++ // Verify that given an empty lease the add will fail. ++ Lease4Ptr empty_lease; ++ ASSERT_THROW_MSG(store.addContext(empty_lease, queries_[0], 1, 100), BadValue, ++ "PingContextStore::addContext failed:" ++ " PingContext ctor - lease cannot be empty"); ++ ++ // Verify that given an empty query the add will fail. ++ Pkt4Ptr empty_query; ++ ASSERT_THROW_MSG(store.addContext(leases_[0], empty_query, 1, 100), BadValue, ++ "PingContextStore::addContext failed:" ++ " PingContext ctor - query cannot be empty"); ++ } ++ ++ /// @brief Verify that contexts can be deleted from the store. ++ void deleteContextTest() { ++ PingContextStore store; ++ ++ // Add contexts to store. ++ for (int i = 0; i < leases_.size(); ++i) { ++ PingContextPtr context; ++ ASSERT_NO_THROW_LOG(context = store.addContext(leases_[i], queries_[i], 1, 100)); ++ ASSERT_TRUE(context); ++ EXPECT_EQ(leases_[i], context->getLease()); ++ EXPECT_EQ(queries_[i], context->getQuery()); ++ } ++ ++ // Fetch the second context. ++ PingContextPtr orig_context; ++ ASSERT_NO_THROW_LOG(orig_context = store.getContextByAddress(leases_[1]->addr_)); ++ ASSERT_TRUE(orig_context); ++ EXPECT_EQ(leases_[1], orig_context->getLease()); ++ ++ // Delete it. ++ ASSERT_NO_THROW_LOG(store.deleteContext(orig_context)); ++ ++ // Try to fetch it, shouldn't find it. ++ PingContextPtr context; ++ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[1]->addr_)); ++ ASSERT_FALSE(context); ++ ++ // Deleting it again should do no harm. ++ ASSERT_NO_THROW_LOG(store.deleteContext(orig_context)); ++ } ++ ++ /// @brief Verify that contexts in the store can be updated. ++ void updateContextTest() { ++ PingContextStore store; ++ PingContextPtr context; ++ ++ // Try to update a context that doesn't exist. It should throw. ++ ASSERT_NO_THROW_LOG(context.reset(new PingContext(leases_[0], queries_[0]))); ++ ASSERT_THROW_MSG(store.updateContext(context), InvalidOperation, ++ "PingContextStore::updateContext failed for address:" ++ " 192.0.2.1, not in store"); ++ ++ auto test_start = PingContext::now(); ++ ++ // Add contexts to store. ++ for (int i = 0; i < leases_.size(); ++i) { ++ ASSERT_NO_THROW_LOG(context = store.addContext(leases_[i], queries_[i], 1, 100)); ++ ASSERT_TRUE(context); ++ EXPECT_EQ(leases_[i], context->getLease()); ++ EXPECT_EQ(queries_[i], context->getQuery()); ++ } ++ ++ // Fetch the second context. ++ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[1]->addr_)); ++ ASSERT_TRUE(context); ++ ASSERT_EQ(leases_[1], context->getLease()); ++ ASSERT_EQ(queries_[1], context->getQuery()); ++ ++ // Check initial values for state and expiration. ++ EXPECT_EQ(PingContext::WAITING_TO_SEND, context->getState()); ++ EXPECT_LE(test_start, context->getSendWaitStart()); ++ EXPECT_LE(PingContext::EMPTY_TIME(), context->getNextExpiry()); ++ ++ // Modify the state and expiration, then update the context. ++ auto wait_start = PingContext::now(); ++ context->beginWaitingForReply(wait_start); ++ ASSERT_NO_THROW_LOG(store.updateContext(context)); ++ ++ // Fetch the context and verify the values are correct. ++ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[1]->addr_)); ++ ASSERT_TRUE(context); ++ EXPECT_EQ(PingContext::WAITING_FOR_REPLY, context->getState()); ++ EXPECT_LE(wait_start + milliseconds(context->getReplyTimeout()), context->getNextExpiry()); ++ } ++ ++ /// @brief Verify that contexts can be fetched based on when they entered WAITING_TO_SEND ++ /// by getNextToSend(). ++ void getNextToSendTest() { ++ PingContextStore store; ++ PingContextPtr context; ++ ++ // Capture time now. ++ auto start_time = PingContext::now(); ++ ++ // Add contexts to store. ++ for (int i = 0; i < leases_.size(); ++i) { ++ ASSERT_NO_THROW_LOG(context = store.addContext(leases_[i], queries_[i], 1, 100)); ++ ASSERT_TRUE(context); ++ EXPECT_EQ(leases_[i], context->getLease()); ++ EXPECT_EQ(queries_[i], context->getQuery()); ++ usleep(1000); ++ } ++ ++ // Fetching the next context to send should return the first context as ++ // it has the oldest send wait start time. ++ context.reset(); ++ ASSERT_NO_THROW(context = store.getNextToSend()); ++ ASSERT_TRUE(context); ++ EXPECT_EQ(leases_[0], context->getLease()); ++ EXPECT_EQ(queries_[0], context->getQuery()); ++ EXPECT_LE(start_time, context->getSendWaitStart()); ++ ++ // Update the first context's state to TARGET_FREE which should ++ // disqualify it from being returned as next to send. ++ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[0]->addr_)); ++ ASSERT_TRUE(context); ++ ASSERT_EQ(PingContext::WAITING_TO_SEND, context->getState()); ++ context->setState(PingContext::TARGET_FREE); ++ ASSERT_NO_THROW_LOG(store.updateContext(context)); ++ ++ // Update the send wait start of the second context making it the ++ // youngest send wait start time. ++ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[1]->addr_)); ++ ASSERT_TRUE(context); ++ ASSERT_EQ(PingContext::WAITING_TO_SEND, context->getState()); ++ context->setSendWaitStart(start_time + milliseconds(1000)); ++ ASSERT_NO_THROW_LOG(store.updateContext(context)); ++ ++ // Update the send wait start of the third context, making it the oldest. ++ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[2]->addr_)); ++ ASSERT_TRUE(context); ++ ASSERT_EQ(PingContext::WAITING_TO_SEND, context->getState()); ++ context->setSendWaitStart(start_time + milliseconds(500)); ++ ASSERT_NO_THROW_LOG(store.updateContext(context)); ++ ++ // Fetching the next context to send should return the third context. ++ context.reset(); ++ ASSERT_NO_THROW(context = store.getNextToSend()); ++ ASSERT_TRUE(context); ++ EXPECT_EQ(leases_[2], context->getLease()); ++ EXPECT_EQ(queries_[2], context->getQuery()); ++ EXPECT_EQ(start_time + milliseconds(500), context->getSendWaitStart()); ++ } ++ ++ /// @brief Verify that contexts can be fetched based on when they expire using ++ /// getExpiresNext() and getExpiredSince(). ++ void getByExpirationTest() { ++ PingContextStore store; ++ PingContextPtr context; ++ ++ // Add contexts to store. ++ for (int i = 0; i < leases_.size(); ++i) { ++ ASSERT_NO_THROW_LOG(context = store.addContext(leases_[i], queries_[i], 1, 100)); ++ ASSERT_TRUE(context); ++ EXPECT_EQ(leases_[i], context->getLease()); ++ EXPECT_EQ(queries_[i], context->getQuery()); ++ } ++ ++ // Capture time now. ++ auto start_time = PingContext::now(); ++ ++ // Update the state and expiration of the first context. ++ // State set to TARGET_FREE should disqualify if from ++ // fetch by expiration even though it has the soonest expiration ++ // time. ++ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[0]->addr_)); ++ ASSERT_TRUE(context); ++ context->setState(PingContext::TARGET_FREE); ++ context->setNextExpiry(start_time + milliseconds(1)); ++ ASSERT_NO_THROW_LOG(store.updateContext(context)); ++ ++ // Update the state and expiration of the second context giving it ++ // the youngest expiration time. ++ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[1]->addr_)); ++ ASSERT_TRUE(context); ++ context->setState(PingContext::WAITING_FOR_REPLY); ++ context->setNextExpiry(start_time + milliseconds(1000)); ++ ASSERT_NO_THROW_LOG(store.updateContext(context)); ++ ++ // Update the state and expiration of the third context, make it the ++ // soonest qualified expiration time. ++ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[2]->addr_)); ++ ASSERT_TRUE(context); ++ context->setState(PingContext::WAITING_FOR_REPLY); ++ context->setNextExpiry(start_time + milliseconds(500)); ++ ASSERT_NO_THROW_LOG(store.updateContext(context)); ++ ++ // Fetching the context that expires next should return the third context. ++ context.reset(); ++ ASSERT_NO_THROW(context = store.getExpiresNext()); ++ ASSERT_TRUE(context); ++ EXPECT_EQ(leases_[2], context->getLease()); ++ EXPECT_EQ(queries_[2], context->getQuery()); ++ EXPECT_EQ(start_time + milliseconds(500), context->getNextExpiry()); ++ ++ // Fetch all that have expired since current time. Should be none. ++ PingContextCollectionPtr expired_since; ++ ASSERT_NO_THROW_LOG(expired_since = store.getExpiredSince()); ++ ASSERT_TRUE(expired_since); ++ EXPECT_EQ(0, expired_since->size()); ++ ++ // Fetch all that have expired since start time + 750 ms, should be third context. ++ ASSERT_NO_THROW_LOG(expired_since = store.getExpiredSince(start_time + milliseconds(750))); ++ ASSERT_TRUE(expired_since); ++ EXPECT_EQ(1, expired_since->size()); ++ context = (*expired_since)[0]; ++ EXPECT_EQ(leases_[2], context->getLease()); ++ EXPECT_EQ(queries_[2], context->getQuery()); ++ EXPECT_EQ(start_time + milliseconds(500), context->getNextExpiry()); ++ ++ // Fetch all that have expired since start time + 1500 ms ++ // Should be the third and second contexts ++ ASSERT_NO_THROW_LOG(expired_since = store.getExpiredSince(start_time + milliseconds(1500))); ++ ASSERT_TRUE(expired_since); ++ EXPECT_EQ(2, expired_since->size()); ++ ++ // First in list should be the third context. ++ context = (*expired_since)[0]; ++ EXPECT_EQ(leases_[2], context->getLease()); ++ EXPECT_EQ(queries_[2], context->getQuery()); ++ EXPECT_EQ(start_time + milliseconds(500), context->getNextExpiry()); ++ ++ // The last one in the list should be the second context. ++ context = (*expired_since)[1]; ++ EXPECT_EQ(leases_[1], context->getLease()); ++ EXPECT_EQ(queries_[1], context->getQuery()); ++ EXPECT_EQ(start_time + milliseconds(1000), context->getNextExpiry()); ++ } ++ ++ /// @brief Verifies that getAll() and clear() work properly. ++ void getAllAndClearTest() { ++ PingContextStore store; ++ ++ // Add contexts to store. ++ for (int i = 0; i < leases_.size(); ++i) { ++ PingContextPtr context; ++ ASSERT_NO_THROW_LOG(context = store.addContext(leases_[i], queries_[i], 1, 100)); ++ ASSERT_TRUE(context); ++ EXPECT_EQ(leases_[i], context->getLease()); ++ EXPECT_EQ(queries_[i], context->getQuery()); ++ } ++ ++ // Fetch them all. ++ PingContextCollectionPtr contexts; ++ ASSERT_NO_THROW_LOG(contexts = store.getAll()); ++ ASSERT_EQ(leases_.size(), contexts->size()); ++ ++ // Verify we got them all in order. ++ int i = 0; ++ for (auto const& context : *contexts) { ++ EXPECT_EQ(leases_[i], context->getLease()); ++ EXPECT_EQ(queries_[i], context->getQuery()); ++ ++i; ++ } ++ ++ // Now clear the store. Verify it's empty. ++ ASSERT_NO_THROW_LOG(store.clear()); ++ ASSERT_NO_THROW_LOG(contexts = store.getAll()); ++ ASSERT_EQ(0, contexts->size()); ++ ++ // Verify clearing an empty store does no harm. ++ ASSERT_NO_THROW_LOG(store.clear()); ++ } ++ ++private: ++ /// @brief Prepares the class for a test. ++ virtual void SetUp() { ++ Lease4Ptr lease; ++ lease.reset(new Lease4()); ++ lease->addr_ = IOAddress("192.0.2.1"); ++ leases_.push_back(lease); ++ ++ lease.reset(new Lease4()); ++ lease->addr_ = IOAddress("192.0.2.2"); ++ leases_.push_back(lease); ++ ++ lease.reset(new Lease4()); ++ lease->addr_ = IOAddress("192.0.2.3"); ++ leases_.push_back(lease); ++ ++ Pkt4Ptr query; ++ query.reset(new Pkt4(DHCPDISCOVER, 101)); ++ queries_.push_back(query); ++ ++ query.reset(new Pkt4(DHCPDISCOVER, 102)); ++ queries_.push_back(query); ++ ++ query.reset(new Pkt4(DHCPDISCOVER, 103)); ++ queries_.push_back(query); ++ ++ ASSERT_EQ(leases_.size(), queries_.size()); ++ } ++ ++public: ++ /// @brief List of pre-made leases. ++ std::vector<Lease4Ptr> leases_; ++ ++ /// @brief List of pre-made queries. ++ std::vector<Pkt4Ptr> queries_; ++}; ++ ++TEST_F(PingContextStoreTest, addContext) { ++ addContextTest(); ++} ++ ++TEST_F(PingContextStoreTest, addContextMultiThreading) { ++ MultiThreadingTest mt; ++ addContextTest(); ++} ++ ++TEST_F(PingContextStoreTest, addContextDuplicate) { ++ addContextDuplicateTest(); ++} ++ ++TEST_F(PingContextStoreTest, addContextDuplicateMultiThreading) { ++ MultiThreadingTest mt; ++ addContextDuplicateTest(); ++} ++ ++TEST_F(PingContextStoreTest, addContextInvalid) { ++ addContextInvalidTest(); ++} ++ ++TEST_F(PingContextStoreTest, addContextInvalidMultiThreading) { ++ MultiThreadingTest mt; ++ addContextInvalidTest(); ++} ++ ++TEST_F(PingContextStoreTest, deleteContext) { ++ deleteContextTest(); ++} ++ ++TEST_F(PingContextStoreTest, deleteContextMultiThreading) { ++ MultiThreadingTest mt; ++ deleteContextTest(); ++} ++ ++TEST_F(PingContextStoreTest, updateContext) { ++ updateContextTest(); ++} ++ ++TEST_F(PingContextStoreTest, updateContextMultiThreading) { ++ MultiThreadingTest mt; ++ updateContextTest(); ++} ++ ++TEST_F(PingContextStoreTest, getNextToSend) { ++ getNextToSendTest(); ++} ++ ++TEST_F(PingContextStoreTest, getNextToSendMultiThreading) { ++ MultiThreadingTest mt; ++ getNextToSendTest(); ++} ++ ++TEST_F(PingContextStoreTest, getByExpiration) { ++ getByExpirationTest(); ++} ++ ++TEST_F(PingContextStoreTest, getByExpirationMultiThreading) { ++ MultiThreadingTest mt; ++ getByExpirationTest(); ++} ++ ++TEST_F(PingContextStoreTest, getAllAndClear) { ++ getAllAndClearTest(); ++} ++ ++TEST_F(PingContextStoreTest, getAllAndClearMultiThreading) { ++ MultiThreadingTest mt; ++ getAllAndClearTest(); ++} ++ ++} // end of anonymous namespace +diff --git a/src/hooks/dhcp/ping_check/tests/ping_context_unittests.cc b/src/hooks/dhcp/ping_check/tests/ping_context_unittests.cc +new file mode 100644 +index 0000000000..4a38277ad6 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/tests/ping_context_unittests.cc +@@ -0,0 +1,146 @@ ++// Copyright (C) 2023-2025 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 This file contains tests which exercise the PingContext class. ++ ++#include <config.h> ++#include <ping_context.h> ++#include <asiolink/io_address.h> ++#include <testutils/gtest_utils.h> ++ ++#include <gtest/gtest.h> ++#include <sstream> ++ ++using namespace std; ++using namespace isc; ++using namespace isc::asiolink; ++using namespace isc::dhcp; ++using namespace isc::ping_check; ++using namespace std::chrono; ++ ++namespace { ++ ++TEST(PingContextTest, validConstruction) { ++ // Make a valid lease and query. ++ Lease4Ptr lease(new Lease4()); ++ lease->addr_ = IOAddress("192.0.2.1"); ++ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234)); ++ ++ // Capture time now. ++ auto start_time = PingContext::now(); ++ ++ // Construct the context. ++ PingContextPtr context; ++ ASSERT_NO_THROW_LOG(context.reset(new PingContext(lease, query))); ++ ++ // Verify initial content. ++ EXPECT_EQ(lease->addr_, context->getTarget()); ++ EXPECT_EQ(1, context->getMinEchos()); ++ EXPECT_EQ(100, context->getReplyTimeout()); ++ EXPECT_EQ(0, context->getEchosSent()); ++ EXPECT_EQ(PingContext::EMPTY_TIME(), context->getLastEchoSentTime()); ++ EXPECT_EQ(PingContext::EMPTY_TIME(), context->getSendWaitStart()); ++ EXPECT_EQ(PingContext::EMPTY_TIME(), context->getNextExpiry()); ++ EXPECT_EQ(PingContext::NEW, context->getState()); ++ ++ // Start time should be less than or equal to created time. ++ EXPECT_LE(start_time, context->getCreatedTime()); ++ EXPECT_EQ(lease, context->getLease()); ++ EXPECT_EQ(query, context->getQuery()); ++} ++ ++TEST(PingContextTest, invalidConstruction) { ++ // Make a valid lease and query. ++ Lease4Ptr lease(new Lease4()); ++ lease->addr_ = IOAddress("192.0.2.1"); ++ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234)); ++ ++ // Empty lease should throw. ++ Lease4Ptr empty_lease; ++ PingContextPtr context; ++ ASSERT_THROW_MSG(context.reset(new PingContext(empty_lease, query)), BadValue, ++ "PingContext ctor - lease cannot be empty"); ++ ++ // Empty query should throw. ++ Pkt4Ptr empty_query; ++ ASSERT_THROW_MSG(context.reset(new PingContext(lease, empty_query)), BadValue, ++ "PingContext ctor - query cannot be empty"); ++ ++ // Empty lease address should throw. ++ lease->addr_ = IOAddress::IPV4_ZERO_ADDRESS(); ++ ASSERT_THROW_MSG(context.reset(new PingContext(lease, query)), BadValue, ++ "PingContext ctor - target address cannot be 0.0.0.0"); ++} ++ ++// Tests conversion of PingContext::State to string and vice-versa. ++TEST(PingContext, stateConversion) { ++ EXPECT_EQ(PingContext::NEW, PingContext::stringToState("NEW")); ++ EXPECT_EQ(PingContext::WAITING_TO_SEND, PingContext::stringToState("WAITING_TO_SEND")); ++ EXPECT_EQ(PingContext::SENDING, PingContext::stringToState("SENDING")); ++ EXPECT_EQ(PingContext::WAITING_FOR_REPLY, PingContext::stringToState("WAITING_FOR_REPLY")); ++ EXPECT_EQ(PingContext::TARGET_FREE, PingContext::stringToState("TARGET_FREE")); ++ EXPECT_EQ(PingContext::TARGET_IN_USE, PingContext::stringToState("TARGET_IN_USE")); ++ ASSERT_THROW_MSG(PingContext::stringToState("bogus"), BadValue, ++ "Invalid PingContext::State: 'bogus'"); ++ ++ EXPECT_EQ("NEW", PingContext::stateToString(PingContext::NEW)); ++ EXPECT_EQ("WAITING_TO_SEND", PingContext::stateToString(PingContext::WAITING_TO_SEND)); ++ EXPECT_EQ("SENDING", PingContext::stateToString(PingContext::SENDING)); ++ EXPECT_EQ("WAITING_FOR_REPLY", PingContext::stateToString(PingContext::WAITING_FOR_REPLY)); ++ EXPECT_EQ("TARGET_FREE", PingContext::stateToString(PingContext::TARGET_FREE)); ++ EXPECT_EQ("TARGET_IN_USE", PingContext::stateToString(PingContext::TARGET_IN_USE)); ++} ++ ++TEST(PingContext, accessors) { ++ // Make a valid lease and query. ++ Lease4Ptr lease(new Lease4()); ++ lease->addr_ = IOAddress("192.0.2.1"); ++ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234)); ++ ++ // Capture time now. ++ auto time_now = PingContext::now(); ++ ++ // Construct a context. ++ PingContextPtr context; ++ ASSERT_NO_THROW_LOG(context.reset(new PingContext(lease, query, 1, 50))); ++ ++ EXPECT_NO_THROW_LOG(context->setMinEchos(4)); ++ EXPECT_EQ(4, context->getMinEchos()); ++ ++ EXPECT_NO_THROW_LOG(context->setReplyTimeout(200)); ++ EXPECT_EQ(200, context->getReplyTimeout()); ++ ++ EXPECT_NO_THROW_LOG(context->setEchosSent(7)); ++ EXPECT_EQ(7, context->getEchosSent()); ++ ++ EXPECT_NO_THROW_LOG(context->setLastEchoSentTime(time_now)); ++ EXPECT_EQ(time_now, context->getLastEchoSentTime()); ++ ++ EXPECT_NO_THROW_LOG(context->setState(PingContext::SENDING)); ++ EXPECT_EQ(PingContext::SENDING, context->getState()); ++ ++ time_now += milliseconds(100); ++ EXPECT_NO_THROW_LOG(context->setSendWaitStart(time_now)); ++ EXPECT_EQ(time_now, context->getSendWaitStart()); ++ ++ time_now += milliseconds(100); ++ EXPECT_NO_THROW_LOG(context->setNextExpiry(time_now)); ++ EXPECT_EQ(time_now, context->getNextExpiry()); ++ ++ EXPECT_FALSE(context->isWaitingToSend()); ++ time_now += milliseconds(100); ++ ASSERT_NO_THROW_LOG(context->beginWaitingToSend(time_now)); ++ EXPECT_EQ(time_now, context->getSendWaitStart()); ++ EXPECT_TRUE(context->isWaitingToSend()); ++ ++ EXPECT_FALSE(context->isWaitingForReply()); ++ auto exp_expiry = time_now + milliseconds(context->getReplyTimeout()); ++ ASSERT_NO_THROW_LOG(context->beginWaitingForReply(time_now)); ++ EXPECT_EQ(exp_expiry, context->getNextExpiry()); ++ EXPECT_TRUE(context->isWaitingForReply()); ++} ++ ++} // end of anonymous namespace +diff --git a/src/hooks/dhcp/ping_check/tests/ping_test_utils.h b/src/hooks/dhcp/ping_check/tests/ping_test_utils.h +new file mode 100644 +index 0000000000..df1ede7526 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/tests/ping_test_utils.h +@@ -0,0 +1,396 @@ ++// Copyright (C) 2023-2025 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 PING_TEST_UTILS_H ++#define PING_TEST_UTILS_H ++ ++#include <ping_channel.h> ++#include <asiolink/interval_timer.h> ++#include <asiolink/io_service.h> ++#include <asiolink/io_address.h> ++#include <testutils/gtest_utils.h> ++#include <asiolink/io_service_thread_pool.h> ++#include <util/multi_threading_mgr.h> ++#include <mutex> ++ ++#include <gtest/gtest.h> ++#include <queue> ++#include <list> ++#include <thread> ++#include <map> ++ ++namespace isc { ++namespace ping_check { ++ ++/// @brief Test timeout (ms). ++const long TEST_TIMEOUT = 10000; ++ ++/// @brief Maps IOAddresses to sequence numbers. ++/// ++/// Outbound requests are assigned a unique id and sequence ++/// number. This map is used to track the request's destination ++/// address by its sequence number. The channel can then substitute ++/// the loopback address, 127.0.0.1, as the destination address. ++/// Upon response receipt, the original destination can be found by ++/// the sequence number sent back in the response. ++class LoopbackMap { ++public: ++ /// @brief Constructor. ++ LoopbackMap() : map_(), mutex_(new std::mutex) { ++ } ++ ++ /// @brief Destructor. ++ ~LoopbackMap() = default; ++ ++ /// @brief Find and IOAddress associated with a sequence number. ++ /// ++ /// @param sequence sequence number to search by ++ /// ++ /// @return address found or IPV4_ZERO_ADDRESS. ++ asiolink::IOAddress find(uint16_t sequence) { ++ util::MultiThreadingLock lock(*mutex_); ++ auto const& iter = map_.find(sequence); ++ if (iter == map_.end()) { ++ return (asiolink::IOAddress::IPV4_ZERO_ADDRESS()); ++ } ++ ++ return (iter->second); ++ } ++ ++ /// @brief Adds an entry for a sequence number and address ++ /// ++ /// @param sequence sequence number associated with the address ++ /// @param address address to add to the map ++ /// ++ /// @return true if the entry was added, false otherwise. ++ bool add(uint16_t sequence, const asiolink::IOAddress& address) { ++ util::MultiThreadingLock lock(*mutex_); ++ if (map_.count(sequence)) { ++ return (false); ++ } ++ ++ map_.emplace(sequence, address); ++ return (true); ++ }; ++ ++ /// @brief Map of addresses by sequence number. ++ std::map<uint16_t, asiolink::IOAddress> map_; ++ ++ /// @brief Mutex to protect the map during operations. ++ const boost::scoped_ptr<std::mutex> mutex_; ++}; ++ ++/// @brief Testable derivation of PingChannel ++/// ++/// Overrides read and write functions to inject IO errors. ++class TestablePingChannel : public PingChannel { ++public: ++ /// @brief Constructor ++ /// ++ /// Instantiates the channel with its socket closed. ++ /// ++ /// @param io_service pointer to the IOService instance that will manage ++ /// the channel's IO. Must not be empty ++ /// @param next_to_send_cb callback to invoke to fetch the next IOAddress ++ /// to ping ++ /// @param echo_sent_cb callback to invoke when an ECHO send has completed ++ /// @param reply_received_cb callback to invoke when an ICMP reply has been ++ /// received. This callback is passed all inbound ICMP messages (e.g. ECHO ++ /// REPLY, UNREACHABLE, etc...) ++ /// @param shutdown_cb callback to invoke when the channel has shutdown due ++ /// to an error ++ /// ++ /// @throw BadValue if io_service is empty. ++ TestablePingChannel(asiolink::IOServicePtr& io_service, ++ NextToSendCallback next_to_send_cb, ++ EchoSentCallback echo_sent_cb, ++ ReplyReceivedCallback reply_received_cb, ++ ShutdownCallback shutdown_cb = ShutdownCallback()) ++ : PingChannel(io_service, next_to_send_cb, echo_sent_cb, reply_received_cb, shutdown_cb), ++ read_number_(0), throw_on_read_number_(0), ec_on_read_number_(0), read_error_ec_(), ++ write_number_(0), throw_on_write_number_(0), ec_on_write_number_(0), write_error_ec_(), ++ route_loopback_(true), loopback_map_(), stopped_(false) { ++ } ++ ++ /// @brief Virtual destructor ++ virtual ~TestablePingChannel() { ++ stopped_ = true; ++ } ++ ++ // @brief Schedules the next send. ++ // ++ // If the socket is not currently sending it posts a call to @c sendNext() ++ // to the channel's IOService. ++ virtual void startSend() { ++ if (stopped_) { ++ return; ++ } ++ PingChannel::startSend(); ++ } ++ ++ /// @brief Perform asynchronous read or feign a read error ++ /// ++ /// This virtual function is provided as means to inject errors during ++ /// read operations to facilitate testing. It tracks the number of ++ /// reads that have occurred since channel open and instigates an ++ /// error trigger on the trigger read number if a trigger has been set. ++ /// ++ /// @param data buffer to receive incoming message ++ /// @param length length of the data buffer ++ /// @param offset offset into buffer where data is to be put ++ /// @param endpoint source of the communication ++ /// @param callback callback object ++ virtual void asyncReceive(void* data, size_t length, size_t offset, ++ asiolink::IOEndpoint* endpoint, SocketCallback& callback) { ++ if (stopped_) { ++ return; ++ } ++ ++read_number_; ++ ++ // If we're set to fail with an exception, do so. ++ if (throw_on_read_number_ && (read_number_ == throw_on_read_number_)) { ++ isc_throw(Unexpected, "Injected read error"); ++ } ++ ++ // If we're set to fail via the callback, post a call with the ++ // desired error code. ++ if (ec_on_read_number_ && read_number_ == ec_on_read_number_) { ++ getIOService()->post([this]() { socketReadCallback(read_error_ec_, 0); }); ++ return; ++ } ++ ++ // No scheduled error, proceed with normal read. ++ PingChannel::asyncReceive(data, length, offset, endpoint, callback); ++ } ++ ++ /// @brief Perform asynchronous write or feign a write error ++ /// ++ /// This virtual function is provided as means to inject errors during ++ /// write operations to facilitate testing. It tracks the number of ++ /// writes that have occurred since channel open and instigates an ++ /// error trigger on the trigger write number if a trigger has been set. ++ /// ++ /// @param data buffer of data to write ++ /// @param length length of the data buffer ++ /// @param endpoint destination of the communication ++ /// @param callback callback object ++ virtual void asyncSend(void* data, size_t length, asiolink::IOEndpoint* endpoint, ++ SocketCallback& callback) { ++ if (stopped_) { ++ return; ++ } ++ ++write_number_; ++ if (throw_on_write_number_ && (write_number_ == throw_on_write_number_)) { ++ isc_throw(Unexpected, "Injected write error"); ++ } ++ ++ if (ec_on_write_number_ && write_number_ == ec_on_write_number_) { ++ ICMPMsgPtr fake_echo(new ICMPMsg()); ++ fake_echo->setType(ICMPMsg::ECHO_REQUEST); ++ fake_echo->setDestination(endpoint->getAddress()); ++ getIOService()->post([this, fake_echo]() { socketWriteCallback(fake_echo, write_error_ec_, 0); }); ++ return; ++ } ++ ++ // In order to make testing more predictable, we need slow writes down a bit. ++ usleep(5000); ++ ++ // If loopback routing is enabled, store the destination address by ++ // sequence number in the loopback map, then replace the destination ++ // endpoint with 127.0.0.1 and send it there. ++ if (route_loopback_) { ++ struct icmp* reply = (struct icmp*)(data); ++ auto sequence = (ntohs(reply->icmp_hun.ih_idseq.icd_seq)); ++ loopback_map_.add(sequence, endpoint->getAddress()); ++ ICMPEndpoint lo_endpoint(asiolink::IOAddress("127.0.0.1")); ++ PingChannel::asyncSend(data, length, &lo_endpoint, callback); ++ return; ++ } ++ ++ PingChannel::asyncSend(data, length, endpoint, callback); ++ } ++ ++ /// @brief Fetches the PingSocket. ++ /// ++ /// @return pointer to the PingSocket instance. ++ PingSocketPtr getPingSocket() { ++ return (socket_); ++ } ++ ++ /// @brief Checks if channel was opened in single-threaded mode. ++ /// ++ /// @return True if channel is single-threaded. ++ bool getSingleThreaded() const { ++ return (single_threaded_); ++ } ++ ++ /// @brief Fetch the WatchSocket instance. ++ /// ++ /// @return pointer to the WatchSocket. ++ util::WatchSocketPtr getWatchSocket() const { ++ return (watch_socket_); ++ } ++ ++ /// @brief The "write-ready" socket descriptor registered IfaceMgr. ++ /// ++ /// @return registered socket descriptor. ++ int getRegisteredWriteFd() const { ++ return (registered_write_fd_); ++ } ++ ++ /// @brief The "read-ready" socket descriptor registered IfaceMgr. ++ /// ++ /// @return registered socket descriptor. ++ int getRegisteredReadFd() const { ++ return (registered_read_fd_); ++ } ++ ++ /// @brief Tracks the number of reads since the channel was created ++ size_t read_number_; ++ ++ /// @brief Read number on which to thrown an exception from asyncReceive() ++ size_t throw_on_read_number_; ++ ++ /// @brief Read number on which to inject a socketReadCallback with an error code ++ size_t ec_on_read_number_; ++ ++ /// @brief Error code to inject on read error trigger ++ boost::system::error_code read_error_ec_; ++ ++ /// @brief Tracks the number of writes since the channel was created ++ size_t write_number_; ++ ++ /// @brief Write number on which to thrown an exception from asyncSend() ++ size_t throw_on_write_number_; ++ ++ /// @brief Error code to inject on write error trigger ++ size_t ec_on_write_number_; ++ ++ /// @brief Error code to inject on write error trigger ++ boost::system::error_code write_error_ec_; ++ ++ /// @brief Enables routing of 127.0.0.x by to 127.0.0.1 via sequence number. ++ bool route_loopback_; ++ ++ /// @brief Maps loopback addresses to sequence numbers when loopback routing ++ /// is enabled. ++ LoopbackMap loopback_map_; ++ ++ /// @brief Flag which indicates that the manager has been stopped. ++ bool stopped_; ++}; ++ ++/// @brief Defines a pointer to a TestablePingChannel ++typedef boost::shared_ptr<TestablePingChannel> TestablePingChannelPtr; ++ ++/// @brief Defines a callback type for test completion check functions. ++typedef std::function<bool()> TestDoneCallback; ++ ++/// @brief Test fixture class which uses an IOService for time management and/or IO ++class IOServiceTest : public ::testing::Test { ++public: ++ /// @brief Constructor. ++ /// ++ /// Starts test timer which detects timeouts. ++ IOServiceTest() ++ : test_io_service_(new asiolink::IOService()), ++ test_timer_(test_io_service_), ++ run_io_service_timer_(test_io_service_), ++ test_done_cb_() { ++ test_timer_.setup(std::bind(&IOServiceTest::timeoutHandler, this, true), ++ TEST_TIMEOUT, ++ asiolink::IntervalTimer::ONE_SHOT); ++ } ++ ++ /// @brief Indicates if current user is not root ++ /// ++ /// @return True if neither the uid or the effective ++ /// uid is root. ++ static bool notRoot() { ++ return (getuid() != 0 && geteuid() != 0); ++ } ++ ++ /// @brief Destructor. ++ /// ++ /// Removes active clients. ++ virtual ~IOServiceTest() { ++ test_timer_.cancel(); ++ run_io_service_timer_.cancel(); ++ test_io_service_->stopAndPoll(); ++ } ++ ++ /// @brief Callback function invoke upon test timeout. ++ /// ++ /// It stops the IO service and reports test timeout. ++ /// ++ /// @param fail_on_timeout Specifies if test failure should be reported. ++ void timeoutHandler(const bool fail_on_timeout) { ++ if (fail_on_timeout) { ++ ADD_FAILURE() << "Timeout occurred while running the test!"; ++ } ++ ++ test_io_service_->stop(); ++ } ++ ++ /// @brief Stops the IOService if criteria for test completion has been met. ++ /// ++ /// Stops the IOService If there either no test completion callback or the ++ /// call back returns true. ++ void stopIfDone() { ++ // If there is no done test callback or it returns true, stop the service. ++ if (!test_done_cb_ || (test_done_cb_)()) { ++ test_io_service_->stop(); ++ } ++ } ++ ++ /// @brief Posts a call to stop the io service to the io service. ++ /// ++ /// This should be used when stopping the service from callbacks on ++ /// thread pool threads. ++ void stopTestService() { ++ if (!test_io_service_->stopped()) { ++ test_io_service_->post([&]() { test_io_service_->stop(); }); ++ } ++ } ++ ++ /// @brief Runs IO service with optional timeout. ++ /// ++ /// @param timeout number of milliseconds to run the io service. Defaults to ++ /// zero which means run forever. ++ void runIOService(long timeout = 0) { ++ test_io_service_->stop(); ++ test_io_service_->restart(); ++ ++ if (timeout > 0) { ++ run_io_service_timer_.setup(std::bind(&IOServiceTest::timeoutHandler, ++ this, false), ++ timeout, ++ asiolink::IntervalTimer::ONE_SHOT); ++ } ++ ++ test_io_service_->run(); ++ test_io_service_->stopAndPoll(); ++ } ++ ++ /// @brief IO service used in the tests. ++ asiolink::IOServicePtr test_io_service_; ++ ++ /// @brief Asynchronous timer service to detect timeouts. ++ asiolink::IntervalTimer test_timer_; ++ ++ /// @brief Asynchronous timer for running IO service for a specified amount ++ /// of time. ++ asiolink::IntervalTimer run_io_service_timer_; ++ ++ /// @brief Callback function which event handlers can use to check if service ++ /// run should stop. ++ TestDoneCallback test_done_cb_; ++}; ++ ++} // end of namespace ping_check ++} // end of namespace isc ++ ++#endif +diff --git a/src/hooks/dhcp/ping_check/tests/run_unittests.cc b/src/hooks/dhcp/ping_check/tests/run_unittests.cc +new file mode 100644 +index 0000000000..d249e2362e +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/tests/run_unittests.cc +@@ -0,0 +1,19 @@ ++// Copyright (C) 2023-2025 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 <gtest/gtest.h> ++ ++int ++main(int argc, char* argv[]) { ++ ::testing::InitGoogleTest(&argc, argv); ++ isc::log::initLogger(); ++ int result = RUN_ALL_TESTS(); ++ ++ return (result); ++} +diff --git a/src/hooks/dhcp/ping_check/version.cc b/src/hooks/dhcp/ping_check/version.cc +new file mode 100644 +index 0000000000..f2250ab126 +--- /dev/null ++++ b/src/hooks/dhcp/ping_check/version.cc +@@ -0,0 +1,17 @@ ++// Copyright (C) 2023-2025 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 <hooks/hooks.h> ++ ++extern "C" { ++ ++/// @brief returns Kea hooks version. ++int version() { ++ return (KEA_HOOKS_VERSION); ++} ++ ++} +-- +2.39.5 (Apple Git-154) + |