summaryrefslogtreecommitdiff
path: root/scripts/package-build
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/package-build')
-rw-r--r--scripts/package-build/kea/.gitignore1
l---------scripts/package-build/kea/build.py1
-rw-r--r--scripts/package-build/kea/package.toml4
-rw-r--r--scripts/package-build/kea/patches/isc-kea/0001-Add-multithreading-test-mode.patch135
-rw-r--r--scripts/package-build/kea/patches/isc-kea/0002-Add-ping_check-hook-library.patch13277
5 files changed, 0 insertions, 13418 deletions
diff --git a/scripts/package-build/kea/.gitignore b/scripts/package-build/kea/.gitignore
deleted file mode 100644
index 70219f63..00000000
--- a/scripts/package-build/kea/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/isc-kea/
diff --git a/scripts/package-build/kea/build.py b/scripts/package-build/kea/build.py
deleted file mode 120000
index 3c76af73..00000000
--- a/scripts/package-build/kea/build.py
+++ /dev/null
@@ -1 +0,0 @@
-../build.py \ No newline at end of file
diff --git a/scripts/package-build/kea/package.toml b/scripts/package-build/kea/package.toml
deleted file mode 100644
index 872be441..00000000
--- a/scripts/package-build/kea/package.toml
+++ /dev/null
@@ -1,4 +0,0 @@
-[[packages]]
-name = "isc-kea"
-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
deleted file mode 100644
index 981e6f1d..00000000
--- a/scripts/package-build/kea/patches/isc-kea/0001-Add-multithreading-test-mode.patch
+++ /dev/null
@@ -1,135 +0,0 @@
-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
deleted file mode 100644
index c2f172ca..00000000
--- a/scripts/package-build/kea/patches/isc-kea/0002-Add-ping_check-hook-library.patch
+++ /dev/null
@@ -1,13277 +0,0 @@
-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)
-