summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/trigger_rebuild_packages.yml6
-rwxr-xr-xdata/live-build-config/hooks/live/18-enable-disable_services.chroot2
-rw-r--r--scripts/package-build/kea/package.toml2
-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
-rw-r--r--scripts/package-build/libnss-mapuser/.gitignore1
l---------scripts/package-build/libnss-mapuser/build.py1
-rw-r--r--scripts/package-build/libnss-mapuser/package.toml9
8 files changed, 13432 insertions, 1 deletions
diff --git a/.github/workflows/trigger_rebuild_packages.yml b/.github/workflows/trigger_rebuild_packages.yml
index e0065c8f..c98212ea 100644
--- a/.github/workflows/trigger_rebuild_packages.yml
+++ b/.github/workflows/trigger_rebuild_packages.yml
@@ -53,6 +53,8 @@ jobs:
- 'scripts/package-build/kea/**'
keepalived:
- 'scripts/package-build/keepalived/**'
+ libnss-mapuser:
+ - 'scripts/package-build/libnss-mapuser/**'
libpam-radius-auth:
- 'scripts/package-build/libpam-radius-auth/**'
linux-kernel:
@@ -167,6 +169,10 @@ jobs:
trigger_build "keepalived"
fi
+ if [ "${{ steps.changes.outputs.libnss-mapuser }}" == "true" ]; then
+ trigger_build "libnss-mapuser"
+ fi
+
if [ "${{ steps.changes.outputs.libpam-radius-auth }}" == "true" ]; then
trigger_build "libpam-radius-auth"
fi
diff --git a/data/live-build-config/hooks/live/18-enable-disable_services.chroot b/data/live-build-config/hooks/live/18-enable-disable_services.chroot
index 3b4efe38..21b41b23 100755
--- a/data/live-build-config/hooks/live/18-enable-disable_services.chroot
+++ b/data/live-build-config/hooks/live/18-enable-disable_services.chroot
@@ -72,6 +72,8 @@ systemctl disable suricata.service
systemctl disable vyconfd.service
systemctl disable vpp.service
systemctl disable vyos-commitd.service
+systemctl disable netplug.service
+
echo I: Enabling services
systemctl enable vyos-hostsd.service
diff --git a/scripts/package-build/kea/package.toml b/scripts/package-build/kea/package.toml
index 0bfce21e..872be441 100644
--- a/scripts/package-build/kea/package.toml
+++ b/scripts/package-build/kea/package.toml
@@ -1,4 +1,4 @@
[[packages]]
name = "isc-kea"
-commit_id = "debian/2.4.1-3"
+commit_id = "debian/2.6.1-2"
scm_url = "https://salsa.debian.org/debian/isc-kea"
diff --git a/scripts/package-build/kea/patches/isc-kea/0001-Add-multithreading-test-mode.patch b/scripts/package-build/kea/patches/isc-kea/0001-Add-multithreading-test-mode.patch
new file mode 100644
index 00000000..981e6f1d
--- /dev/null
+++ b/scripts/package-build/kea/patches/isc-kea/0001-Add-multithreading-test-mode.patch
@@ -0,0 +1,135 @@
+From cb2b064162e2d5bf09331c619abf76a40130ade1 Mon Sep 17 00:00:00 2001
+From: sarthurdev <s.arthur@vyos.io>
+Date: Wed, 2 Apr 2025 08:48:48 +0000
+Subject: [PATCH 1/2] Add multithreading test mode
+
+---
+ src/bin/dhcp4/json_config_parser.cc | 4 ++++
+ src/bin/dhcp6/json_config_parser.cc | 6 +++++-
+ src/lib/config/cmd_http_listener.cc | 3 +++
+ src/lib/tcp/mt_tcp_listener_mgr.cc | 3 +++
+ src/lib/util/multi_threading_mgr.cc | 3 ++-
+ src/lib/util/multi_threading_mgr.h | 19 +++++++++++++++++++
+ 6 files changed, 36 insertions(+), 2 deletions(-)
+
+diff --git a/src/bin/dhcp4/json_config_parser.cc b/src/bin/dhcp4/json_config_parser.cc
+index c2e34c5..1350816 100644
+--- a/src/bin/dhcp4/json_config_parser.cc
++++ b/src/bin/dhcp4/json_config_parser.cc
+@@ -718,6 +718,10 @@ configureDhcp4Server(Dhcpv4Srv& server, isc::data::ConstElementPtr config_set,
+ LOG_DEBUG(dhcp4_logger, DBG_DHCP4_COMMAND, DHCP4_CONFIG_START)
+ .arg(server.redactConfig(config_set)->str());
+
++ if (check_only) {
++ MultiThreadingMgr::instance().setTestMode(true);
++ }
++
+ auto answer = processDhcp4Config(config_set);
+
+ int status_code = CONTROL_RESULT_SUCCESS;
+diff --git a/src/bin/dhcp6/json_config_parser.cc b/src/bin/dhcp6/json_config_parser.cc
+index 671d69a..a74a568 100644
+--- a/src/bin/dhcp6/json_config_parser.cc
++++ b/src/bin/dhcp6/json_config_parser.cc
+@@ -850,6 +850,10 @@ configureDhcp6Server(Dhcpv6Srv& server, isc::data::ConstElementPtr config_set,
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_COMMAND, DHCP6_CONFIG_START)
+ .arg(server.redactConfig(config_set)->str());
+
++ if (check_only) {
++ MultiThreadingMgr::instance().setTestMode(true);
++ }
++
+ auto answer = processDhcp6Config(config_set);
+
+ int status_code = CONTROL_RESULT_SUCCESS;
+@@ -953,7 +957,7 @@ configureDhcp6Server(Dhcpv6Srv& server, isc::data::ConstElementPtr config_set,
+ // configuration. This will add created subnets and option values into
+ // the server's configuration.
+ // This operation should be exception safe but let's make sure.
+- if (status_code == CONTROL_RESULT_SUCCESS && (!check_only || extra_checks)) {
++ if (status_code == CONTROL_RESULT_SUCCESS && !check_only) {
+ try {
+
+ // Setup the command channel.
+diff --git a/src/lib/config/cmd_http_listener.cc b/src/lib/config/cmd_http_listener.cc
+index 9dfea59..394806e 100644
+--- a/src/lib/config/cmd_http_listener.cc
++++ b/src/lib/config/cmd_http_listener.cc
+@@ -40,6 +40,9 @@ CmdHttpListener::~CmdHttpListener() {
+
+ void
+ CmdHttpListener::start() {
++ if (MultiThreadingMgr::instance().isTestMode()) {
++ return;
++ }
+ // We must be in multi-threading mode.
+ if (!MultiThreadingMgr::instance().getMode()) {
+ isc_throw(InvalidOperation, "CmdHttpListener cannot be started"
+diff --git a/src/lib/tcp/mt_tcp_listener_mgr.cc b/src/lib/tcp/mt_tcp_listener_mgr.cc
+index e880284..4680717 100644
+--- a/src/lib/tcp/mt_tcp_listener_mgr.cc
++++ b/src/lib/tcp/mt_tcp_listener_mgr.cc
+@@ -40,6 +40,9 @@ MtTcpListenerMgr::~MtTcpListenerMgr() {
+
+ void
+ MtTcpListenerMgr::start() {
++ if (MultiThreadingMgr::instance().isTestMode()) {
++ return;
++ }
+ // We must be in multi-threading mode.
+ if (!MultiThreadingMgr::instance().getMode()) {
+ isc_throw(InvalidOperation, "MtTcpListenerMgr cannot be started"
+diff --git a/src/lib/util/multi_threading_mgr.cc b/src/lib/util/multi_threading_mgr.cc
+index d1526b9..cab284d 100644
+--- a/src/lib/util/multi_threading_mgr.cc
++++ b/src/lib/util/multi_threading_mgr.cc
+@@ -14,7 +14,8 @@ namespace isc {
+ namespace util {
+
+ MultiThreadingMgr::MultiThreadingMgr()
+- : enabled_(false), critical_section_count_(0), thread_pool_size_(0) {
++ : enabled_(false), test_mode_(false), critical_section_count_(0),
++ thread_pool_size_(0) {
+ }
+
+ MultiThreadingMgr::~MultiThreadingMgr() {
+diff --git a/src/lib/util/multi_threading_mgr.h b/src/lib/util/multi_threading_mgr.h
+index e86c488..f3da67b 100644
+--- a/src/lib/util/multi_threading_mgr.h
++++ b/src/lib/util/multi_threading_mgr.h
+@@ -154,6 +154,22 @@ public:
+ /// @param enabled The new mode.
+ void setMode(bool enabled);
+
++ /// @brief Sets or clears the test mode for @c MultiThreadingMgr.
++ ///
++ /// @param test_mode A flag which indicates that the @c MultiThreadingMgr is
++ /// in the test mode (if true), or not (if false).
++ void setTestMode(const bool test_mode) {
++ test_mode_ = test_mode;
++ }
++
++ /// @brief Checks if the @c MultiThreadingMgr is in the test mode.
++ ///
++ /// @return true if the @c MultiThreadingMgr is in the test mode, false
++ /// otherwise.
++ bool isTestMode() const {
++ return (test_mode_);
++ }
++
+ /// @brief Enter critical section.
+ ///
+ /// When entering @ref MultiThreadingCriticalSection, increment internal
+@@ -308,6 +324,9 @@ private:
+ /// otherwise.
+ bool enabled_;
+
++ /// @brief Indicates if the @c MultiThreadingMgr is in the test mode.
++ bool test_mode_;
++
+ /// @brief The critical section count.
+ ///
+ /// In case the configuration is applied within a
+--
+2.39.5
+
diff --git a/scripts/package-build/kea/patches/isc-kea/0002-Add-ping_check-hook-library.patch b/scripts/package-build/kea/patches/isc-kea/0002-Add-ping_check-hook-library.patch
new file mode 100644
index 00000000..c2f172ca
--- /dev/null
+++ b/scripts/package-build/kea/patches/isc-kea/0002-Add-ping_check-hook-library.patch
@@ -0,0 +1,13277 @@
+From 6f198a187195a7fa4ad2cf9d147532bd64724f65 Mon Sep 17 00:00:00 2001
+From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
+Date: Mon, 24 Mar 2025 19:38:34 +0100
+Subject: [PATCH] Add ping_check hook library
+
+---
+ configure.ac | 3 +
+ src/hooks/dhcp/Makefile.am | 2 +-
+ src/hooks/dhcp/ping_check/Doxyfile | 2568 +++++++++++++++++
+ src/hooks/dhcp/ping_check/Makefile.am | 104 +
+ src/hooks/dhcp/ping_check/config_cache.cc | 107 +
+ src/hooks/dhcp/ping_check/config_cache.h | 146 +
+ src/hooks/dhcp/ping_check/icmp_endpoint.h | 134 +
+ src/hooks/dhcp/ping_check/icmp_msg.cc | 112 +
+ src/hooks/dhcp/ping_check/icmp_msg.h | 223 ++
+ src/hooks/dhcp/ping_check/icmp_socket.h | 359 +++
+ .../dhcp/ping_check/libloadtests/.gitignore | 1 +
+ .../dhcp/ping_check/libloadtests/Makefile.am | 60 +
+ .../libloadtests/load_unload_unittests.cc | 107 +
+ .../dhcp/ping_check/libloadtests/meson.build | 21 +
+ .../ping_check/libloadtests/run_unittests.cc | 19 +
+ src/hooks/dhcp/ping_check/meson.build | 41 +
+ src/hooks/dhcp/ping_check/ping_channel.cc | 466 +++
+ src/hooks/dhcp/ping_check/ping_channel.h | 371 +++
+ src/hooks/dhcp/ping_check/ping_check.dox | 44 +
+ .../dhcp/ping_check/ping_check_callouts.cc | 240 ++
+ .../dhcp/ping_check/ping_check_config.cc | 98 +
+ src/hooks/dhcp/ping_check/ping_check_config.h | 134 +
+ src/hooks/dhcp/ping_check/ping_check_log.cc | 17 +
+ src/hooks/dhcp/ping_check/ping_check_log.h | 23 +
+ .../dhcp/ping_check/ping_check_messages.cc | 99 +
+ .../dhcp/ping_check/ping_check_messages.h | 50 +
+ .../dhcp/ping_check/ping_check_messages.mes | 229 ++
+ src/hooks/dhcp/ping_check/ping_check_mgr.cc | 798 +++++
+ src/hooks/dhcp/ping_check/ping_check_mgr.h | 436 +++
+ src/hooks/dhcp/ping_check/ping_context.cc | 237 ++
+ src/hooks/dhcp/ping_check/ping_context.h | 280 ++
+ .../dhcp/ping_check/ping_context_store.cc | 144 +
+ .../dhcp/ping_check/ping_context_store.h | 240 ++
+ src/hooks/dhcp/ping_check/tests/.gitignore | 1 +
+ src/hooks/dhcp/ping_check/tests/Makefile.am | 70 +
+ .../tests/config_cache_unittests.cc | 245 ++
+ .../tests/icmp_endpoint_unittests.cc | 44 +
+ .../ping_check/tests/icmp_msg_unittests.cc | 172 ++
+ .../ping_check/tests/icmp_socket_unittests.cc | 380 +++
+ src/hooks/dhcp/ping_check/tests/meson.build | 21 +
+ .../tests/ping_channel_unittests.cc | 821 ++++++
+ .../tests/ping_check_config_unittests.cc | 287 ++
+ .../tests/ping_check_mgr_unittests.cc | 1878 ++++++++++++
+ .../tests/ping_context_store_unittests.cc | 467 +++
+ .../tests/ping_context_unittests.cc | 146 +
+ .../dhcp/ping_check/tests/ping_test_utils.h | 396 +++
+ .../dhcp/ping_check/tests/run_unittests.cc | 19 +
+ src/hooks/dhcp/ping_check/version.cc | 17 +
+ 48 files changed, 12876 insertions(+), 1 deletion(-)
+ create mode 100644 src/hooks/dhcp/ping_check/Doxyfile
+ create mode 100644 src/hooks/dhcp/ping_check/Makefile.am
+ create mode 100644 src/hooks/dhcp/ping_check/config_cache.cc
+ create mode 100644 src/hooks/dhcp/ping_check/config_cache.h
+ create mode 100644 src/hooks/dhcp/ping_check/icmp_endpoint.h
+ create mode 100644 src/hooks/dhcp/ping_check/icmp_msg.cc
+ create mode 100644 src/hooks/dhcp/ping_check/icmp_msg.h
+ create mode 100644 src/hooks/dhcp/ping_check/icmp_socket.h
+ create mode 100644 src/hooks/dhcp/ping_check/libloadtests/.gitignore
+ create mode 100644 src/hooks/dhcp/ping_check/libloadtests/Makefile.am
+ create mode 100644 src/hooks/dhcp/ping_check/libloadtests/load_unload_unittests.cc
+ create mode 100644 src/hooks/dhcp/ping_check/libloadtests/meson.build
+ create mode 100644 src/hooks/dhcp/ping_check/libloadtests/run_unittests.cc
+ create mode 100644 src/hooks/dhcp/ping_check/meson.build
+ create mode 100644 src/hooks/dhcp/ping_check/ping_channel.cc
+ create mode 100644 src/hooks/dhcp/ping_check/ping_channel.h
+ create mode 100644 src/hooks/dhcp/ping_check/ping_check.dox
+ create mode 100644 src/hooks/dhcp/ping_check/ping_check_callouts.cc
+ create mode 100644 src/hooks/dhcp/ping_check/ping_check_config.cc
+ create mode 100644 src/hooks/dhcp/ping_check/ping_check_config.h
+ create mode 100644 src/hooks/dhcp/ping_check/ping_check_log.cc
+ create mode 100644 src/hooks/dhcp/ping_check/ping_check_log.h
+ create mode 100644 src/hooks/dhcp/ping_check/ping_check_messages.cc
+ create mode 100644 src/hooks/dhcp/ping_check/ping_check_messages.h
+ create mode 100644 src/hooks/dhcp/ping_check/ping_check_messages.mes
+ create mode 100644 src/hooks/dhcp/ping_check/ping_check_mgr.cc
+ create mode 100644 src/hooks/dhcp/ping_check/ping_check_mgr.h
+ create mode 100644 src/hooks/dhcp/ping_check/ping_context.cc
+ create mode 100644 src/hooks/dhcp/ping_check/ping_context.h
+ create mode 100644 src/hooks/dhcp/ping_check/ping_context_store.cc
+ create mode 100644 src/hooks/dhcp/ping_check/ping_context_store.h
+ create mode 100644 src/hooks/dhcp/ping_check/tests/.gitignore
+ create mode 100644 src/hooks/dhcp/ping_check/tests/Makefile.am
+ create mode 100644 src/hooks/dhcp/ping_check/tests/config_cache_unittests.cc
+ create mode 100644 src/hooks/dhcp/ping_check/tests/icmp_endpoint_unittests.cc
+ create mode 100644 src/hooks/dhcp/ping_check/tests/icmp_msg_unittests.cc
+ create mode 100644 src/hooks/dhcp/ping_check/tests/icmp_socket_unittests.cc
+ create mode 100644 src/hooks/dhcp/ping_check/tests/meson.build
+ create mode 100644 src/hooks/dhcp/ping_check/tests/ping_channel_unittests.cc
+ create mode 100644 src/hooks/dhcp/ping_check/tests/ping_check_config_unittests.cc
+ create mode 100644 src/hooks/dhcp/ping_check/tests/ping_check_mgr_unittests.cc
+ create mode 100644 src/hooks/dhcp/ping_check/tests/ping_context_store_unittests.cc
+ create mode 100644 src/hooks/dhcp/ping_check/tests/ping_context_unittests.cc
+ create mode 100644 src/hooks/dhcp/ping_check/tests/ping_test_utils.h
+ create mode 100644 src/hooks/dhcp/ping_check/tests/run_unittests.cc
+ create mode 100644 src/hooks/dhcp/ping_check/version.cc
+
+diff --git a/configure.ac b/configure.ac
+index cc1b31af71..23c8eefb81 100644
+--- a/configure.ac
++++ b/configure.ac
+@@ -1582,6 +1582,9 @@ AC_CONFIG_FILES([src/hooks/dhcp/lease_cmds/tests/Makefile])
+ AC_CONFIG_FILES([src/hooks/dhcp/mysql_cb/Makefile])
+ AC_CONFIG_FILES([src/hooks/dhcp/mysql_cb/libloadtests/Makefile])
+ AC_CONFIG_FILES([src/hooks/dhcp/mysql_cb/tests/Makefile])
++AC_CONFIG_FILES([src/hooks/dhcp/ping_check/Makefile])
++AC_CONFIG_FILES([src/hooks/dhcp/ping_check/libloadtests/Makefile])
++AC_CONFIG_FILES([src/hooks/dhcp/ping_check/tests/Makefile])
+ AC_CONFIG_FILES([src/hooks/dhcp/pgsql_cb/Makefile])
+ AC_CONFIG_FILES([src/hooks/dhcp/pgsql_cb/libloadtests/Makefile])
+ AC_CONFIG_FILES([src/hooks/dhcp/pgsql_cb/tests/Makefile])
+diff --git a/src/hooks/dhcp/Makefile.am b/src/hooks/dhcp/Makefile.am
+index 1b77976424..806e310a17 100644
+--- a/src/hooks/dhcp/Makefile.am
++++ b/src/hooks/dhcp/Makefile.am
+@@ -8,4 +8,4 @@ if HAVE_PGSQL
+ SUBDIRS += pgsql_cb
+ endif
+
+-SUBDIRS += run_script stat_cmds user_chk
++SUBDIRS += run_script stat_cmds user_chk ping_check
+diff --git a/src/hooks/dhcp/ping_check/Doxyfile b/src/hooks/dhcp/ping_check/Doxyfile
+new file mode 100644
+index 0000000000..7c8554b557
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/Doxyfile
+@@ -0,0 +1,2568 @@
++# Doxyfile 1.9.1
++
++# This file describes the settings to be used by the documentation system
++# doxygen (www.doxygen.org) for a project.
++#
++# All text after a double hash (##) is considered a comment and is placed in
++# front of the TAG it is preceding.
++#
++# All text after a single hash (#) is considered a comment and will be ignored.
++# The format is:
++# TAG = value [value, ...]
++# For lists, items can also be appended using:
++# TAG += value [value, ...]
++# Values that contain spaces should be placed between quotes (\" \").
++
++#---------------------------------------------------------------------------
++# Project related configuration options
++#---------------------------------------------------------------------------
++
++# This tag specifies the encoding used for all characters in the configuration
++# file that follow. The default is UTF-8 which is also the encoding used for all
++# text before the first occurrence of this tag. Doxygen uses libiconv (or the
++# iconv built into libc) for the transcoding. See
++# https://www.gnu.org/software/libiconv/ for the list of possible encodings.
++# The default value is: UTF-8.
++
++DOXYFILE_ENCODING = UTF-8
++
++# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by
++# double-quotes, unless you are using Doxywizard) that should identify the
++# project for which the documentation is generated. This name is used in the
++# title of most generated pages and in a few other places.
++# The default value is: My Project.
++
++PROJECT_NAME = "Kea Ping Check Hooks Library"
++
++# The PROJECT_NUMBER tag can be used to enter a project or revision number. This
++# could be handy for archiving the generated documentation or if some version
++# control system is used.
++
++PROJECT_NUMBER =
++
++# Using the PROJECT_BRIEF tag one can provide an optional one line description
++# for a project that appears at the top of each page and should give viewer a
++# quick idea about the purpose of the project. Keep the description short.
++
++PROJECT_BRIEF =
++
++# With the PROJECT_LOGO tag one can specify a logo or an icon that is included
++# in the documentation. The maximum height of the logo should not exceed 55
++# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy
++# the logo to the output directory.
++
++PROJECT_LOGO = ../../../../../doc/images/kea-logo-100x70.png
++
++# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path
++# into which the generated documentation will be written. If a relative path is
++# entered, it will be relative to the location where doxygen was started. If
++# left blank the current directory will be used.
++
++OUTPUT_DIRECTORY = html
++
++# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub-
++# directories (in 2 levels) under the output directory of each output format and
++# will distribute the generated files over these directories. Enabling this
++# option can be useful when feeding doxygen a huge amount of source files, where
++# putting all generated files in the same directory would otherwise causes
++# performance problems for the file system.
++# The default value is: NO.
++
++CREATE_SUBDIRS = YES
++
++# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII
++# characters to appear in the names of generated files. If set to NO, non-ASCII
++# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode
++# U+3044.
++# The default value is: NO.
++
++ALLOW_UNICODE_NAMES = NO
++
++# The OUTPUT_LANGUAGE tag is used to specify the language in which all
++# documentation generated by doxygen is written. Doxygen will use this
++# information to generate all constant output in the proper language.
++# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese,
++# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States),
++# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian,
++# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages),
++# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian,
++# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian,
++# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish,
++# Ukrainian and Vietnamese.
++# The default value is: English.
++
++OUTPUT_LANGUAGE = English
++
++# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all
++# documentation generated by doxygen is written. Doxygen will use this
++# information to generate all generated output in the proper direction.
++# Possible values are: None, LTR, RTL and Context.
++# The default value is: None.
++
++OUTPUT_TEXT_DIRECTION = None
++
++# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member
++# descriptions after the members that are listed in the file and class
++# documentation (similar to Javadoc). Set to NO to disable this.
++# The default value is: YES.
++
++BRIEF_MEMBER_DESC = YES
++
++# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief
++# description of a member or function before the detailed description
++#
++# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the
++# brief descriptions will be completely suppressed.
++# The default value is: YES.
++
++REPEAT_BRIEF = YES
++
++# This tag implements a quasi-intelligent brief description abbreviator that is
++# used to form the text in various listings. Each string in this list, if found
++# as the leading text of the brief description, will be stripped from the text
++# and the result, after processing the whole list, is used as the annotated
++# text. Otherwise, the brief description is used as-is. If left blank, the
++# following values are used ($name is automatically replaced with the name of
++# the entity):The $name class, The $name widget, The $name file, is, provides,
++# specifies, contains, represents, a, an and the.
++
++ABBREVIATE_BRIEF =
++
++# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then
++# doxygen will generate a detailed section even if there is only a brief
++# description.
++# The default value is: NO.
++
++ALWAYS_DETAILED_SEC = NO
++
++# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
++# inherited members of a class in the documentation of that class as if those
++# members were ordinary class members. Constructors, destructors and assignment
++# operators of the base classes will not be shown.
++# The default value is: NO.
++
++INLINE_INHERITED_MEMB = NO
++
++# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path
++# before files name in the file list and in the header files. If set to NO the
++# shortest path that makes the file name unique will be used
++# The default value is: YES.
++
++FULL_PATH_NAMES = NO
++
++# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path.
++# Stripping is only done if one of the specified strings matches the left-hand
++# part of the path. The tag can be used to show relative paths in the file list.
++# If left blank the directory from which doxygen is run is used as the path to
++# strip.
++#
++# Note that you can specify absolute paths here, but also relative paths, which
++# will be relative from the directory where doxygen is started.
++# This tag requires that the tag FULL_PATH_NAMES is set to YES.
++
++STRIP_FROM_PATH =
++
++# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the
++# path mentioned in the documentation of a class, which tells the reader which
++# header file to include in order to use a class. If left blank only the name of
++# the header file containing the class definition is used. Otherwise one should
++# specify the list of include paths that are normally passed to the compiler
++# using the -I flag.
++
++STRIP_FROM_INC_PATH =
++
++# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but
++# less readable) file names. This can be useful is your file systems doesn't
++# support long names like on DOS, Mac, or CD-ROM.
++# The default value is: NO.
++
++SHORT_NAMES = NO
++
++# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the
++# first line (until the first dot) of a Javadoc-style comment as the brief
++# description. If set to NO, the Javadoc-style will behave just like regular Qt-
++# style comments (thus requiring an explicit @brief command for a brief
++# description.)
++# The default value is: NO.
++
++JAVADOC_AUTOBRIEF = YES
++
++# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line
++# such as
++# /***************
++# as being the beginning of a Javadoc-style comment "banner". If set to NO, the
++# Javadoc-style will behave just like regular comments and it will not be
++# interpreted by doxygen.
++# The default value is: NO.
++
++JAVADOC_BANNER = NO
++
++# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first
++# line (until the first dot) of a Qt-style comment as the brief description. If
++# set to NO, the Qt-style will behave just like regular Qt-style comments (thus
++# requiring an explicit \brief command for a brief description.)
++# The default value is: NO.
++
++QT_AUTOBRIEF = NO
++
++# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a
++# multi-line C++ special comment block (i.e. a block of //! or /// comments) as
++# a brief description. This used to be the default behavior. The new default is
++# to treat a multi-line C++ comment block as a detailed description. Set this
++# tag to YES if you prefer the old behavior instead.
++#
++# Note that setting this tag to YES also means that rational rose comments are
++# not recognized any more.
++# The default value is: NO.
++
++MULTILINE_CPP_IS_BRIEF = NO
++
++# By default Python docstrings are displayed as preformatted text and doxygen's
++# special commands cannot be used. By setting PYTHON_DOCSTRING to NO the
++# doxygen's special commands can be used and the contents of the docstring
++# documentation blocks is shown as doxygen documentation.
++# The default value is: YES.
++
++PYTHON_DOCSTRING = YES
++
++# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the
++# documentation from any documented member that it re-implements.
++# The default value is: YES.
++
++INHERIT_DOCS = YES
++
++# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new
++# page for each member. If set to NO, the documentation of a member will be part
++# of the file/class/namespace that contains it.
++# The default value is: NO.
++
++SEPARATE_MEMBER_PAGES = NO
++
++# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen
++# uses this value to replace tabs by spaces in code fragments.
++# Minimum value: 1, maximum value: 16, default value: 4.
++
++TAB_SIZE = 4
++
++# This tag can be used to specify a number of aliases that act as commands in
++# the documentation. An alias has the form:
++# name=value
++# For example adding
++# "sideeffect=@par Side Effects:\n"
++# will allow you to put the command \sideeffect (or @sideeffect) in the
++# documentation, which will result in a user-defined paragraph with heading
++# "Side Effects:". You can put \n's in the value part of an alias to insert
++# newlines (in the resulting output). You can put ^^ in the value part of an
++# alias to insert a newline as if a physical newline was in the original file.
++# When you need a literal { or } or , in the value part of an alias you have to
++# escape them by means of a backslash (\), this can lead to conflicts with the
++# commands \{ and \} for these it is advised to use the version @{ and @} or use
++# a double escape (\\{ and \\})
++
++ALIASES =
++
++# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
++# only. Doxygen will then generate output that is more tailored for C. For
++# instance, some of the names that are used will be different. The list of all
++# members will be omitted, etc.
++# The default value is: NO.
++
++OPTIMIZE_OUTPUT_FOR_C = NO
++
++# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or
++# Python sources only. Doxygen will then generate output that is more tailored
++# for that language. For instance, namespaces will be presented as packages,
++# qualified scopes will look different, etc.
++# The default value is: NO.
++
++OPTIMIZE_OUTPUT_JAVA = NO
++
++# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran
++# sources. Doxygen will then generate output that is tailored for Fortran.
++# The default value is: NO.
++
++OPTIMIZE_FOR_FORTRAN = NO
++
++# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL
++# sources. Doxygen will then generate output that is tailored for VHDL.
++# The default value is: NO.
++
++OPTIMIZE_OUTPUT_VHDL = NO
++
++# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice
++# sources only. Doxygen will then generate output that is more tailored for that
++# language. For instance, namespaces will be presented as modules, types will be
++# separated into more groups, etc.
++# The default value is: NO.
++
++OPTIMIZE_OUTPUT_SLICE = NO
++
++# Doxygen selects the parser to use depending on the extension of the files it
++# parses. With this tag you can assign which parser to use for a given
++# extension. Doxygen has a built-in mapping, but you can override or extend it
++# using this tag. The format is ext=language, where ext is a file extension, and
++# language is one of the parsers supported by doxygen: IDL, Java, JavaScript,
++# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, VHDL,
++# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran:
++# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser
++# tries to guess whether the code is fixed or free formatted code, this is the
++# default for Fortran type files). For instance to make doxygen treat .inc files
++# as Fortran files (default is PHP), and .f files as C (default is Fortran),
++# use: inc=Fortran f=C.
++#
++# Note: For files without extension you can use no_extension as a placeholder.
++#
++# Note that for custom extensions you also need to set FILE_PATTERNS otherwise
++# the files are not read by doxygen. When specifying no_extension you should add
++# * to the FILE_PATTERNS.
++#
++# Note see also the list of default file extension mappings.
++
++EXTENSION_MAPPING =
++
++# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments
++# according to the Markdown format, which allows for more readable
++# documentation. See https://daringfireball.net/projects/markdown/ for details.
++# The output of markdown processing is further processed by doxygen, so you can
++# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in
++# case of backward compatibilities issues.
++# The default value is: YES.
++
++MARKDOWN_SUPPORT = YES
++
++# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up
++# to that level are automatically included in the table of contents, even if
++# they do not have an id attribute.
++# Note: This feature currently applies only to Markdown headings.
++# Minimum value: 0, maximum value: 99, default value: 5.
++# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.
++
++TOC_INCLUDE_HEADINGS = 5
++
++# When enabled doxygen tries to link words that correspond to documented
++# classes, or namespaces to their corresponding documentation. Such a link can
++# be prevented in individual cases by putting a % sign in front of the word or
++# globally by setting AUTOLINK_SUPPORT to NO.
++# The default value is: YES.
++
++AUTOLINK_SUPPORT = YES
++
++# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want
++# to include (a tag file for) the STL sources as input, then you should set this
++# tag to YES in order to let doxygen match functions declarations and
++# definitions whose arguments contain STL classes (e.g. func(std::string);
++# versus func(std::string) {}). This also make the inheritance and collaboration
++# diagrams that involve STL classes more complete and accurate.
++# The default value is: NO.
++
++BUILTIN_STL_SUPPORT = YES
++
++# If you use Microsoft's C++/CLI language, you should set this option to YES to
++# enable parsing support.
++# The default value is: NO.
++
++CPP_CLI_SUPPORT = NO
++
++# Set the SIP_SUPPORT tag to YES if your project consists of sip (see:
++# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen
++# will parse them like normal C++ but will assume all classes use public instead
++# of private inheritance when no explicit protection keyword is present.
++# The default value is: NO.
++
++SIP_SUPPORT = NO
++
++# For Microsoft's IDL there are propget and propput attributes to indicate
++# getter and setter methods for a property. Setting this option to YES will make
++# doxygen to replace the get and set methods by a property in the documentation.
++# This will only work if the methods are indeed getting or setting a simple
++# type. If this is not the case, or you want to show the methods anyway, you
++# should set this option to NO.
++# The default value is: YES.
++
++IDL_PROPERTY_SUPPORT = YES
++
++# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
++# tag is set to YES then doxygen will reuse the documentation of the first
++# member in the group (if any) for the other members of the group. By default
++# all members of a group must be documented explicitly.
++# The default value is: NO.
++
++DISTRIBUTE_GROUP_DOC = NO
++
++# If one adds a struct or class to a group and this option is enabled, then also
++# any nested class or struct is added to the same group. By default this option
++# is disabled and one has to add nested compounds explicitly via \ingroup.
++# The default value is: NO.
++
++GROUP_NESTED_COMPOUNDS = NO
++
++# Set the SUBGROUPING tag to YES to allow class member groups of the same type
++# (for instance a group of public functions) to be put as a subgroup of that
++# type (e.g. under the Public Functions section). Set it to NO to prevent
++# subgrouping. Alternatively, this can be done per class using the
++# \nosubgrouping command.
++# The default value is: YES.
++
++SUBGROUPING = YES
++
++# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions
++# are shown inside the group in which they are included (e.g. using \ingroup)
++# instead of on a separate page (for HTML and Man pages) or section (for LaTeX
++# and RTF).
++#
++# Note that this feature does not work in combination with
++# SEPARATE_MEMBER_PAGES.
++# The default value is: NO.
++
++INLINE_GROUPED_CLASSES = NO
++
++# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions
++# with only public data fields or simple typedef fields will be shown inline in
++# the documentation of the scope in which they are defined (i.e. file,
++# namespace, or group documentation), provided this scope is documented. If set
++# to NO, structs, classes, and unions are shown on a separate page (for HTML and
++# Man pages) or section (for LaTeX and RTF).
++# The default value is: NO.
++
++INLINE_SIMPLE_STRUCTS = NO
++
++# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or
++# enum is documented as struct, union, or enum with the name of the typedef. So
++# typedef struct TypeS {} TypeT, will appear in the documentation as a struct
++# with name TypeT. When disabled the typedef will appear as a member of a file,
++# namespace, or class. And the struct will be named TypeS. This can typically be
++# useful for C code in case the coding convention dictates that all compound
++# types are typedef'ed and only the typedef is referenced, never the tag name.
++# The default value is: NO.
++
++TYPEDEF_HIDES_STRUCT = NO
++
++# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This
++# cache is used to resolve symbols given their name and scope. Since this can be
++# an expensive process and often the same symbol appears multiple times in the
++# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small
++# doxygen will become slower. If the cache is too large, memory is wasted. The
++# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range
++# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536
++# symbols. At the end of a run doxygen will report the cache usage and suggest
++# the optimal cache size from a speed point of view.
++# Minimum value: 0, maximum value: 9, default value: 0.
++
++LOOKUP_CACHE_SIZE = 0
++
++# The NUM_PROC_THREADS specifies the number threads doxygen is allowed to use
++# during processing. When set to 0 doxygen will based this on the number of
++# cores available in the system. You can set it explicitly to a value larger
++# than 0 to get more control over the balance between CPU load and processing
++# speed. At this moment only the input processing can be done using multiple
++# threads. Since this is still an experimental feature the default is set to 1,
++# which effectively disables parallel processing. Please report any issues you
++# encounter. Generating dot graphs in parallel is controlled by the
++# DOT_NUM_THREADS setting.
++# Minimum value: 0, maximum value: 32, default value: 1.
++
++NUM_PROC_THREADS = 1
++
++#---------------------------------------------------------------------------
++# Build related configuration options
++#---------------------------------------------------------------------------
++
++# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in
++# documentation are documented, even if no documentation was available. Private
++# class members and static file members will be hidden unless the
++# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES.
++# Note: This will also disable the warnings about undocumented members that are
++# normally produced when WARNINGS is set to YES.
++# The default value is: NO.
++
++EXTRACT_ALL = YES
++
++# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will
++# be included in the documentation.
++# The default value is: NO.
++
++EXTRACT_PRIVATE = NO
++
++# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual
++# methods of a class will be included in the documentation.
++# The default value is: NO.
++
++EXTRACT_PRIV_VIRTUAL = NO
++
++# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal
++# scope will be included in the documentation.
++# The default value is: NO.
++
++EXTRACT_PACKAGE = NO
++
++# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be
++# included in the documentation.
++# The default value is: NO.
++
++EXTRACT_STATIC = NO
++
++# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined
++# locally in source files will be included in the documentation. If set to NO,
++# only classes defined in header files are included. Does not have any effect
++# for Java sources.
++# The default value is: YES.
++
++EXTRACT_LOCAL_CLASSES = YES
++
++# This flag is only useful for Objective-C code. If set to YES, local methods,
++# which are defined in the implementation section but not in the interface are
++# included in the documentation. If set to NO, only methods in the interface are
++# included.
++# The default value is: NO.
++
++EXTRACT_LOCAL_METHODS = NO
++
++# If this flag is set to YES, the members of anonymous namespaces will be
++# extracted and appear in the documentation as a namespace called
++# 'anonymous_namespace{file}', where file will be replaced with the base name of
++# the file that contains the anonymous namespace. By default anonymous namespace
++# are hidden.
++# The default value is: NO.
++
++EXTRACT_ANON_NSPACES = NO
++
++# If this flag is set to YES, the name of an unnamed parameter in a declaration
++# will be determined by the corresponding definition. By default unnamed
++# parameters remain unnamed in the output.
++# The default value is: YES.
++
++RESOLVE_UNNAMED_PARAMS = YES
++
++# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all
++# undocumented members inside documented classes or files. If set to NO these
++# members will be included in the various overviews, but no documentation
++# section is generated. This option has no effect if EXTRACT_ALL is enabled.
++# The default value is: NO.
++
++HIDE_UNDOC_MEMBERS = NO
++
++# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all
++# undocumented classes that are normally visible in the class hierarchy. If set
++# to NO, these classes will be included in the various overviews. This option
++# has no effect if EXTRACT_ALL is enabled.
++# The default value is: NO.
++
++HIDE_UNDOC_CLASSES = NO
++
++# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend
++# declarations. If set to NO, these declarations will be included in the
++# documentation.
++# The default value is: NO.
++
++HIDE_FRIEND_COMPOUNDS = NO
++
++# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any
++# documentation blocks found inside the body of a function. If set to NO, these
++# blocks will be appended to the function's detailed documentation block.
++# The default value is: NO.
++
++HIDE_IN_BODY_DOCS = NO
++
++# The INTERNAL_DOCS tag determines if documentation that is typed after a
++# \internal command is included. If the tag is set to NO then the documentation
++# will be excluded. Set it to YES to include the internal documentation.
++# The default value is: NO.
++
++INTERNAL_DOCS = NO
++
++# With the correct setting of option CASE_SENSE_NAMES doxygen will better be
++# able to match the capabilities of the underlying filesystem. In case the
++# filesystem is case sensitive (i.e. it supports files in the same directory
++# whose names only differ in casing), the option must be set to YES to properly
++# deal with such files in case they appear in the input. For filesystems that
++# are not case sensitive the option should be be set to NO to properly deal with
++# output files written for symbols that only differ in casing, such as for two
++# classes, one named CLASS and the other named Class, and to also support
++# references to files without having to specify the exact matching casing. On
++# Windows (including Cygwin) and MacOS, users should typically set this option
++# to NO, whereas on Linux or other Unix flavors it should typically be set to
++# YES.
++# The default value is: system dependent.
++
++CASE_SENSE_NAMES = YES
++
++# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with
++# their full class and namespace scopes in the documentation. If set to YES, the
++# scope will be hidden.
++# The default value is: NO.
++
++HIDE_SCOPE_NAMES = NO
++
++# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will
++# append additional text to a page's title, such as Class Reference. If set to
++# YES the compound reference will be hidden.
++# The default value is: NO.
++
++HIDE_COMPOUND_REFERENCE= NO
++
++# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of
++# the files that are included by a file in the documentation of that file.
++# The default value is: YES.
++
++SHOW_INCLUDE_FILES = YES
++
++# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each
++# grouped member an include statement to the documentation, telling the reader
++# which file to include in order to use the member.
++# The default value is: NO.
++
++SHOW_GROUPED_MEMB_INC = NO
++
++# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include
++# files with double quotes in the documentation rather than with sharp brackets.
++# The default value is: NO.
++
++FORCE_LOCAL_INCLUDES = NO
++
++# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the
++# documentation for inline members.
++# The default value is: YES.
++
++INLINE_INFO = YES
++
++# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the
++# (detailed) documentation of file and class members alphabetically by member
++# name. If set to NO, the members will appear in declaration order.
++# The default value is: YES.
++
++SORT_MEMBER_DOCS = YES
++
++# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief
++# descriptions of file, namespace and class members alphabetically by member
++# name. If set to NO, the members will appear in declaration order. Note that
++# this will also influence the order of the classes in the class list.
++# The default value is: NO.
++
++SORT_BRIEF_DOCS = YES
++
++# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the
++# (brief and detailed) documentation of class members so that constructors and
++# destructors are listed first. If set to NO the constructors will appear in the
++# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS.
++# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief
++# member documentation.
++# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting
++# detailed member documentation.
++# The default value is: NO.
++
++SORT_MEMBERS_CTORS_1ST = YES
++
++# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy
++# of group names into alphabetical order. If set to NO the group names will
++# appear in their defined order.
++# The default value is: NO.
++
++SORT_GROUP_NAMES = YES
++
++# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by
++# fully-qualified names, including namespaces. If set to NO, the class list will
++# be sorted only by class name, not including the namespace part.
++# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.
++# Note: This option applies only to the class list, not to the alphabetical
++# list.
++# The default value is: NO.
++
++SORT_BY_SCOPE_NAME = NO
++
++# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper
++# type resolution of all parameters of a function it will reject a match between
++# the prototype and the implementation of a member function even if there is
++# only one candidate or it is obvious which candidate to choose by doing a
++# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still
++# accept a match between prototype and implementation in such cases.
++# The default value is: NO.
++
++STRICT_PROTO_MATCHING = NO
++
++# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo
++# list. This list is created by putting \todo commands in the documentation.
++# The default value is: YES.
++
++GENERATE_TODOLIST = YES
++
++# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test
++# list. This list is created by putting \test commands in the documentation.
++# The default value is: YES.
++
++GENERATE_TESTLIST = YES
++
++# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug
++# list. This list is created by putting \bug commands in the documentation.
++# The default value is: YES.
++
++GENERATE_BUGLIST = YES
++
++# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO)
++# the deprecated list. This list is created by putting \deprecated commands in
++# the documentation.
++# The default value is: YES.
++
++GENERATE_DEPRECATEDLIST= YES
++
++# The ENABLED_SECTIONS tag can be used to enable conditional documentation
++# sections, marked by \if <section_label> ... \endif and \cond <section_label>
++# ... \endcond blocks.
++
++ENABLED_SECTIONS =
++
++# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the
++# initial value of a variable or macro / define can have for it to appear in the
++# documentation. If the initializer consists of more lines than specified here
++# it will be hidden. Use a value of 0 to hide initializers completely. The
++# appearance of the value of individual variables and macros / defines can be
++# controlled using \showinitializer or \hideinitializer command in the
++# documentation regardless of this setting.
++# Minimum value: 0, maximum value: 10000, default value: 30.
++
++MAX_INITIALIZER_LINES = 30
++
++# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at
++# the bottom of the documentation of classes and structs. If set to YES, the
++# list will mention the files that were used to generate the documentation.
++# The default value is: YES.
++
++SHOW_USED_FILES = YES
++
++# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This
++# will remove the Files entry from the Quick Index and from the Folder Tree View
++# (if specified).
++# The default value is: YES.
++
++SHOW_FILES = YES
++
++# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces
++# page. This will remove the Namespaces entry from the Quick Index and from the
++# Folder Tree View (if specified).
++# The default value is: YES.
++
++SHOW_NAMESPACES = YES
++
++# The FILE_VERSION_FILTER tag can be used to specify a program or script that
++# doxygen should invoke to get the current version for each file (typically from
++# the version control system). Doxygen will invoke the program by executing (via
++# popen()) the command command input-file, where command is the value of the
++# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided
++# by doxygen. Whatever the program writes to standard output is used as the file
++# version. For an example see the documentation.
++
++FILE_VERSION_FILTER =
++
++# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed
++# by doxygen. The layout file controls the global structure of the generated
++# output files in an output format independent way. To create the layout file
++# that represents doxygen's defaults, run doxygen with the -l option. You can
++# optionally specify a file name after the option, if omitted DoxygenLayout.xml
++# will be used as the name of the layout file.
++#
++# Note that if you run doxygen from a directory containing a file called
++# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE
++# tag is left empty.
++
++LAYOUT_FILE =
++
++# The CITE_BIB_FILES tag can be used to specify one or more bib files containing
++# the reference definitions. This must be a list of .bib files. The .bib
++# extension is automatically appended if omitted. This requires the bibtex tool
++# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info.
++# For LaTeX the style of the bibliography can be controlled using
++# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the
++# search path. See also \cite for info how to create references.
++
++CITE_BIB_FILES =
++
++#---------------------------------------------------------------------------
++# Configuration options related to warning and progress messages
++#---------------------------------------------------------------------------
++
++# The QUIET tag can be used to turn on/off the messages that are generated to
++# standard output by doxygen. If QUIET is set to YES this implies that the
++# messages are off.
++# The default value is: NO.
++
++QUIET = YES
++
++# The WARNINGS tag can be used to turn on/off the warning messages that are
++# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES
++# this implies that the warnings are on.
++#
++# Tip: Turn warnings on while writing the documentation.
++# The default value is: YES.
++
++WARNINGS = YES
++
++# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate
++# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag
++# will automatically be disabled.
++# The default value is: YES.
++
++WARN_IF_UNDOCUMENTED = YES
++
++# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
++# potential errors in the documentation, such as not documenting some parameters
++# in a documented function, or documenting parameters that don't exist or using
++# markup commands wrongly.
++# The default value is: YES.
++
++WARN_IF_DOC_ERROR = YES
++
++# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that
++# are documented, but have no documentation for their parameters or return
++# value. If set to NO, doxygen will only warn about wrong or incomplete
++# parameter documentation, but not about the absence of documentation. If
++# EXTRACT_ALL is set to YES then this flag will automatically be disabled.
++# The default value is: NO.
++
++WARN_NO_PARAMDOC = NO
++
++# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when
++# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS
++# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but
++# at the end of the doxygen process doxygen will return with a non-zero status.
++# Possible values are: NO, YES and FAIL_ON_WARNINGS.
++# The default value is: NO.
++
++WARN_AS_ERROR = NO
++
++# The WARN_FORMAT tag determines the format of the warning messages that doxygen
++# can produce. The string should contain the $file, $line, and $text tags, which
++# will be replaced by the file and line number from which the warning originated
++# and the warning text. Optionally the format may contain $version, which will
++# be replaced by the version of the file (if it could be obtained via
++# FILE_VERSION_FILTER)
++# The default value is: $file:$line: $text.
++
++WARN_FORMAT = "$file:$line: $text"
++
++# The WARN_LOGFILE tag can be used to specify a file to which warning and error
++# messages should be written. If left blank the output is written to standard
++# error (stderr).
++
++WARN_LOGFILE =
++
++#---------------------------------------------------------------------------
++# Configuration options related to the input files
++#---------------------------------------------------------------------------
++
++# The INPUT tag is used to specify the files and/or directories that contain
++# documented source files. You may enter file names like myfile.cpp or
++# directories like /usr/src/myproject. Separate the files or directories with
++# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
++# Note: If this tag is empty the current directory is searched.
++
++INPUT =
++
++# This tag can be used to specify the character encoding of the source files
++# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
++# libiconv (or the iconv built into libc) for the transcoding. See the libiconv
++# documentation (see:
++# https://www.gnu.org/software/libiconv/) for the list of possible encodings.
++# The default value is: UTF-8.
++
++INPUT_ENCODING = UTF-8
++
++# If the value of the INPUT tag contains directories, you can use the
++# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and
++# *.h) to filter out the source-files in the directories.
++#
++# Note that for custom extensions or not directly supported extensions you also
++# need to set EXTENSION_MAPPING for the extension otherwise the files are not
++# read by doxygen.
++#
++# Note the list of default checked file patterns might differ from the list of
++# default file extension mappings.
++#
++# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp,
++# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h,
++# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc,
++# *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C comment),
++# *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, *.vhdl,
++# *.ucf, *.qsf and *.ice.
++
++FILE_PATTERNS = *.c \
++ *.cc \
++ *.h \
++ *.hpp \
++ *.dox
++
++# The RECURSIVE tag can be used to specify whether or not subdirectories should
++# be searched for input files as well.
++# The default value is: NO.
++
++RECURSIVE = NO
++
++# The EXCLUDE tag can be used to specify files and/or directories that should be
++# excluded from the INPUT source files. This way you can easily exclude a
++# subdirectory from a directory tree whose root is specified with the INPUT tag.
++#
++# Note that relative paths are relative to the directory from which doxygen is
++# run.
++
++EXCLUDE =
++
++# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
++# directories that are symbolic links (a Unix file system feature) are excluded
++# from the input.
++# The default value is: NO.
++
++EXCLUDE_SYMLINKS = NO
++
++# If the value of the INPUT tag contains directories, you can use the
++# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude
++# certain files from those directories.
++#
++# Note that the wildcards are matched against the file with absolute path, so to
++# exclude all test directories for example use the pattern */test/*
++
++EXCLUDE_PATTERNS =
++
++# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
++# (namespaces, classes, functions, etc.) that should be excluded from the
++# output. The symbol name can be a fully qualified name, a word, or if the
++# wildcard * is used, a substring. Examples: ANamespace, AClass,
++# AClass::ANamespace, ANamespace::*Test
++#
++# Note that the wildcards are matched against the file with absolute path, so to
++# exclude all test directories use the pattern */test/*
++
++EXCLUDE_SYMBOLS =
++
++# The EXAMPLE_PATH tag can be used to specify one or more files or directories
++# that contain example code fragments that are included (see the \include
++# command).
++
++EXAMPLE_PATH =
++
++# If the value of the EXAMPLE_PATH tag contains directories, you can use the
++# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and
++# *.h) to filter out the source-files in the directories. If left blank all
++# files are included.
++
++EXAMPLE_PATTERNS =
++
++# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be
++# searched for input files to be used with the \include or \dontinclude commands
++# irrespective of the value of the RECURSIVE tag.
++# The default value is: NO.
++
++EXAMPLE_RECURSIVE = NO
++
++# The IMAGE_PATH tag can be used to specify one or more files or directories
++# that contain images that are to be included in the documentation (see the
++# \image command).
++
++IMAGE_PATH = ../../../../../doc/images
++
++# The INPUT_FILTER tag can be used to specify a program that doxygen should
++# invoke to filter for each input file. Doxygen will invoke the filter program
++# by executing (via popen()) the command:
++#
++# <filter> <input-file>
++#
++# where <filter> is the value of the INPUT_FILTER tag, and <input-file> is the
++# name of an input file. Doxygen will then use the output that the filter
++# program writes to standard output. If FILTER_PATTERNS is specified, this tag
++# will be ignored.
++#
++# Note that the filter must not add or remove lines; it is applied before the
++# code is scanned, but not when the output code is generated. If lines are added
++# or removed, the anchors will not be placed correctly.
++#
++# Note that for custom extensions or not directly supported extensions you also
++# need to set EXTENSION_MAPPING for the extension otherwise the files are not
++# properly processed by doxygen.
++
++INPUT_FILTER =
++
++# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern
++# basis. Doxygen will compare the file name with each pattern and apply the
++# filter if there is a match. The filters are a list of the form: pattern=filter
++# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how
++# filters are used. If the FILTER_PATTERNS tag is empty or if none of the
++# patterns match the file name, INPUT_FILTER is applied.
++#
++# Note that for custom extensions or not directly supported extensions you also
++# need to set EXTENSION_MAPPING for the extension otherwise the files are not
++# properly processed by doxygen.
++
++FILTER_PATTERNS =
++
++# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
++# INPUT_FILTER) will also be used to filter the input files that are used for
++# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES).
++# The default value is: NO.
++
++FILTER_SOURCE_FILES = NO
++
++# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file
++# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and
++# it is also possible to disable source filtering for a specific pattern using
++# *.ext= (so without naming a filter).
++# This tag requires that the tag FILTER_SOURCE_FILES is set to YES.
++
++FILTER_SOURCE_PATTERNS =
++
++# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that
++# is part of the input, its contents will be placed on the main page
++# (index.html). This can be useful if you have a project on for instance GitHub
++# and want to reuse the introduction page also for the doxygen output.
++
++USE_MDFILE_AS_MAINPAGE =
++
++#---------------------------------------------------------------------------
++# Configuration options related to source browsing
++#---------------------------------------------------------------------------
++
++# If the SOURCE_BROWSER tag is set to YES then a list of source files will be
++# generated. Documented entities will be cross-referenced with these sources.
++#
++# Note: To get rid of all source code in the generated output, make sure that
++# also VERBATIM_HEADERS is set to NO.
++# The default value is: NO.
++
++SOURCE_BROWSER = YES
++
++# Setting the INLINE_SOURCES tag to YES will include the body of functions,
++# classes and enums directly into the documentation.
++# The default value is: NO.
++
++INLINE_SOURCES = NO
++
++# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any
++# special comment blocks from generated source code fragments. Normal C, C++ and
++# Fortran comments will always remain visible.
++# The default value is: YES.
++
++STRIP_CODE_COMMENTS = YES
++
++# If the REFERENCED_BY_RELATION tag is set to YES then for each documented
++# entity all documented functions referencing it will be listed.
++# The default value is: NO.
++
++REFERENCED_BY_RELATION = YES
++
++# If the REFERENCES_RELATION tag is set to YES then for each documented function
++# all documented entities called/used by that function will be listed.
++# The default value is: NO.
++
++REFERENCES_RELATION = YES
++
++# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set
++# to YES then the hyperlinks from functions in REFERENCES_RELATION and
++# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will
++# link to the documentation.
++# The default value is: YES.
++
++REFERENCES_LINK_SOURCE = YES
++
++# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the
++# source code will show a tooltip with additional information such as prototype,
++# brief description and links to the definition and documentation. Since this
++# will make the HTML file larger and loading of large files a bit slower, you
++# can opt to disable this feature.
++# The default value is: YES.
++# This tag requires that the tag SOURCE_BROWSER is set to YES.
++
++SOURCE_TOOLTIPS = YES
++
++# If the USE_HTAGS tag is set to YES then the references to source code will
++# point to the HTML generated by the htags(1) tool instead of doxygen built-in
++# source browser. The htags tool is part of GNU's global source tagging system
++# (see https://www.gnu.org/software/global/global.html). You will need version
++# 4.8.6 or higher.
++#
++# To use it do the following:
++# - Install the latest version of global
++# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file
++# - Make sure the INPUT points to the root of the source tree
++# - Run doxygen as normal
++#
++# Doxygen will invoke htags (and that will in turn invoke gtags), so these
++# tools must be available from the command line (i.e. in the search path).
++#
++# The result: instead of the source browser generated by doxygen, the links to
++# source code will now point to the output of htags.
++# The default value is: NO.
++# This tag requires that the tag SOURCE_BROWSER is set to YES.
++
++USE_HTAGS = NO
++
++# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a
++# verbatim copy of the header file for each class for which an include is
++# specified. Set to NO to disable this.
++# See also: Section \class.
++# The default value is: YES.
++
++VERBATIM_HEADERS = YES
++
++#---------------------------------------------------------------------------
++# Configuration options related to the alphabetical class index
++#---------------------------------------------------------------------------
++
++# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all
++# compounds will be generated. Enable this if the project contains a lot of
++# classes, structs, unions or interfaces.
++# The default value is: YES.
++
++ALPHABETICAL_INDEX = YES
++
++# In case all classes in a project start with a common prefix, all classes will
++# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag
++# can be used to specify a prefix (or a list of prefixes) that should be ignored
++# while generating the index headers.
++# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
++
++IGNORE_PREFIX =
++
++#---------------------------------------------------------------------------
++# Configuration options related to the HTML output
++#---------------------------------------------------------------------------
++
++# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output
++# The default value is: YES.
++
++GENERATE_HTML = YES
++
++# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a
++# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
++# it.
++# The default directory is: html.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++HTML_OUTPUT = ../html
++
++# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each
++# generated HTML page (for example: .htm, .php, .asp).
++# The default value is: .html.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++HTML_FILE_EXTENSION = .html
++
++# The HTML_HEADER tag can be used to specify a user-defined HTML header file for
++# each generated HTML page. If the tag is left blank doxygen will generate a
++# standard header.
++#
++# To get valid HTML the header file that includes any scripts and style sheets
++# that doxygen needs, which is dependent on the configuration options used (e.g.
++# the setting GENERATE_TREEVIEW). It is highly recommended to start with a
++# default header using
++# doxygen -w html new_header.html new_footer.html new_stylesheet.css
++# YourConfigFile
++# and then modify the file new_header.html. See also section "Doxygen usage"
++# for information on how to generate the default header that doxygen normally
++# uses.
++# Note: The header is subject to change so you typically have to regenerate the
++# default header when upgrading to a newer version of doxygen. For a description
++# of the possible markers and block names see the documentation.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++HTML_HEADER =
++
++# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each
++# generated HTML page. If the tag is left blank doxygen will generate a standard
++# footer. See HTML_HEADER for more information on how to generate a default
++# footer and what special commands can be used inside the footer. See also
++# section "Doxygen usage" for information on how to generate the default footer
++# that doxygen normally uses.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++HTML_FOOTER =
++
++# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style
++# sheet that is used by each HTML page. It can be used to fine-tune the look of
++# the HTML output. If left blank doxygen will generate a default style sheet.
++# See also section "Doxygen usage" for information on how to generate the style
++# sheet that doxygen normally uses.
++# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as
++# it is more robust and this tag (HTML_STYLESHEET) will in the future become
++# obsolete.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++HTML_STYLESHEET =
++
++# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined
++# cascading style sheets that are included after the standard style sheets
++# created by doxygen. Using this option one can overrule certain style aspects.
++# This is preferred over using HTML_STYLESHEET since it does not replace the
++# standard style sheet and is therefore more robust against future updates.
++# Doxygen will copy the style sheet files to the output directory.
++# Note: The order of the extra style sheet files is of importance (e.g. the last
++# style sheet in the list overrules the setting of the previous ones in the
++# list). For an example see the documentation.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++HTML_EXTRA_STYLESHEET =
++
++# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
++# other source files which should be copied to the HTML output directory. Note
++# that these files will be copied to the base HTML output directory. Use the
++# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these
++# files. In the HTML_STYLESHEET file, use the file name only. Also note that the
++# files will be copied as-is; there are no commands or markers available.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++HTML_EXTRA_FILES =
++
++# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
++# will adjust the colors in the style sheet and background images according to
++# this color. Hue is specified as an angle on a colorwheel, see
++# https://en.wikipedia.org/wiki/Hue for more information. For instance the value
++# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300
++# purple, and 360 is red again.
++# Minimum value: 0, maximum value: 359, default value: 220.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++HTML_COLORSTYLE_HUE = 148
++
++# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors
++# in the HTML output. For a value of 0 the output will use grayscales only. A
++# value of 255 will produce the most vivid colors.
++# Minimum value: 0, maximum value: 255, default value: 100.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++HTML_COLORSTYLE_SAT = 93
++
++# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the
++# luminance component of the colors in the HTML output. Values below 100
++# gradually make the output lighter, whereas values above 100 make the output
++# darker. The value divided by 100 is the actual gamma applied, so 80 represents
++# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not
++# change the gamma.
++# Minimum value: 40, maximum value: 240, default value: 80.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++HTML_COLORSTYLE_GAMMA = 80
++
++# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
++# page will contain the date and time when the page was generated. Setting this
++# to YES can help to show when doxygen was last run and thus if the
++# documentation is up to date.
++# The default value is: NO.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++HTML_TIMESTAMP = YES
++
++# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML
++# documentation will contain a main index with vertical navigation menus that
++# are dynamically created via JavaScript. If disabled, the navigation index will
++# consists of multiple levels of tabs that are statically embedded in every HTML
++# page. Disable this option to support browsers that do not have JavaScript,
++# like the Qt help browser.
++# The default value is: YES.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++HTML_DYNAMIC_MENUS = YES
++
++# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML
++# documentation will contain sections that can be hidden and shown after the
++# page has loaded.
++# The default value is: NO.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++HTML_DYNAMIC_SECTIONS = YES
++
++# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries
++# shown in the various tree structured indices initially; the user can expand
++# and collapse entries dynamically later on. Doxygen will expand the tree to
++# such a level that at most the specified number of entries are visible (unless
++# a fully collapsed tree already exceeds this amount). So setting the number of
++# entries 1 will produce a full collapsed tree by default. 0 is a special value
++# representing an infinite number of entries and will result in a full expanded
++# tree by default.
++# Minimum value: 0, maximum value: 9999, default value: 100.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++HTML_INDEX_NUM_ENTRIES = 100
++
++# If the GENERATE_DOCSET tag is set to YES, additional index files will be
++# generated that can be used as input for Apple's Xcode 3 integrated development
++# environment (see:
++# https://developer.apple.com/xcode/), introduced with OSX 10.5 (Leopard). To
++# create a documentation set, doxygen will generate a Makefile in the HTML
++# output directory. Running make will produce the docset in that directory and
++# running make install will install the docset in
++# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at
++# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy
++# genXcode/_index.html for more information.
++# The default value is: NO.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++GENERATE_DOCSET = NO
++
++# This tag determines the name of the docset feed. A documentation feed provides
++# an umbrella under which multiple documentation sets from a single provider
++# (such as a company or product suite) can be grouped.
++# The default value is: Doxygen generated docs.
++# This tag requires that the tag GENERATE_DOCSET is set to YES.
++
++DOCSET_FEEDNAME = "Doxygen generated docs"
++
++# This tag specifies a string that should uniquely identify the documentation
++# set bundle. This should be a reverse domain-name style string, e.g.
++# com.mycompany.MyDocSet. Doxygen will append .docset to the name.
++# The default value is: org.doxygen.Project.
++# This tag requires that the tag GENERATE_DOCSET is set to YES.
++
++DOCSET_BUNDLE_ID = org.doxygen.Project
++
++# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify
++# the documentation publisher. This should be a reverse domain-name style
++# string, e.g. com.mycompany.MyDocSet.documentation.
++# The default value is: org.doxygen.Publisher.
++# This tag requires that the tag GENERATE_DOCSET is set to YES.
++
++DOCSET_PUBLISHER_ID = org.doxygen.Publisher
++
++# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher.
++# The default value is: Publisher.
++# This tag requires that the tag GENERATE_DOCSET is set to YES.
++
++DOCSET_PUBLISHER_NAME = Publisher
++
++# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three
++# additional HTML index files: index.hhp, index.hhc, and index.hhk. The
++# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop
++# (see:
++# https://www.microsoft.com/en-us/download/details.aspx?id=21138) on Windows.
++#
++# The HTML Help Workshop contains a compiler that can convert all HTML output
++# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML
++# files are now used as the Windows 98 help format, and will replace the old
++# Windows help format (.hlp) on all Windows platforms in the future. Compressed
++# HTML files also contain an index, a table of contents, and you can search for
++# words in the documentation. The HTML workshop also contains a viewer for
++# compressed HTML files.
++# The default value is: NO.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++GENERATE_HTMLHELP = NO
++
++# The CHM_FILE tag can be used to specify the file name of the resulting .chm
++# file. You can add a path in front of the file if the result should not be
++# written to the html output directory.
++# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
++
++CHM_FILE =
++
++# The HHC_LOCATION tag can be used to specify the location (absolute path
++# including file name) of the HTML help compiler (hhc.exe). If non-empty,
++# doxygen will try to run the HTML help compiler on the generated index.hhp.
++# The file has to be specified with full path.
++# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
++
++HHC_LOCATION =
++
++# The GENERATE_CHI flag controls if a separate .chi index file is generated
++# (YES) or that it should be included in the main .chm file (NO).
++# The default value is: NO.
++# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
++
++GENERATE_CHI = NO
++
++# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc)
++# and project file content.
++# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
++
++CHM_INDEX_ENCODING =
++
++# The BINARY_TOC flag controls whether a binary table of contents is generated
++# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it
++# enables the Previous and Next buttons.
++# The default value is: NO.
++# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
++
++BINARY_TOC = NO
++
++# The TOC_EXPAND flag can be set to YES to add extra items for group members to
++# the table of contents of the HTML help documentation and to the tree view.
++# The default value is: NO.
++# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
++
++TOC_EXPAND = NO
++
++# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
++# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that
++# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help
++# (.qch) of the generated HTML documentation.
++# The default value is: NO.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++GENERATE_QHP = NO
++
++# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify
++# the file name of the resulting .qch file. The path specified is relative to
++# the HTML output folder.
++# This tag requires that the tag GENERATE_QHP is set to YES.
++
++QCH_FILE =
++
++# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help
++# Project output. For more information please see Qt Help Project / Namespace
++# (see:
++# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace).
++# The default value is: org.doxygen.Project.
++# This tag requires that the tag GENERATE_QHP is set to YES.
++
++QHP_NAMESPACE =
++
++# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt
++# Help Project output. For more information please see Qt Help Project / Virtual
++# Folders (see:
++# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-folders).
++# The default value is: doc.
++# This tag requires that the tag GENERATE_QHP is set to YES.
++
++QHP_VIRTUAL_FOLDER = doc
++
++# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom
++# filter to add. For more information please see Qt Help Project / Custom
++# Filters (see:
++# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters).
++# This tag requires that the tag GENERATE_QHP is set to YES.
++
++QHP_CUST_FILTER_NAME =
++
++# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the
++# custom filter to add. For more information please see Qt Help Project / Custom
++# Filters (see:
++# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters).
++# This tag requires that the tag GENERATE_QHP is set to YES.
++
++QHP_CUST_FILTER_ATTRS =
++
++# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this
++# project's filter section matches. Qt Help Project / Filter Attributes (see:
++# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes).
++# This tag requires that the tag GENERATE_QHP is set to YES.
++
++QHP_SECT_FILTER_ATTRS =
++
++# The QHG_LOCATION tag can be used to specify the location (absolute path
++# including file name) of Qt's qhelpgenerator. If non-empty doxygen will try to
++# run qhelpgenerator on the generated .qhp file.
++# This tag requires that the tag GENERATE_QHP is set to YES.
++
++QHG_LOCATION =
++
++# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be
++# generated, together with the HTML files, they form an Eclipse help plugin. To
++# install this plugin and make it available under the help contents menu in
++# Eclipse, the contents of the directory containing the HTML and XML files needs
++# to be copied into the plugins directory of eclipse. The name of the directory
++# within the plugins directory should be the same as the ECLIPSE_DOC_ID value.
++# After copying Eclipse needs to be restarted before the help appears.
++# The default value is: NO.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++GENERATE_ECLIPSEHELP = NO
++
++# A unique identifier for the Eclipse help plugin. When installing the plugin
++# the directory name containing the HTML and XML files should also have this
++# name. Each documentation set should have its own identifier.
++# The default value is: org.doxygen.Project.
++# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES.
++
++ECLIPSE_DOC_ID = org.doxygen.Project
++
++# If you want full control over the layout of the generated HTML pages it might
++# be necessary to disable the index and replace it with your own. The
++# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top
++# of each HTML page. A value of NO enables the index and the value YES disables
++# it. Since the tabs in the index contain the same information as the navigation
++# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES.
++# The default value is: NO.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++DISABLE_INDEX = NO
++
++# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
++# structure should be generated to display hierarchical information. If the tag
++# value is set to YES, a side panel will be generated containing a tree-like
++# index structure (just like the one that is generated for HTML Help). For this
++# to work a browser that supports JavaScript, DHTML, CSS and frames is required
++# (i.e. any modern browser). Windows users are probably better off using the
++# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can
++# further fine-tune the look of the index. As an example, the default style
++# sheet generated by doxygen has an example that shows how to put an image at
++# the root of the tree instead of the PROJECT_NAME. Since the tree basically has
++# the same information as the tab index, you could consider setting
++# DISABLE_INDEX to YES when enabling this option.
++# The default value is: NO.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++GENERATE_TREEVIEW = YES
++
++# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that
++# doxygen will group on one line in the generated HTML documentation.
++#
++# Note that a value of 0 will completely suppress the enum values from appearing
++# in the overview section.
++# Minimum value: 0, maximum value: 20, default value: 4.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++ENUM_VALUES_PER_LINE = 4
++
++# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used
++# to set the initial width (in pixels) of the frame in which the tree is shown.
++# Minimum value: 0, maximum value: 1500, default value: 250.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++TREEVIEW_WIDTH = 180
++
++# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to
++# external symbols imported via tag files in a separate window.
++# The default value is: NO.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++EXT_LINKS_IN_WINDOW = NO
++
++# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg
++# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see
++# https://inkscape.org) to generate formulas as SVG images instead of PNGs for
++# the HTML output. These images will generally look nicer at scaled resolutions.
++# Possible values are: png (the default) and svg (looks nicer but requires the
++# pdf2svg or inkscape tool).
++# The default value is: png.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++HTML_FORMULA_FORMAT = png
++
++# Use this tag to change the font size of LaTeX formulas included as images in
++# the HTML documentation. When you change the font size after a successful
++# doxygen run you need to manually remove any form_*.png images from the HTML
++# output directory to force them to be regenerated.
++# Minimum value: 8, maximum value: 50, default value: 10.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++FORMULA_FONTSIZE = 10
++
++# Use the FORMULA_TRANSPARENT tag to determine whether or not the images
++# generated for formulas are transparent PNGs. Transparent PNGs are not
++# supported properly for IE 6.0, but are supported on all modern browsers.
++#
++# Note that when changing this option you need to delete any form_*.png files in
++# the HTML output directory before the changes have effect.
++# The default value is: YES.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++FORMULA_TRANSPARENT = YES
++
++# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands
++# to create new LaTeX commands to be used in formulas as building blocks. See
++# the section "Including formulas" for details.
++
++FORMULA_MACROFILE =
++
++# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see
++# https://www.mathjax.org) which uses client side JavaScript for the rendering
++# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX
++# installed or if you want to formulas look prettier in the HTML output. When
++# enabled you may also need to install MathJax separately and configure the path
++# to it using the MATHJAX_RELPATH option.
++# The default value is: NO.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++USE_MATHJAX = NO
++
++# When MathJax is enabled you can set the default output format to be used for
++# the MathJax output. See the MathJax site (see:
++# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details.
++# Possible values are: HTML-CSS (which is slower, but has the best
++# compatibility), NativeMML (i.e. MathML) and SVG.
++# The default value is: HTML-CSS.
++# This tag requires that the tag USE_MATHJAX is set to YES.
++
++MATHJAX_FORMAT = HTML-CSS
++
++# When MathJax is enabled you need to specify the location relative to the HTML
++# output directory using the MATHJAX_RELPATH option. The destination directory
++# should contain the MathJax.js script. For instance, if the mathjax directory
++# is located at the same level as the HTML output directory, then
++# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax
++# Content Delivery Network so you can quickly see the result without installing
++# MathJax. However, it is strongly recommended to install a local copy of
++# MathJax from https://www.mathjax.org before deployment.
++# The default value is: https://cdn.jsdelivr.net/npm/mathjax@2.
++# This tag requires that the tag USE_MATHJAX is set to YES.
++
++MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest
++
++# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
++# extension names that should be enabled during MathJax rendering. For example
++# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
++# This tag requires that the tag USE_MATHJAX is set to YES.
++
++MATHJAX_EXTENSIONS =
++
++# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces
++# of code that will be used on startup of the MathJax code. See the MathJax site
++# (see:
++# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an
++# example see the documentation.
++# This tag requires that the tag USE_MATHJAX is set to YES.
++
++MATHJAX_CODEFILE =
++
++# When the SEARCHENGINE tag is enabled doxygen will generate a search box for
++# the HTML output. The underlying search engine uses javascript and DHTML and
++# should work on any modern browser. Note that when using HTML help
++# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET)
++# there is already a search function so this one should typically be disabled.
++# For large projects the javascript based search engine can be slow, then
++# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to
++# search using the keyboard; to jump to the search box use <access key> + S
++# (what the <access key> is depends on the OS and browser, but it is typically
++# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down
++# key> to jump into the search results window, the results can be navigated
++# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel
++# the search. The filter options can be selected when the cursor is inside the
++# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys>
++# to select a filter and <Enter> or <escape> to activate or cancel the filter
++# option.
++# The default value is: YES.
++# This tag requires that the tag GENERATE_HTML is set to YES.
++
++SEARCHENGINE = NO
++
++# When the SERVER_BASED_SEARCH tag is enabled the search engine will be
++# implemented using a web server instead of a web client using JavaScript. There
++# are two flavors of web server based searching depending on the EXTERNAL_SEARCH
++# setting. When disabled, doxygen will generate a PHP script for searching and
++# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing
++# and searching needs to be provided by external tools. See the section
++# "External Indexing and Searching" for details.
++# The default value is: NO.
++# This tag requires that the tag SEARCHENGINE is set to YES.
++
++SERVER_BASED_SEARCH = NO
++
++# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP
++# script for searching. Instead the search results are written to an XML file
++# which needs to be processed by an external indexer. Doxygen will invoke an
++# external search engine pointed to by the SEARCHENGINE_URL option to obtain the
++# search results.
++#
++# Doxygen ships with an example indexer (doxyindexer) and search engine
++# (doxysearch.cgi) which are based on the open source search engine library
++# Xapian (see:
++# https://xapian.org/).
++#
++# See the section "External Indexing and Searching" for details.
++# The default value is: NO.
++# This tag requires that the tag SEARCHENGINE is set to YES.
++
++EXTERNAL_SEARCH = NO
++
++# The SEARCHENGINE_URL should point to a search engine hosted by a web server
++# which will return the search results when EXTERNAL_SEARCH is enabled.
++#
++# Doxygen ships with an example indexer (doxyindexer) and search engine
++# (doxysearch.cgi) which are based on the open source search engine library
++# Xapian (see:
++# https://xapian.org/). See the section "External Indexing and Searching" for
++# details.
++# This tag requires that the tag SEARCHENGINE is set to YES.
++
++SEARCHENGINE_URL =
++
++# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed
++# search data is written to a file for indexing by an external tool. With the
++# SEARCHDATA_FILE tag the name of this file can be specified.
++# The default file is: searchdata.xml.
++# This tag requires that the tag SEARCHENGINE is set to YES.
++
++SEARCHDATA_FILE = searchdata.xml
++
++# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the
++# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is
++# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple
++# projects and redirect the results back to the right project.
++# This tag requires that the tag SEARCHENGINE is set to YES.
++
++EXTERNAL_SEARCH_ID =
++
++# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen
++# projects other than the one defined by this configuration file, but that are
++# all added to the same external search index. Each project needs to have a
++# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of
++# to a relative location where the documentation can be found. The format is:
++# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ...
++# This tag requires that the tag SEARCHENGINE is set to YES.
++
++EXTRA_SEARCH_MAPPINGS =
++
++#---------------------------------------------------------------------------
++# Configuration options related to the LaTeX output
++#---------------------------------------------------------------------------
++
++# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output.
++# The default value is: YES.
++
++GENERATE_LATEX = NO
++
++# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a
++# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
++# it.
++# The default directory is: latex.
++# This tag requires that the tag GENERATE_LATEX is set to YES.
++
++LATEX_OUTPUT = latex
++
++# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be
++# invoked.
++#
++# Note that when not enabling USE_PDFLATEX the default is latex when enabling
++# USE_PDFLATEX the default is pdflatex and when in the later case latex is
++# chosen this is overwritten by pdflatex. For specific output languages the
++# default can have been set differently, this depends on the implementation of
++# the output language.
++# This tag requires that the tag GENERATE_LATEX is set to YES.
++
++LATEX_CMD_NAME = latex
++
++# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate
++# index for LaTeX.
++# Note: This tag is used in the Makefile / make.bat.
++# See also: LATEX_MAKEINDEX_CMD for the part in the generated output file
++# (.tex).
++# The default file is: makeindex.
++# This tag requires that the tag GENERATE_LATEX is set to YES.
++
++MAKEINDEX_CMD_NAME = makeindex
++
++# The LATEX_MAKEINDEX_CMD tag can be used to specify the command name to
++# generate index for LaTeX. In case there is no backslash (\) as first character
++# it will be automatically added in the LaTeX code.
++# Note: This tag is used in the generated output file (.tex).
++# See also: MAKEINDEX_CMD_NAME for the part in the Makefile / make.bat.
++# The default value is: makeindex.
++# This tag requires that the tag GENERATE_LATEX is set to YES.
++
++LATEX_MAKEINDEX_CMD = makeindex
++
++# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX
++# documents. This may be useful for small projects and may help to save some
++# trees in general.
++# The default value is: NO.
++# This tag requires that the tag GENERATE_LATEX is set to YES.
++
++COMPACT_LATEX = NO
++
++# The PAPER_TYPE tag can be used to set the paper type that is used by the
++# printer.
++# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x
++# 14 inches) and executive (7.25 x 10.5 inches).
++# The default value is: a4.
++# This tag requires that the tag GENERATE_LATEX is set to YES.
++
++PAPER_TYPE = a4
++
++# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names
++# that should be included in the LaTeX output. The package can be specified just
++# by its name or with the correct syntax as to be used with the LaTeX
++# \usepackage command. To get the times font for instance you can specify :
++# EXTRA_PACKAGES=times or EXTRA_PACKAGES={times}
++# To use the option intlimits with the amsmath package you can specify:
++# EXTRA_PACKAGES=[intlimits]{amsmath}
++# If left blank no extra packages will be included.
++# This tag requires that the tag GENERATE_LATEX is set to YES.
++
++EXTRA_PACKAGES =
++
++# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the
++# generated LaTeX document. The header should contain everything until the first
++# chapter. If it is left blank doxygen will generate a standard header. See
++# section "Doxygen usage" for information on how to let doxygen write the
++# default header to a separate file.
++#
++# Note: Only use a user-defined header if you know what you are doing! The
++# following commands have a special meaning inside the header: $title,
++# $datetime, $date, $doxygenversion, $projectname, $projectnumber,
++# $projectbrief, $projectlogo. Doxygen will replace $title with the empty
++# string, for the replacement values of the other commands the user is referred
++# to HTML_HEADER.
++# This tag requires that the tag GENERATE_LATEX is set to YES.
++
++LATEX_HEADER =
++
++# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the
++# generated LaTeX document. The footer should contain everything after the last
++# chapter. If it is left blank doxygen will generate a standard footer. See
++# LATEX_HEADER for more information on how to generate a default footer and what
++# special commands can be used inside the footer.
++#
++# Note: Only use a user-defined footer if you know what you are doing!
++# This tag requires that the tag GENERATE_LATEX is set to YES.
++
++LATEX_FOOTER =
++
++# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined
++# LaTeX style sheets that are included after the standard style sheets created
++# by doxygen. Using this option one can overrule certain style aspects. Doxygen
++# will copy the style sheet files to the output directory.
++# Note: The order of the extra style sheet files is of importance (e.g. the last
++# style sheet in the list overrules the setting of the previous ones in the
++# list).
++# This tag requires that the tag GENERATE_LATEX is set to YES.
++
++LATEX_EXTRA_STYLESHEET =
++
++# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or
++# other source files which should be copied to the LATEX_OUTPUT output
++# directory. Note that the files will be copied as-is; there are no commands or
++# markers available.
++# This tag requires that the tag GENERATE_LATEX is set to YES.
++
++LATEX_EXTRA_FILES =
++
++# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is
++# prepared for conversion to PDF (using ps2pdf or pdflatex). The PDF file will
++# contain links (just like the HTML output) instead of page references. This
++# makes the output suitable for online browsing using a PDF viewer.
++# The default value is: YES.
++# This tag requires that the tag GENERATE_LATEX is set to YES.
++
++PDF_HYPERLINKS = NO
++
++# If the USE_PDFLATEX tag is set to YES, doxygen will use the engine as
++# specified with LATEX_CMD_NAME to generate the PDF file directly from the LaTeX
++# files. Set this option to YES, to get a higher quality PDF documentation.
++#
++# See also section LATEX_CMD_NAME for selecting the engine.
++# The default value is: YES.
++# This tag requires that the tag GENERATE_LATEX is set to YES.
++
++USE_PDFLATEX = NO
++
++# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode
++# command to the generated LaTeX files. This will instruct LaTeX to keep running
++# if errors occur, instead of asking the user for help. This option is also used
++# when generating formulas in HTML.
++# The default value is: NO.
++# This tag requires that the tag GENERATE_LATEX is set to YES.
++
++LATEX_BATCHMODE = NO
++
++# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the
++# index chapters (such as File Index, Compound Index, etc.) in the output.
++# The default value is: NO.
++# This tag requires that the tag GENERATE_LATEX is set to YES.
++
++LATEX_HIDE_INDICES = NO
++
++# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source
++# code with syntax highlighting in the LaTeX output.
++#
++# Note that which sources are shown also depends on other settings such as
++# SOURCE_BROWSER.
++# The default value is: NO.
++# This tag requires that the tag GENERATE_LATEX is set to YES.
++
++LATEX_SOURCE_CODE = NO
++
++# The LATEX_BIB_STYLE tag can be used to specify the style to use for the
++# bibliography, e.g. plainnat, or ieeetr. See
++# https://en.wikipedia.org/wiki/BibTeX and \cite for more info.
++# The default value is: plain.
++# This tag requires that the tag GENERATE_LATEX is set to YES.
++
++LATEX_BIB_STYLE = plain
++
++# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated
++# page will contain the date and time when the page was generated. Setting this
++# to NO can help when comparing the output of multiple runs.
++# The default value is: NO.
++# This tag requires that the tag GENERATE_LATEX is set to YES.
++
++LATEX_TIMESTAMP = NO
++
++# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute)
++# path from which the emoji images will be read. If a relative path is entered,
++# it will be relative to the LATEX_OUTPUT directory. If left blank the
++# LATEX_OUTPUT directory will be used.
++# This tag requires that the tag GENERATE_LATEX is set to YES.
++
++LATEX_EMOJI_DIRECTORY =
++
++#---------------------------------------------------------------------------
++# Configuration options related to the RTF output
++#---------------------------------------------------------------------------
++
++# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The
++# RTF output is optimized for Word 97 and may not look too pretty with other RTF
++# readers/editors.
++# The default value is: NO.
++
++GENERATE_RTF = NO
++
++# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. If a
++# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
++# it.
++# The default directory is: rtf.
++# This tag requires that the tag GENERATE_RTF is set to YES.
++
++RTF_OUTPUT = rtf
++
++# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF
++# documents. This may be useful for small projects and may help to save some
++# trees in general.
++# The default value is: NO.
++# This tag requires that the tag GENERATE_RTF is set to YES.
++
++COMPACT_RTF = NO
++
++# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated will
++# contain hyperlink fields. The RTF file will contain links (just like the HTML
++# output) instead of page references. This makes the output suitable for online
++# browsing using Word or some other Word compatible readers that support those
++# fields.
++#
++# Note: WordPad (write) and others do not support links.
++# The default value is: NO.
++# This tag requires that the tag GENERATE_RTF is set to YES.
++
++RTF_HYPERLINKS = NO
++
++# Load stylesheet definitions from file. Syntax is similar to doxygen's
++# configuration file, i.e. a series of assignments. You only have to provide
++# replacements, missing definitions are set to their default value.
++#
++# See also section "Doxygen usage" for information on how to generate the
++# default style sheet that doxygen normally uses.
++# This tag requires that the tag GENERATE_RTF is set to YES.
++
++RTF_STYLESHEET_FILE =
++
++# Set optional variables used in the generation of an RTF document. Syntax is
++# similar to doxygen's configuration file. A template extensions file can be
++# generated using doxygen -e rtf extensionFile.
++# This tag requires that the tag GENERATE_RTF is set to YES.
++
++RTF_EXTENSIONS_FILE =
++
++# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code
++# with syntax highlighting in the RTF output.
++#
++# Note that which sources are shown also depends on other settings such as
++# SOURCE_BROWSER.
++# The default value is: NO.
++# This tag requires that the tag GENERATE_RTF is set to YES.
++
++RTF_SOURCE_CODE = NO
++
++#---------------------------------------------------------------------------
++# Configuration options related to the man page output
++#---------------------------------------------------------------------------
++
++# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for
++# classes and files.
++# The default value is: NO.
++
++GENERATE_MAN = NO
++
++# The MAN_OUTPUT tag is used to specify where the man pages will be put. If a
++# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
++# it. A directory man3 will be created inside the directory specified by
++# MAN_OUTPUT.
++# The default directory is: man.
++# This tag requires that the tag GENERATE_MAN is set to YES.
++
++MAN_OUTPUT = man
++
++# The MAN_EXTENSION tag determines the extension that is added to the generated
++# man pages. In case the manual section does not start with a number, the number
++# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is
++# optional.
++# The default value is: .3.
++# This tag requires that the tag GENERATE_MAN is set to YES.
++
++MAN_EXTENSION = .3
++
++# The MAN_SUBDIR tag determines the name of the directory created within
++# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by
++# MAN_EXTENSION with the initial . removed.
++# This tag requires that the tag GENERATE_MAN is set to YES.
++
++MAN_SUBDIR =
++
++# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it
++# will generate one additional man file for each entity documented in the real
++# man page(s). These additional files only source the real man page, but without
++# them the man command would be unable to find the correct page.
++# The default value is: NO.
++# This tag requires that the tag GENERATE_MAN is set to YES.
++
++MAN_LINKS = NO
++
++#---------------------------------------------------------------------------
++# Configuration options related to the XML output
++#---------------------------------------------------------------------------
++
++# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that
++# captures the structure of the code including all documentation.
++# The default value is: NO.
++
++GENERATE_XML = NO
++
++# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a
++# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
++# it.
++# The default directory is: xml.
++# This tag requires that the tag GENERATE_XML is set to YES.
++
++XML_OUTPUT = xml
++
++# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program
++# listings (including syntax highlighting and cross-referencing information) to
++# the XML output. Note that enabling this will significantly increase the size
++# of the XML output.
++# The default value is: YES.
++# This tag requires that the tag GENERATE_XML is set to YES.
++
++XML_PROGRAMLISTING = NO
++
++# If the XML_NS_MEMB_FILE_SCOPE tag is set to YES, doxygen will include
++# namespace members in file scope as well, matching the HTML output.
++# The default value is: NO.
++# This tag requires that the tag GENERATE_XML is set to YES.
++
++XML_NS_MEMB_FILE_SCOPE = NO
++
++#---------------------------------------------------------------------------
++# Configuration options related to the DOCBOOK output
++#---------------------------------------------------------------------------
++
++# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files
++# that can be used to generate PDF.
++# The default value is: NO.
++
++GENERATE_DOCBOOK = NO
++
++# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put.
++# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in
++# front of it.
++# The default directory is: docbook.
++# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
++
++DOCBOOK_OUTPUT = docbook
++
++# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the
++# program listings (including syntax highlighting and cross-referencing
++# information) to the DOCBOOK output. Note that enabling this will significantly
++# increase the size of the DOCBOOK output.
++# The default value is: NO.
++# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
++
++DOCBOOK_PROGRAMLISTING = NO
++
++#---------------------------------------------------------------------------
++# Configuration options for the AutoGen Definitions output
++#---------------------------------------------------------------------------
++
++# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an
++# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures
++# the structure of the code including all documentation. Note that this feature
++# is still experimental and incomplete at the moment.
++# The default value is: NO.
++
++GENERATE_AUTOGEN_DEF = NO
++
++#---------------------------------------------------------------------------
++# Configuration options related to the Perl module output
++#---------------------------------------------------------------------------
++
++# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module
++# file that captures the structure of the code including all documentation.
++#
++# Note that this feature is still experimental and incomplete at the moment.
++# The default value is: NO.
++
++GENERATE_PERLMOD = NO
++
++# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary
++# Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI
++# output from the Perl module output.
++# The default value is: NO.
++# This tag requires that the tag GENERATE_PERLMOD is set to YES.
++
++PERLMOD_LATEX = NO
++
++# If the PERLMOD_PRETTY tag is set to YES, the Perl module output will be nicely
++# formatted so it can be parsed by a human reader. This is useful if you want to
++# understand what is going on. On the other hand, if this tag is set to NO, the
++# size of the Perl module output will be much smaller and Perl will parse it
++# just the same.
++# The default value is: YES.
++# This tag requires that the tag GENERATE_PERLMOD is set to YES.
++
++PERLMOD_PRETTY = YES
++
++# The names of the make variables in the generated doxyrules.make file are
++# prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. This is useful
++# so different doxyrules.make files included by the same Makefile don't
++# overwrite each other's variables.
++# This tag requires that the tag GENERATE_PERLMOD is set to YES.
++
++PERLMOD_MAKEVAR_PREFIX =
++
++#---------------------------------------------------------------------------
++# Configuration options related to the preprocessor
++#---------------------------------------------------------------------------
++
++# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all
++# C-preprocessor directives found in the sources and include files.
++# The default value is: YES.
++
++ENABLE_PREPROCESSING = YES
++
++# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names
++# in the source code. If set to NO, only conditional compilation will be
++# performed. Macro expansion can be done in a controlled way by setting
++# EXPAND_ONLY_PREDEF to YES.
++# The default value is: NO.
++# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
++
++MACRO_EXPANSION = YES
++
++# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then
++# the macro expansion is limited to the macros specified with the PREDEFINED and
++# EXPAND_AS_DEFINED tags.
++# The default value is: NO.
++# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
++
++EXPAND_ONLY_PREDEF = NO
++
++# If the SEARCH_INCLUDES tag is set to YES, the include files in the
++# INCLUDE_PATH will be searched if a #include is found.
++# The default value is: YES.
++# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
++
++SEARCH_INCLUDES = YES
++
++# The INCLUDE_PATH tag can be used to specify one or more directories that
++# contain include files that are not input files but should be processed by the
++# preprocessor.
++# This tag requires that the tag SEARCH_INCLUDES is set to YES.
++
++INCLUDE_PATH =
++
++# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard
++# patterns (like *.h and *.hpp) to filter out the header-files in the
++# directories. If left blank, the patterns specified with FILE_PATTERNS will be
++# used.
++# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
++
++INCLUDE_FILE_PATTERNS =
++
++# The PREDEFINED tag can be used to specify one or more macro names that are
++# defined before the preprocessor is started (similar to the -D option of e.g.
++# gcc). The argument of the tag is a list of macros of the form: name or
++# name=definition (no spaces). If the definition and the "=" are omitted, "=1"
++# is assumed. To prevent a macro definition from being undefined via #undef or
++# recursively expanded use the := operator instead of the = operator.
++# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
++
++PREDEFINED =
++
++# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
++# tag can be used to specify a list of macro names that should be expanded. The
++# macro definition that is found in the sources will be used. Use the PREDEFINED
++# tag if you want to use a different macro definition that overrules the
++# definition found in the source code.
++# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
++
++EXPAND_AS_DEFINED =
++
++# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will
++# remove all references to function-like macros that are alone on a line, have
++# an all uppercase name, and do not end with a semicolon. Such function macros
++# are typically used for boiler-plate code, and will confuse the parser if not
++# removed.
++# The default value is: YES.
++# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
++
++SKIP_FUNCTION_MACROS = YES
++
++#---------------------------------------------------------------------------
++# Configuration options related to external references
++#---------------------------------------------------------------------------
++
++# The TAGFILES tag can be used to specify one or more tag files. For each tag
++# file the location of the external documentation should be added. The format of
++# a tag file without this location is as follows:
++# TAGFILES = file1 file2 ...
++# Adding location for the tag files is done as follows:
++# TAGFILES = file1=loc1 "file2 = loc2" ...
++# where loc1 and loc2 can be relative or absolute paths or URLs. See the
++# section "Linking to external documentation" for more information about the use
++# of tag files.
++# Note: Each tag file must have a unique name (where the name does NOT include
++# the path). If a tag file is not located in the directory in which doxygen is
++# run, you must also specify the path to the tagfile here.
++
++TAGFILES =
++
++# When a file name is specified after GENERATE_TAGFILE, doxygen will create a
++# tag file that is based on the input files it reads. See section "Linking to
++# external documentation" for more information about the usage of tag files.
++
++GENERATE_TAGFILE =
++
++# If the ALLEXTERNALS tag is set to YES, all external class will be listed in
++# the class index. If set to NO, only the inherited external classes will be
++# listed.
++# The default value is: NO.
++
++ALLEXTERNALS = NO
++
++# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed
++# in the modules index. If set to NO, only the current project's groups will be
++# listed.
++# The default value is: YES.
++
++EXTERNAL_GROUPS = YES
++
++# If the EXTERNAL_PAGES tag is set to YES, all external pages will be listed in
++# the related pages index. If set to NO, only the current project's pages will
++# be listed.
++# The default value is: YES.
++
++EXTERNAL_PAGES = YES
++
++#---------------------------------------------------------------------------
++# Configuration options related to the dot tool
++#---------------------------------------------------------------------------
++
++# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram
++# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to
++# NO turns the diagrams off. Note that this option also works with HAVE_DOT
++# disabled, but it is recommended to install and use dot, since it yields more
++# powerful graphs.
++# The default value is: YES.
++
++CLASS_DIAGRAMS = YES
++
++# You can include diagrams made with dia in doxygen documentation. Doxygen will
++# then run dia to produce the diagram and insert it in the documentation. The
++# DIA_PATH tag allows you to specify the directory where the dia binary resides.
++# If left empty dia is assumed to be found in the default search path.
++
++DIA_PATH =
++
++# If set to YES the inheritance and collaboration graphs will hide inheritance
++# and usage relations if the target is undocumented or is not a class.
++# The default value is: YES.
++
++HIDE_UNDOC_RELATIONS = YES
++
++# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
++# available from the path. This tool is part of Graphviz (see:
++# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
++# Bell Labs. The other options in this section have no effect if this option is
++# set to NO
++# The default value is: NO.
++
++HAVE_DOT = YES
++
++# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed
++# to run in parallel. When set to 0 doxygen will base this on the number of
++# processors available in the system. You can set it explicitly to a value
++# larger than 0 to get control over the balance between CPU load and processing
++# speed.
++# Minimum value: 0, maximum value: 32, default value: 0.
++# This tag requires that the tag HAVE_DOT is set to YES.
++
++DOT_NUM_THREADS = 0
++
++# When you want a differently looking font in the dot files that doxygen
++# generates you can specify the font name using DOT_FONTNAME. You need to make
++# sure dot is able to find the font, which can be done by putting it in a
++# standard location or by setting the DOTFONTPATH environment variable or by
++# setting DOT_FONTPATH to the directory containing the font.
++# The default value is: Helvetica.
++# This tag requires that the tag HAVE_DOT is set to YES.
++
++DOT_FONTNAME = Helvetica
++
++# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of
++# dot graphs.
++# Minimum value: 4, maximum value: 24, default value: 10.
++# This tag requires that the tag HAVE_DOT is set to YES.
++
++DOT_FONTSIZE = 10
++
++# By default doxygen will tell dot to use the default font as specified with
++# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set
++# the path where dot can find it using this tag.
++# This tag requires that the tag HAVE_DOT is set to YES.
++
++DOT_FONTPATH =
++
++# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for
++# each documented class showing the direct and indirect inheritance relations.
++# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO.
++# The default value is: YES.
++# This tag requires that the tag HAVE_DOT is set to YES.
++
++CLASS_GRAPH = YES
++
++# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a
++# graph for each documented class showing the direct and indirect implementation
++# dependencies (inheritance, containment, and class references variables) of the
++# class with other documented classes.
++# The default value is: YES.
++# This tag requires that the tag HAVE_DOT is set to YES.
++
++COLLABORATION_GRAPH = NO
++
++# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
++# groups, showing the direct groups dependencies.
++# The default value is: YES.
++# This tag requires that the tag HAVE_DOT is set to YES.
++
++GROUP_GRAPHS = YES
++
++# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and
++# collaboration diagrams in a style similar to the OMG's Unified Modeling
++# Language.
++# The default value is: NO.
++# This tag requires that the tag HAVE_DOT is set to YES.
++
++UML_LOOK = NO
++
++# If the UML_LOOK tag is enabled, the fields and methods are shown inside the
++# class node. If there are many fields or methods and many nodes the graph may
++# become too big to be useful. The UML_LIMIT_NUM_FIELDS threshold limits the
++# number of items for each type to make the size more manageable. Set this to 0
++# for no limit. Note that the threshold may be exceeded by 50% before the limit
++# is enforced. So when you set the threshold to 10, up to 15 fields may appear,
++# but if the number exceeds 15, the total amount of fields shown is limited to
++# 10.
++# Minimum value: 0, maximum value: 100, default value: 10.
++# This tag requires that the tag UML_LOOK is set to YES.
++
++UML_LIMIT_NUM_FIELDS = 10
++
++# If the DOT_UML_DETAILS tag is set to NO, doxygen will show attributes and
++# methods without types and arguments in the UML graphs. If the DOT_UML_DETAILS
++# tag is set to YES, doxygen will add type and arguments for attributes and
++# methods in the UML graphs. If the DOT_UML_DETAILS tag is set to NONE, doxygen
++# will not generate fields with class member information in the UML graphs. The
++# class diagrams will look similar to the default class diagrams but using UML
++# notation for the relationships.
++# Possible values are: NO, YES and NONE.
++# The default value is: NO.
++# This tag requires that the tag UML_LOOK is set to YES.
++
++DOT_UML_DETAILS = NO
++
++# The DOT_WRAP_THRESHOLD tag can be used to set the maximum number of characters
++# to display on a single line. If the actual line length exceeds this threshold
++# significantly it will wrapped across multiple lines. Some heuristics are apply
++# to avoid ugly line breaks.
++# Minimum value: 0, maximum value: 1000, default value: 17.
++# This tag requires that the tag HAVE_DOT is set to YES.
++
++DOT_WRAP_THRESHOLD = 17
++
++# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and
++# collaboration graphs will show the relations between templates and their
++# instances.
++# The default value is: NO.
++# This tag requires that the tag HAVE_DOT is set to YES.
++
++TEMPLATE_RELATIONS = NO
++
++# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to
++# YES then doxygen will generate a graph for each documented file showing the
++# direct and indirect include dependencies of the file with other documented
++# files.
++# The default value is: YES.
++# This tag requires that the tag HAVE_DOT is set to YES.
++
++INCLUDE_GRAPH = YES
++
++# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are
++# set to YES then doxygen will generate a graph for each documented file showing
++# the direct and indirect include dependencies of the file with other documented
++# files.
++# The default value is: YES.
++# This tag requires that the tag HAVE_DOT is set to YES.
++
++INCLUDED_BY_GRAPH = YES
++
++# If the CALL_GRAPH tag is set to YES then doxygen will generate a call
++# dependency graph for every global function or class method.
++#
++# Note that enabling this option will significantly increase the time of a run.
++# So in most cases it will be better to enable call graphs for selected
++# functions only using the \callgraph command. Disabling a call graph can be
++# accomplished by means of the command \hidecallgraph.
++# The default value is: NO.
++# This tag requires that the tag HAVE_DOT is set to YES.
++
++CALL_GRAPH = YES
++
++# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller
++# dependency graph for every global function or class method.
++#
++# Note that enabling this option will significantly increase the time of a run.
++# So in most cases it will be better to enable caller graphs for selected
++# functions only using the \callergraph command. Disabling a caller graph can be
++# accomplished by means of the command \hidecallergraph.
++# The default value is: NO.
++# This tag requires that the tag HAVE_DOT is set to YES.
++
++CALLER_GRAPH = NO
++
++# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical
++# hierarchy of all classes instead of a textual one.
++# The default value is: YES.
++# This tag requires that the tag HAVE_DOT is set to YES.
++
++GRAPHICAL_HIERARCHY = YES
++
++# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the
++# dependencies a directory has on other directories in a graphical way. The
++# dependency relations are determined by the #include relations between the
++# files in the directories.
++# The default value is: YES.
++# This tag requires that the tag HAVE_DOT is set to YES.
++
++DIRECTORY_GRAPH = YES
++
++# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
++# generated by dot. For an explanation of the image formats see the section
++# output formats in the documentation of the dot tool (Graphviz (see:
++# http://www.graphviz.org/)).
++# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order
++# to make the SVG files visible in IE 9+ (other browsers do not have this
++# requirement).
++# Possible values are: png, jpg, gif, svg, png:gd, png:gd:gd, png:cairo,
++# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and
++# png:gdiplus:gdiplus.
++# The default value is: png.
++# This tag requires that the tag HAVE_DOT is set to YES.
++
++DOT_IMAGE_FORMAT = png
++
++# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to
++# enable generation of interactive SVG images that allow zooming and panning.
++#
++# Note that this requires a modern browser other than Internet Explorer. Tested
++# and working are Firefox, Chrome, Safari, and Opera.
++# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make
++# the SVG files visible. Older versions of IE do not have SVG support.
++# The default value is: NO.
++# This tag requires that the tag HAVE_DOT is set to YES.
++
++INTERACTIVE_SVG = NO
++
++# The DOT_PATH tag can be used to specify the path where the dot tool can be
++# found. If left blank, it is assumed the dot tool can be found in the path.
++# This tag requires that the tag HAVE_DOT is set to YES.
++
++DOT_PATH =
++
++# The DOTFILE_DIRS tag can be used to specify one or more directories that
++# contain dot files that are included in the documentation (see the \dotfile
++# command).
++# This tag requires that the tag HAVE_DOT is set to YES.
++
++DOTFILE_DIRS =
++
++# The MSCFILE_DIRS tag can be used to specify one or more directories that
++# contain msc files that are included in the documentation (see the \mscfile
++# command).
++
++MSCFILE_DIRS =
++
++# The DIAFILE_DIRS tag can be used to specify one or more directories that
++# contain dia files that are included in the documentation (see the \diafile
++# command).
++
++DIAFILE_DIRS =
++
++# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the
++# path where java can find the plantuml.jar file. If left blank, it is assumed
++# PlantUML is not used or called during a preprocessing step. Doxygen will
++# generate a warning when it encounters a \startuml command in this case and
++# will not generate output for the diagram.
++
++PLANTUML_JAR_PATH =
++
++# When using plantuml, the PLANTUML_CFG_FILE tag can be used to specify a
++# configuration file for plantuml.
++
++PLANTUML_CFG_FILE =
++
++# When using plantuml, the specified paths are searched for files specified by
++# the !include statement in a plantuml block.
++
++PLANTUML_INCLUDE_PATH =
++
++# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes
++# that will be shown in the graph. If the number of nodes in a graph becomes
++# larger than this value, doxygen will truncate the graph, which is visualized
++# by representing a node as a red box. Note that doxygen if the number of direct
++# children of the root node in a graph is already larger than
++# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that
++# the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH.
++# Minimum value: 0, maximum value: 10000, default value: 50.
++# This tag requires that the tag HAVE_DOT is set to YES.
++
++DOT_GRAPH_MAX_NODES = 200
++
++# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs
++# generated by dot. A depth value of 3 means that only nodes reachable from the
++# root by following a path via at most 3 edges will be shown. Nodes that lay
++# further from the root node will be omitted. Note that setting this option to 1
++# or 2 may greatly reduce the computation time needed for large code bases. Also
++# note that the size of a graph can be further restricted by
++# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction.
++# Minimum value: 0, maximum value: 1000, default value: 0.
++# This tag requires that the tag HAVE_DOT is set to YES.
++
++MAX_DOT_GRAPH_DEPTH = 0
++
++# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent
++# background. This is disabled by default, because dot on Windows does not seem
++# to support this out of the box.
++#
++# Warning: Depending on the platform used, enabling this option may lead to
++# badly anti-aliased labels on the edges of a graph (i.e. they become hard to
++# read).
++# The default value is: NO.
++# This tag requires that the tag HAVE_DOT is set to YES.
++
++DOT_TRANSPARENT = NO
++
++# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
++# files in one run (i.e. multiple -o and -T options on the command line). This
++# makes dot run faster, but since only newer versions of dot (>1.8.10) support
++# this, this feature is disabled by default.
++# The default value is: NO.
++# This tag requires that the tag HAVE_DOT is set to YES.
++
++DOT_MULTI_TARGETS = NO
++
++# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
++# explaining the meaning of the various boxes and arrows in the dot generated
++# graphs.
++# The default value is: YES.
++# This tag requires that the tag HAVE_DOT is set to YES.
++
++GENERATE_LEGEND = YES
++
++# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate
++# files that are used to generate the various graphs.
++#
++# Note: This setting is not only used for dot files but also for msc and
++# plantuml temporary files.
++# The default value is: YES.
++
++DOT_CLEANUP = YES
+diff --git a/src/hooks/dhcp/ping_check/Makefile.am b/src/hooks/dhcp/ping_check/Makefile.am
+new file mode 100644
+index 0000000000..a7ea17f400
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/Makefile.am
+@@ -0,0 +1,104 @@
++SUBDIRS = . libloadtests tests
++
++AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
++AM_CPPFLAGS += $(BOOST_INCLUDES) $(CRYPTO_CFLAGS) $(CRYPTO_INCLUDES)
++AM_CXXFLAGS = $(KEA_CXXFLAGS)
++
++# Ensure that the message file and doxygen file is included in the distribution
++EXTRA_DIST = ping_check_messages.mes
++
++CLEANFILES = *.gcno *.gcda
++
++# convenience archive
++
++noinst_LTLIBRARIES = libping_check.la
++
++libping_check_la_SOURCES = ping_check_callouts.cc
++libping_check_la_SOURCES += ping_check_log.cc ping_check_log.h
++libping_check_la_SOURCES += ping_check_messages.cc ping_check_messages.h
++libping_check_la_SOURCES += icmp_endpoint.h icmp_socket.h
++libping_check_la_SOURCES += ping_context.cc ping_context.h
++libping_check_la_SOURCES += ping_context_store.cc ping_context_store.h
++libping_check_la_SOURCES += icmp_msg.h icmp_msg.cc
++libping_check_la_SOURCES += ping_channel.cc ping_channel.h
++libping_check_la_SOURCES += ping_check_mgr.cc ping_check_mgr.h
++libping_check_la_SOURCES += ping_check_config.cc ping_check_config.h
++libping_check_la_SOURCES += config_cache.cc config_cache.h
++libping_check_la_SOURCES += version.cc
++
++libping_check_la_CXXFLAGS = $(AM_CXXFLAGS)
++libping_check_la_CPPFLAGS = $(AM_CPPFLAGS)
++
++# install the shared object into $(libdir)/kea/hooks
++lib_hooksdir = $(libdir)/kea/hooks
++lib_hooks_LTLIBRARIES = libdhcp_ping_check.la
++
++libdhcp_ping_check_la_SOURCES =
++libdhcp_ping_check_la_LDFLAGS = $(AM_LDFLAGS)
++libdhcp_ping_check_la_LDFLAGS += -avoid-version -export-dynamic -module
++libdhcp_ping_check_la_LIBADD = libping_check.la
++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la
++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/process/libkea-process.la
++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/eval/libkea-eval.la
++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la
++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/stats/libkea-stats.la
++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/config/libkea-cfgclient.la
++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/http/libkea-http.la
++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la
++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la
++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/database/libkea-database.la
++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/cc/libkea-cc.la
++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/dns/libkea-dns++.la
++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la
++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/log/libkea-log.la
++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/util/libkea-util.la
++libdhcp_ping_check_la_LIBADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
++libdhcp_ping_check_la_LIBADD += $(LOG4CPLUS_LIBS)
++libdhcp_ping_check_la_LIBADD += $(CRYPTO_LIBS)
++libdhcp_ping_check_la_LIBADD += $(BOOST_LIBS)
++
++# Doxygen documentation
++EXTRA_DIST += ping_check.dox Doxyfile
++
++devel:
++ mkdir -p html
++ (cat Doxyfile; echo PROJECT_NUMBER=$(PACKAGE_VERSION)) | doxygen - > html/doxygen.log 2> html/doxygen-error.log
++ echo `grep -i ": warning:" html/doxygen-error.log | wc -l` warnings/errors detected.
++
++clean-local:
++ rm -rf html
++
++# If we want to get rid of all generated messages files, we need to use
++# make maintainer-clean. The proper way to introduce custom commands for
++# that operation is to define maintainer-clean-local target. However,
++# make maintainer-clean also removes Makefile, so running configure script
++# is required. To make it easy to rebuild messages without going through
++# reconfigure, a new target messages-clean has been added.
++maintainer-clean-local:
++ rm -f ping_check_messages.h ping_check_messages.cc
++
++# To regenerate messages files, one can do:
++#
++# make messages-clean
++# make messages
++#
++# This is needed only when a .mes file is modified.
++messages-clean: maintainer-clean-local
++
++if GENERATE_MESSAGES
++
++# Define rule to build logging source files from message file
++messages: ping_check_messages.h ping_check_messages.cc
++ @echo Message files regenerated
++
++ping_check_messages.h ping_check_messages.cc: ping_check_messages.mes
++ (cd $(top_srcdir); \
++ $(abs_top_builddir)/src/lib/log/compiler/kea-msg-compiler src/hooks/dhcp/ping_check/ping_check_messages.mes)
++
++else
++
++messages ping_check_messages.h ping_check_messages.cc:
++ @echo Messages generation disabled. Configure with --enable-generate-messages to enable it.
++
++endif
+diff --git a/src/hooks/dhcp/ping_check/config_cache.cc b/src/hooks/dhcp/ping_check/config_cache.cc
+new file mode 100644
+index 0000000000..9a8f9dd4bb
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/config_cache.cc
+@@ -0,0 +1,107 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++#include <config.h>
++
++#include <config_cache.h>
++#include <util/multi_threading_mgr.h>
++
++using namespace isc;
++using namespace isc::data;
++using namespace isc::dhcp;
++using namespace isc::util;
++using namespace std;
++
++namespace isc {
++namespace ping_check {
++
++PingCheckConfigPtr&
++ConfigCache::getGlobalConfig() {
++ return (global_config_);
++}
++
++void
++ConfigCache::setGlobalConfig(PingCheckConfigPtr& config) {
++ if (!config) {
++ isc_throw(BadValue, "ConfigCache - global config cannot be empty");
++ }
++
++ global_config_ = config;
++}
++
++bool
++ConfigCache::findConfig(const SubnetID& subnet_id, PingCheckConfigPtr& config) {
++ MultiThreadingLock lock(*mutex_);
++ return (findConfigInternal(subnet_id, config));
++}
++
++bool
++ConfigCache::findConfigInternal(const SubnetID& subnet_id, PingCheckConfigPtr& config) const {
++ auto it = configs_.find(subnet_id);
++ if (it != configs_.end()) {
++ config = it->second;
++ return (true);
++ }
++
++ config = PingCheckConfigPtr();
++ return (false);
++}
++
++PingCheckConfigPtr
++ConfigCache::parseAndCacheConfig(const SubnetID& subnet_id, ConstElementPtr& user_context) {
++ PingCheckConfigPtr config;
++ if (user_context) {
++ ConstElementPtr ping_check_params = user_context->get("ping-check");
++ if (ping_check_params) {
++ // Copy construct from global to start with.
++ config.reset(new PingCheckConfig(*getGlobalConfig()));
++
++ // Now parse in subnet-specific values. This may throw a DhcpConfigError but
++ // that's OK, dealt with by the caller.
++ try {
++ config->parse(ping_check_params);
++ } catch (...) {
++ throw;
++ }
++ }
++ }
++
++ // Cache the config. We allow empty configs so higher precedence scopes may
++ // override lower precedence scopes.
++ cacheConfig(subnet_id, config);
++ return (config);
++}
++
++void
++ConfigCache::cacheConfig(const SubnetID& subnet_id, PingCheckConfigPtr& config) {
++ MultiThreadingLock lock(*mutex_);
++ configs_[subnet_id] = config;
++}
++
++void
++ConfigCache::flush() {
++ MultiThreadingLock lock(*mutex_);
++ // Discard the contents.
++ configs_.clear();
++
++ // We use modification time to remember the last time we flushed.
++ updateModificationTime();
++}
++
++size_t
++ConfigCache::size() {
++ MultiThreadingLock lock(*mutex_);
++ return (configs_.size());
++}
++
++boost::posix_time::ptime
++ConfigCache::getLastFlushTime() {
++ MultiThreadingLock lock(*mutex_);
++ return (BaseStampedElement::getModificationTime());
++}
++
++} // end of namespace ping_check
++} // end of namespace isc
+diff --git a/src/hooks/dhcp/ping_check/config_cache.h b/src/hooks/dhcp/ping_check/config_cache.h
+new file mode 100644
+index 0000000000..b69cf6f124
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/config_cache.h
+@@ -0,0 +1,146 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++#ifndef CONFIG_CACHE_H
++#define CONFIG_CACHE_H
++
++#include <ping_check_config.h>
++#include <cc/base_stamped_element.h>
++#include <cc/data.h>
++#include <dhcpsrv/subnet.h>
++
++#include <map>
++#include <mutex>
++
++namespace isc {
++namespace ping_check {
++
++/// @brief ConfigCache stores ping check config per subnet
++///
++/// The intent is parse subnet ping-check parameters from its user-context
++/// as few times as possible rather than on every ping check request, while
++/// also allowing for run time updates via config back end or subnet cmds.
++///
++/// For every subnet we store:
++///
++/// -# subnet id
++/// -# PingCheckConfig pointer
++/// where:
++/// - empty config pointer means that subnet does not specify ping check config
++/// - non-empty means subnet specifies at least some ping check parameters
++///
++/// Each time we clear the cache we update the modification time.
++///
++/// When presented with a subnet:
++///
++/// 1. no cache entry:
++/// cache it
++///
++/// 2. entry exists:
++/// subnet mod time >= last flush
++/// cache is stale flush it
++/// cache it
++///
++/// subnet mod time < last flush
++/// use it
++///
++class ConfigCache : public data::BaseStampedElement {
++public:
++ /// @brief Constructor
++ ConfigCache() : configs_(), global_config_(new PingCheckConfig()), mutex_(new std::mutex) {
++ }
++
++ /// @brief Destructor
++ virtual ~ConfigCache() = default;
++
++ /// @brief Get the config for a given subnet.
++ ///
++ /// @param subnet_id ID of the subnet for which the config is desired.
++ /// @param[out] config a reference to a pointer in which to store the
++ /// config if found. If there is no entry for the subnet, it will be set
++ /// to an empty pointer.
++ ///
++ /// @return True if an entry for subnet was found, false otherwise. This
++ /// allows callers to distinguish between unknown subnets (entries that do
++ /// not exist) and subnets that are known but do not define a config.
++ bool findConfig(const dhcp::SubnetID& subnet_id,
++ PingCheckConfigPtr& config);
++
++ /// @brief Parses a config string and caches for the given subnet.
++ ///
++ /// @param subnet_id ID of the subnet for which the config is desired.
++ /// @param user_context user-context Element map of the subnet.
++ ///
++ /// @return pointer to the parsed config.
++ /// @throw BadValue if an error occurred during config parsing.
++ PingCheckConfigPtr parseAndCacheConfig(const dhcp::SubnetID& subnet_id,
++ data::ConstElementPtr& user_context);
++
++ /// @brief Adds (or replaces) the config for a given subnet to the cache.
++ ///
++ /// @param subnet_id ID of the subnet for which the config is desired.
++ /// @param config pointer to the config to store. This may be an
++ /// empty pointer.
++ void cacheConfig(const dhcp::SubnetID& subnet_id,
++ PingCheckConfigPtr& config);
++
++ /// @brief Discards the subnet entries in the cache.
++ void flush();
++
++ /// @brief Get the number of entries in the cache.
++ ///
++ /// @return number of entries in the cache.
++ size_t size();
++
++ /// @brief Get the last time the cache was flushed.
++ ///
++ /// @return the last time the cache was flushed (or the time it was
++ /// created if it has never been flushed).
++ boost::posix_time::ptime getLastFlushTime();
++
++ /// @brief Get the global level configuration.
++ ///
++ /// @return pointer to the global configuration.
++ PingCheckConfigPtr& getGlobalConfig();
++
++ /// @brief Set the global level configuration.
++ ///
++ /// @param config configuration to store as the global configuration.
++ void setGlobalConfig(PingCheckConfigPtr& config);
++
++private:
++ /// @brief Get the config for a given subnet.
++ ///
++ /// Must be called from with a thread-safe context.
++ ///
++ /// @param subnet_id ID of the subnet for which the config is desired.
++ /// @param[out] config a reference to a pointer in which to store the
++ /// config if found. If there is no entry for the subnet, it will be set
++ /// to an empty pointer.
++ ///
++ /// @return True if an entry for subnet was found, false otherwise. This
++ /// allows callers to distinguish between unknown subnets (entries that do
++ /// not exist) and subnets that are known but do not define a config.
++ bool findConfigInternal(const dhcp::SubnetID& subnet_id,
++ PingCheckConfigPtr& config) const;
++
++ /// @brief Per subnet config cache. Note that the global config in stored
++ /// using SUBNET_ID_GLOBAL.
++ std::map<dhcp::SubnetID, PingCheckConfigPtr> configs_;
++
++ /// @brief Stores the global configuration parameters.
++ PingCheckConfigPtr global_config_;
++
++ /// @brief The mutex used to protect internal state.
++ const boost::scoped_ptr<std::mutex> mutex_;
++};
++
++/// @brief Defines a shared pointer to a ConfigCache.
++typedef boost::shared_ptr<ConfigCache> ConfigCachePtr;
++
++} // end of namespace ping_check
++} // end of namespace isc
++#endif
+diff --git a/src/hooks/dhcp/ping_check/icmp_endpoint.h b/src/hooks/dhcp/ping_check/icmp_endpoint.h
+new file mode 100644
+index 0000000000..5d047d286f
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/icmp_endpoint.h
+@@ -0,0 +1,134 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++#ifndef ICMP_ENDPOINT_H
++#define ICMP_ENDPOINT_H 1
++
++#include <asiolink/io_endpoint.h>
++
++namespace isc {
++namespace ping_check {
++
++/// @brief The @c ICMPEndpoint class is a concrete derived class of
++/// @c IOEndpoint that represents an endpoint of a ICMP packet.
++///
++/// Other notes about @c TCPEndpoint applies to this class, too.
++class ICMPEndpoint : public asiolink::IOEndpoint {
++public:
++ ///
++ /// @name Constructors and Destructor.
++ ///
++ //@{
++
++ /// @brief Default Constructor
++ ///
++ /// Creates an internal endpoint. This is expected to be set by some
++ /// external call.
++ ICMPEndpoint() :
++ asio_endpoint_placeholder_(new boost::asio::ip::icmp::endpoint()),
++ asio_endpoint_(*asio_endpoint_placeholder_)
++ {}
++
++ /// @brief Constructor from an address.
++ ///
++ /// @param address The IP address of the endpoint.
++ explicit ICMPEndpoint(const asiolink::IOAddress& address) :
++ asio_endpoint_placeholder_(
++ new boost::asio::ip::icmp::endpoint(boost::asio::ip::make_address(address.toText()), 0)),
++ asio_endpoint_(*asio_endpoint_placeholder_)
++ {}
++
++ /// @brief Copy Constructor from an ASIO ICMP endpoint.
++ ///
++ /// This constructor is designed to be an efficient wrapper for the
++ /// corresponding ASIO class, @c icmp::endpoint.
++ ///
++ /// @param asio_endpoint The ASIO representation of the ICMP endpoint.
++ explicit ICMPEndpoint(boost::asio::ip::icmp::endpoint& asio_endpoint) :
++ asio_endpoint_placeholder_(0), asio_endpoint_(asio_endpoint)
++ {}
++
++ /// @brief Constructor from a const ASIO ICMP endpoint.
++ ///
++ /// This constructor is designed to be an efficient wrapper for the
++ /// corresponding ASIO class, @c icmp::endpoint.
++ ///
++ /// @param asio_endpoint The ASIO representation of the TCP endpoint.
++ explicit ICMPEndpoint(const boost::asio::ip::icmp::endpoint& asio_endpoint) :
++ asio_endpoint_placeholder_(new boost::asio::ip::icmp::endpoint(asio_endpoint)),
++ asio_endpoint_(*asio_endpoint_placeholder_)
++ {}
++
++ /// @brief The destructor.
++ virtual ~ICMPEndpoint() { delete asio_endpoint_placeholder_; }
++ //@}
++
++ /// @brief Fetches the IP address of the endpoint.
++ ///
++ /// @return the endpoint's IP address as an IOAddress.
++ virtual asiolink::IOAddress getAddress() const {
++ return (asio_endpoint_.address());
++ }
++
++ /// @brief Fetches the IP address of the endpoint in native form.
++ ///
++ /// @return the endpoint's IP address as a struct sockaddr.
++ virtual const struct sockaddr& getSockAddr() const {
++ return (*asio_endpoint_.data());
++ }
++
++ /// @brief Fetches the IP port number of the endpoint.
++ ///
++ /// @return the endpoint's port number as a unit16_t.
++ virtual uint16_t getPort() const {
++ return (asio_endpoint_.port());
++ }
++
++ /// @brief Fetches the network protocol of the endpoint.
++ ///
++ /// @return the endpoint's protocol as a short
++ virtual short getProtocol() const {
++ return (asio_endpoint_.protocol().protocol());
++ }
++
++ /// @brief Fetches the network protocol family of the endpoint.
++ ///
++ /// @return the endpoint's protocol as a short
++ virtual short getFamily() const {
++ return (asio_endpoint_.protocol().family());
++ }
++
++ /// @brief Fetches the underlying ASIO endpoint implementation
++ ///
++ /// This is not part of the exposed IOEndpoint API but allows
++ /// direct access to the ASIO implementation of the endpoint
++ ///
++ /// @return the wrapped ASIO endpoint instance as a const
++ inline const boost::asio::ip::icmp::endpoint& getASIOEndpoint() const {
++ return (asio_endpoint_);
++ }
++
++ /// @brief Fetches the underlying ASIO endpoint implementation
++ ///
++ /// This is not part of the exposed IOEndpoint API but allows
++ /// direct access to the ASIO implementation of the endpoint
++ ///
++ /// @return the wrapped ASIO endpoint instance as a non-const
++ inline boost::asio::ip::icmp::endpoint& getASIOEndpoint() {
++ return (asio_endpoint_);
++ }
++
++private:
++ /// @brief Pointer to the ASIO endpoint placeholder.
++ boost::asio::ip::icmp::endpoint* asio_endpoint_placeholder_;
++
++ /// @brief Reference to the underlying ASIO endpoint instance.
++ boost::asio::ip::icmp::endpoint& asio_endpoint_;
++};
++
++} // namespace ping_check
++} // namespace isc
++#endif // ICMP_ENDPOINT_H
+diff --git a/src/hooks/dhcp/ping_check/icmp_msg.cc b/src/hooks/dhcp/ping_check/icmp_msg.cc
+new file mode 100644
+index 0000000000..3d236820da
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/icmp_msg.cc
+@@ -0,0 +1,112 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++#include <config.h>
++#include <icmp_msg.h>
++#include <util/io.h>
++#include <exceptions/exceptions.h>
++
++#include <netinet/ip_icmp.h>
++#include <iostream>
++
++using namespace isc;
++using namespace isc::asiolink;
++using namespace isc::util;
++
++namespace isc {
++namespace ping_check {
++
++ICMPMsg::ICMPMsg()
++ : source_(IOAddress::IPV4_ZERO_ADDRESS()),
++ destination_(IOAddress::IPV4_ZERO_ADDRESS()),
++ msg_type_(0), code_(0), check_sum_(0), id_(0), sequence_(0),
++ payload_(0) {
++}
++
++ICMPMsgPtr
++ICMPMsg::unpack(const uint8_t* wire_data, size_t length) {
++ ICMPMsgPtr msg(new ICMPMsg());
++ if (length < sizeof(struct ip)) {
++ isc_throw(BadValue,
++ "ICMPMsg::unpack - truncated ip header, length: "
++ << length);
++ }
++
++ // Find the IP header length...
++ struct ip* ip_header = (struct ip*)(wire_data);
++ auto hlen = (ip_header->ip_hl << 2);
++
++ // Make sure we received enough data.
++ if (length < (hlen + sizeof(struct icmp))) {
++ isc_throw(BadValue, "ICMPMsg::truncated packet? length: "
++ << length << ", hlen: " << hlen);
++ }
++
++ // Grab the source and destination addresses.
++ msg->setSource(IOAddress(ntohl(ip_header->ip_src.s_addr)));
++ msg->setDestination(IOAddress(ntohl(ip_header->ip_dst.s_addr)));
++
++ // Get the message type.
++ struct icmp* reply = (struct icmp*)(wire_data + hlen);
++ msg->setType(reply->icmp_type);
++ msg->setCode(reply->icmp_code);
++
++ msg->setChecksum(ntohs(reply->icmp_cksum));
++ msg->setId(ntohs(reply->icmp_hun.ih_idseq.icd_id));
++ msg->setSequence(ntohs(reply->icmp_hun.ih_idseq.icd_seq));
++
++ auto payload_len = length - hlen - ICMP_HEADER_SIZE;
++ msg->setPayload((const uint8_t*)(&reply->icmp_dun), payload_len);
++
++ return (msg);
++}
++
++ICMPPtr
++ICMPMsg::pack() const {
++ ICMPPtr outbound(new struct icmp());
++ memset(outbound.get(), 0x00, sizeof(struct icmp));
++ outbound->icmp_type = msg_type_;
++ outbound->icmp_id = htons(id_);
++ outbound->icmp_seq = htons(sequence_);
++ /// @todo copy in payload - not needed for ECHO REQUEST
++ outbound->icmp_cksum = htons(~calcChecksum((const uint8_t*)(outbound.get()), sizeof(struct icmp)));
++ return (outbound);
++}
++
++void
++ICMPMsg::setPayload(const uint8_t* data, size_t length) {
++ payload_.insert(payload_.end(), data, data + length);
++}
++
++uint32_t
++ICMPMsg::calcChecksum(const uint8_t* buf, size_t length) {
++ uint32_t sum = 0;
++
++ /* Checksum all the pairs of bytes first... */
++ size_t i;
++ for (i = 0; i < (length & ~1U); i += 2) {
++ sum += static_cast<uint32_t>(readUint16(buf + i, sizeof(uint16_t)));
++ /* Add carry. */
++ if (sum > 0xFFFF) {
++ sum -= 0xFFFF;
++ }
++ }
++
++ /* If there's a single byte left over, checksum it, too. Network
++ byte order is big-endian, so the remaining byte is the high byte. */
++ if (i < length) {
++ sum += buf[i] << 8;
++ /* Add carry. */
++ if (sum > 0xFFFF) {
++ sum -= 0xFFFF;
++ }
++ }
++
++ return (sum);
++}
++
++} // end of namespace ping_check
++} // end of namespace isc
+diff --git a/src/hooks/dhcp/ping_check/icmp_msg.h b/src/hooks/dhcp/ping_check/icmp_msg.h
+new file mode 100644
+index 0000000000..ace322d1ca
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/icmp_msg.h
+@@ -0,0 +1,223 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++#ifndef ICMP_MSG_H
++#define ICMP_MSG_H
++
++#include <asiolink/io_address.h>
++
++#include <arpa/inet.h>
++#include <netinet/in.h>
++#include <netinet/ip.h>
++#include <unistd.h>
++#include <netinet/ip_icmp.h>
++#include <boost/shared_ptr.hpp>
++
++namespace isc {
++namespace ping_check {
++
++// Forward class definition.
++class ICMPMsg;
++
++/// @brief Shared pointer type for ICMPMsg.
++typedef boost::shared_ptr<ICMPMsg> ICMPMsgPtr;
++
++/// @brief Shared pointer type for struct icmp.
++typedef boost::shared_ptr<struct icmp> ICMPPtr;
++
++/// @brief Embodies an ICMP message
++///
++/// Provides functions for marshalling of ICMP protocol
++/// messages to and from wire form
++class ICMPMsg {
++public:
++ /// @brief ICMP message types. We only define the ones
++ /// we care about.
++ enum ICMPMsgType {
++ ECHO_REPLY = 0,
++ TARGET_UNREACHABLE = 3,
++ ECHO_REQUEST = 8
++ };
++
++ /// @brief Size in octets of ICMP message header.
++ /// 1 (msg type) + 1 (code) + 2 (checksum) + 4 (either unused
++ /// or used differently basing on the ICMP type and code e.g
++ /// Identifier and Sequence Number for Echo or Echo Reply Message)
++ constexpr static size_t ICMP_HEADER_SIZE = 8;
++
++ /// @brief Constructor.
++ ICMPMsg();
++
++ /// @brief Destructor.
++ virtual ~ICMPMsg() = default;
++
++ /// @brief Unpacks an ICMP message from the given wire_data
++ ///
++ /// The wire data is expected to include the IP header followed
++ /// by an ICMP message.
++ ///
++ /// @param wire_data raw data received from the socket
++ /// @param length number of bytes in the wire_data contents
++ ///
++ /// @return Pointer to the newly constructed message
++ /// @throw BadValue if the wire data is invalid
++ static ICMPMsgPtr unpack(const uint8_t* wire_data, size_t length);
++
++ /// @brief Packs the message into an ICMP structure.
++ ///
++ /// @return Pointer to the newly constructed ICMP structure.
++ ICMPPtr pack() const;
++
++ /// @brief Fetches the ICMP message type (e.g. ECHO_REQUEST, ECHO_REPLY)
++ ///
++ /// @return message type as a uint8_t
++ uint8_t getType() const {
++ return (msg_type_);
++ }
++
++ /// @brief Sets the ICMP message type
++ ///
++ /// @param msg_type new value for the message type
++ void setType(uint8_t msg_type) {
++ msg_type_ = msg_type;
++ }
++
++ /// @brief Fetches the ICMP message code
++ ///
++ /// @return uint8_t containing the message code
++ uint8_t getCode() const {
++ return (code_);
++ }
++
++ /// @brief Sets the ICMP code
++ ///
++ /// @param code new value for the message type
++ void setCode(uint8_t code) {
++ code_ = code;
++ }
++
++ /// @brief Fetches the checksum
++ ///
++ /// @return uint16_t containing the message checksum
++ uint16_t getChecksum() const {
++ return (check_sum_);
++ }
++
++ /// @brief Sets the check sum
++ ///
++ /// @param check_sum new value for the check sum
++ void setChecksum(uint16_t check_sum) {
++ check_sum_ = check_sum;
++ }
++
++ /// @brief Fetches the message id
++ ///
++ /// @return uint16_t containing the id
++ uint16_t getId() const {
++ return (id_);
++ }
++
++ /// @brief Sets the message id
++ ///
++ /// @param id new value for the message id
++ void setId(const uint16_t id) {
++ id_ = id;
++ }
++
++ /// @brief Fetches the message sequence number
++ ///
++ /// @return uint16_t containing the sequence number
++ uint16_t getSequence() const {
++ return (sequence_);
++ }
++
++ /// @brief Sets the message sequence number
++ ///
++ /// @param sequence new value for the message sequence number
++ void setSequence(uint16_t sequence) {
++ sequence_ = sequence;
++ }
++
++ /// @brief Fetches the source IP address
++ ///
++ /// @return IOAddress containing the IP address of the message source
++ const isc::asiolink::IOAddress& getSource() const {
++ return (source_);
++ }
++
++ /// @brief Sets the source IP address
++ ///
++ /// @param source new value for the source IP address
++ void setSource(const isc::asiolink::IOAddress& source) {
++ source_ = source;
++ }
++
++ /// @brief Fetches the destination IP address
++ ///
++ /// @return IOAddress containing the IP address of the message destination
++ const isc::asiolink::IOAddress& getDestination() const {
++ return (destination_);
++ }
++
++ /// @brief Sets the destination IP address
++ ///
++ /// @param destination new value for the destination IP address
++ void setDestination(const isc::asiolink::IOAddress& destination) {
++ destination_ = destination;
++ }
++
++ /// @brief Fetches the message payload
++ ///
++ /// @return vector containing the message payload
++ const std::vector<uint8_t>& getPayload() const {
++ return (payload_);
++ }
++
++ /// @brief Sets the message payload to the given data
++ ///
++ /// @param data pointer to data buffer from which to copy
++ /// @param length number of bytes in data buffer
++ void setPayload(const uint8_t* data, size_t length);
++
++ /// @brief Calculates the checksum of the given data buffer
++ ///
++ /// @param data pointer to data buffer from which to copy
++ /// @param length number of bytes in data buffer
++ ///
++ /// @return uint32_t containing the calculated checksum
++ static uint32_t calcChecksum(const uint8_t* data, size_t length);
++
++private:
++ /// @brief IP address from which the message origin
++ isc::asiolink::IOAddress source_;
++
++ /// @brief IP address of the message destination
++ isc::asiolink::IOAddress destination_;
++
++ /// @brief ICMP message type
++ uint8_t msg_type_;
++
++ /// @brief ICMP message code
++ uint8_t code_;
++
++ /// @brief Checksum of the message
++ uint16_t check_sum_;
++
++ /// @brief Message ID
++ uint16_t id_;
++
++ /// @brief Message sequence number
++ uint16_t sequence_;
++
++ // data beyond the ICMP header
++ std::vector<uint8_t> payload_;
++};
++
++
++} // end of namespace ping_check
++} // end of namespace isc
++
++#endif
+diff --git a/src/hooks/dhcp/ping_check/icmp_socket.h b/src/hooks/dhcp/ping_check/icmp_socket.h
+new file mode 100644
+index 0000000000..091057d749
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/icmp_socket.h
+@@ -0,0 +1,359 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++#ifndef ICMP_SOCKET_H
++#define ICMP_SOCKET_H 1
++
++#include <netinet/in.h>
++#include <sys/socket.h>
++#include <unistd.h>
++
++#include <cstddef>
++
++#include <asiolink/io_asio_socket.h>
++#include <asiolink/io_service.h>
++#include <icmp_endpoint.h>
++
++#include <exceptions/isc_assert.h>
++
++namespace isc {
++namespace ping_check {
++
++/// @brief The @c ICMPSocket class is a concrete derived class of @c IOAsioSocket
++/// that represents a ICMP socket.
++///
++/// @param C Callback type
++template <typename C>
++class ICMPSocket : public asiolink::IOAsioSocket<C> {
++private:
++ /// @brief Class is non-copyable
++ explicit ICMPSocket(const ICMPSocket&);
++ ICMPSocket& operator=(const ICMPSocket&);
++
++public:
++ enum {
++ MIN_SIZE = 4096 // Minimum send and receive size
++ };
++
++ /// @brief Constructor from an ASIO ICMP socket.
++ ///
++ /// @param socket The ASIO representation of the ICMP socket. It is assumed
++ /// that the caller will open and close the socket, so these
++ /// operations are a no-op for that socket.
++ explicit ICMPSocket(boost::asio::ip::icmp::socket& socket);
++
++ /// @brief Constructor
++ ///
++ /// Used when the ICMPSocket is being asked to manage its own internal
++ /// socket. In this case, the open() and close() methods are used.
++ ///
++ /// @param service I/O Service object used to manage the socket.
++ explicit ICMPSocket(const asiolink::IOServicePtr& service);
++
++ /// @brief Destructor
++ virtual ~ICMPSocket();
++
++ /// @brief Return file descriptor of underlying socket
++ ///
++ /// @return socket's native file descriptor as an int.
++ virtual int getNative() const {
++#if BOOST_VERSION < 106600
++ return (socket_.native());
++#else
++ return (socket_.native_handle());
++#endif
++ }
++
++ /// @brief Return protocol of socket
++ ///
++ /// @return Always IPPROTO_ICMP.
++ virtual int getProtocol() const {
++ return (IPPROTO_ICMP);
++ }
++
++ /// @brief Is "open()" synchronous?
++ ///
++ /// Indicates that the opening of a ICMP socket is synchronous.
++ /// @return Always true.
++ virtual bool isOpenSynchronous() const {
++ return true;
++ }
++
++ /// @brief Indicates if the socket is currently open.
++ ///
++ /// @return true if socket is open.
++ virtual bool isOpen() const {
++ return isopen_;
++ }
++
++ /// @brief Open Socket
++ ///
++ /// Opens the ICMP socket. This is a synchronous operation.
++ ///
++ /// @param endpoint Endpoint to which the socket will send data. This is
++ /// used to determine the address family that should be used for the
++ /// underlying socket.
++ /// @param callback Unused as the operation is synchronous.
++ virtual void open(const asiolink::IOEndpoint* endpoint, C& callback);
++
++ /// @brief Send Asynchronously
++ ///
++ /// Calls the underlying socket's async_send_to() method to send a packet of
++ /// data asynchronously to the remote endpoint. The callback will be called
++ /// on completion.
++ ///
++ /// @param data Data to send
++ /// @param length Length of data to send
++ /// @param endpoint Target of the send
++ /// @param callback Callback object.
++ virtual void asyncSend(const void* data, size_t length,
++ const asiolink::IOEndpoint* endpoint, C& callback);
++
++ /// @brief Receive Asynchronously
++ ///
++ /// Calls the underlying socket's async_receive_from() method to read a
++ /// packet of data from a remote endpoint. Arrival of the data is signalled
++ /// via a call to the callback function.
++ ///
++ /// @param data Buffer to receive incoming message
++ /// @param length Length of the data buffer
++ /// @param offset Offset into buffer where data is to be put
++ /// @param endpoint Source of the communication
++ /// @param callback Callback object
++ virtual void asyncReceive(void* data, size_t length, size_t offset,
++ asiolink::IOEndpoint* endpoint, C& callback);
++
++ /// @brief Process received data
++ ///
++ /// See the description of IOAsioSocket::receiveComplete for a complete
++ /// description of this method.
++ ///
++ /// @param staging Pointer to the start of the staging buffer.
++ /// @param length Amount of data in the staging buffer.
++ /// @param cumulative Amount of data received before the staging buffer is
++ /// processed.
++ /// @param offset Unused.
++ /// @param expected unused.
++ /// @param outbuff Output buffer. Data in the staging buffer is be copied
++ /// to this output buffer in the call.
++ ///
++ /// @return Always true
++ virtual bool processReceivedData(const void* staging, size_t length,
++ size_t& cumulative, size_t& offset,
++ size_t& expected,
++ isc::util::OutputBufferPtr& outbuff);
++
++ /// @brief Cancel I/O On Socket
++ virtual void cancel();
++
++ /// @brief Close socket
++ virtual void close();
++
++ /// @brief Calculates the checksum for the given buffer of data.
++ ///
++ /// @param buf pointer to the data buffer.
++ /// @param buf_size number of bytes in the data buffer.
++ ///
++ /// @return calculated checksum of the data as a uint16_t.
++ static uint16_t calcChecksum(const uint8_t* buf, const uint32_t buf_size);
++
++private:
++ /// @brief The IO service used to handle events.
++ isc::asiolink::IOServicePtr io_service_;
++
++ // Two variables to hold the socket - a socket and a pointer to it. This
++ // handles the case where a socket is passed to the ICMPSocket on
++ // construction, or where it is asked to manage its own socket.
++
++ /// Pointer to own socket
++ std::unique_ptr<boost::asio::ip::icmp::socket> socket_ptr_;
++
++ // Socket
++ boost::asio::ip::icmp::socket& socket_;
++
++ // True when socket is open
++ bool isopen_;
++};
++
++// Constructor - caller manages socket
++
++template <typename C>
++ICMPSocket<C>::ICMPSocket(boost::asio::ip::icmp::socket& socket) :
++ socket_ptr_(), socket_(socket), isopen_(true) {
++}
++
++// Constructor - create socket on the fly
++
++template <typename C>
++ICMPSocket<C>::ICMPSocket(const asiolink::IOServicePtr& io_service) :
++ io_service_(io_service),
++ socket_ptr_(new boost::asio::ip::icmp::socket(io_service_->getInternalIOService())),
++ socket_(*socket_ptr_), isopen_(false) {
++}
++
++// Destructor.
++
++template <typename C>
++ICMPSocket<C>::~ICMPSocket() {
++}
++
++// Open the socket.
++
++template <typename C> void
++ICMPSocket<C>::open(const asiolink::IOEndpoint* endpoint, C&) {
++
++ // Ignore opens on already-open socket. (Don't throw a failure because
++ // of uncertainties as to what precedes when using asynchronous I/O.)
++ // It also allows us a treat a passed-in socket in exactly the same way as
++ // a self-managed socket (in that we can call the open() and close() methods
++ // of this class).
++ if (!isopen_) {
++ if (endpoint->getFamily() == AF_INET) {
++ socket_.open(boost::asio::ip::icmp::v4());
++ } else {
++ socket_.open(boost::asio::ip::icmp::v6());
++ }
++ isopen_ = true;
++
++ // Ensure it can send and receive at least 4K buffers.
++ boost::asio::ip::icmp::socket::send_buffer_size snd_size;
++ socket_.get_option(snd_size);
++ if (snd_size.value() < MIN_SIZE) {
++ snd_size = MIN_SIZE;
++ socket_.set_option(snd_size);
++ }
++
++ boost::asio::ip::icmp::socket::receive_buffer_size rcv_size;
++ socket_.get_option(rcv_size);
++ if (rcv_size.value() < MIN_SIZE) {
++ rcv_size = MIN_SIZE;
++ socket_.set_option(rcv_size);
++ }
++
++ boost::asio::socket_base::do_not_route option(true);
++ socket_.set_option(option);
++ }
++}
++
++// Send a message. Should never do this if the socket is not open, so throw
++// an exception if this is the case.
++
++template <typename C> void
++ICMPSocket<C>::asyncSend(const void* data, size_t length,
++ const asiolink::IOEndpoint* endpoint, C& callback) {
++ if (isopen_) {
++
++ // Upconvert to a ICMPEndpoint. We need to do this because although
++ // IOEndpoint is the base class of ICMPEndpoint and TCPEndpoint, it
++ // does not contain a method for getting at the underlying endpoint
++ // type - that is in the derived class and the two classes differ on
++ // return type.
++ isc_throw_assert(endpoint->getProtocol() == IPPROTO_ICMP);
++ const ICMPEndpoint* udp_endpoint =
++ static_cast<const ICMPEndpoint*>(endpoint);
++
++ // ... and send the message.
++ socket_.async_send_to(boost::asio::buffer(data, length),
++ udp_endpoint->getASIOEndpoint(), callback);
++ } else {
++ isc_throw(asiolink::SocketNotOpen,
++ "attempt to send on a ICMP socket that is not open");
++ }
++}
++
++// Receive a message. Should never do this if the socket is not open, so throw
++// an exception if this is the case.
++
++template <typename C> void
++ICMPSocket<C>::asyncReceive(void* data, size_t length, size_t offset,
++ asiolink::IOEndpoint* endpoint, C& callback) {
++ if (isopen_) {
++
++ // Upconvert the endpoint again.
++ isc_throw_assert(endpoint->getProtocol() == IPPROTO_ICMP);
++ ICMPEndpoint* udp_endpoint = static_cast<ICMPEndpoint*>(endpoint);
++
++ // Ensure we can write into the buffer
++ if (offset >= length) {
++ isc_throw(asiolink::BufferOverflow, "attempt to read into area beyond end of "
++ "ICMP receive buffer");
++ }
++ void* buffer_start = static_cast<void*>(static_cast<uint8_t*>(data) + offset);
++
++ // Issue the read
++ socket_.async_receive_from(boost::asio::buffer(buffer_start, length - offset),
++ udp_endpoint->getASIOEndpoint(), callback);
++ } else {
++ isc_throw(asiolink::SocketNotOpen,
++ "attempt to receive from a ICMP socket that is not open");
++ }
++}
++
++// Receive complete. Just copy the data across to the output buffer and
++// update arguments as appropriate.
++
++template <typename C> bool
++ICMPSocket<C>::processReceivedData(const void* staging, size_t length,
++ size_t& cumulative, size_t& offset,
++ size_t& expected,
++ isc::util::OutputBufferPtr& outbuff) {
++ // Set return values to what we should expect.
++ cumulative = length;
++ expected = length;
++ offset = 0;
++
++ // Copy data across
++ outbuff->writeData(staging, length);
++
++ // ... and mark that we have everything.
++ return (true);
++}
++
++// Cancel I/O on the socket. No-op if the socket is not open.
++
++template <typename C> void
++ICMPSocket<C>::cancel() {
++ if (isopen_) {
++ socket_.cancel();
++ }
++}
++
++// Close the socket down. Can only do this if the socket is open and we are
++// managing it ourself.
++
++template <typename C> void
++ICMPSocket<C>::close() {
++ if (isopen_ && socket_ptr_) {
++ socket_.close();
++ isopen_ = false;
++ }
++}
++
++template <typename C> uint16_t
++ICMPSocket<C>::calcChecksum(const uint8_t* buf, const uint32_t buf_size) {
++ uint32_t sum = 0;
++ uint32_t i;
++ for (i = 0; i < (buf_size & ~1U); i += 2) {
++ uint16_t chunk = buf[i] << 8 | buf[i + 1];
++ sum += chunk;
++ if (sum > 0xFFFF) {
++ sum -= 0xFFFF;
++ }
++ }
++ // If one byte has left, we also need to add it to the checksum.
++ if (i < buf_size) {
++ sum += buf[i] << 8;
++ if (sum > 0xFFFF) {
++ sum -= 0xFFFF;
++ }
++ }
++
++ return (sum);
++}
++
++} // namespace ping_check
++} // namespace isc
++#endif // ICMP_SOCKET_H
+diff --git a/src/hooks/dhcp/ping_check/libloadtests/.gitignore b/src/hooks/dhcp/ping_check/libloadtests/.gitignore
+new file mode 100644
+index 0000000000..ada6ed5036
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/libloadtests/.gitignore
+@@ -0,0 +1 @@
++hook_load_unittests
+diff --git a/src/hooks/dhcp/ping_check/libloadtests/Makefile.am b/src/hooks/dhcp/ping_check/libloadtests/Makefile.am
+new file mode 100644
+index 0000000000..139a068b3c
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/libloadtests/Makefile.am
+@@ -0,0 +1,60 @@
++SUBDIRS = .
++
++AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
++AM_CPPFLAGS += -I$(top_builddir)/src/hooks/dhcp/ping_check -I$(top_srcdir)/src/hooks/dhcp/ping_check
++AM_CPPFLAGS += $(BOOST_INCLUDES) $(CRYPTO_CFLAGS) $(CRYPTO_INCLUDES)
++AM_CPPFLAGS += -DPING_CHECK_LIB_SO=\"$(abs_top_builddir)/src/hooks/dhcp/ping_check/.libs/libdhcp_ping_check.so\"
++AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\"
++
++AM_CXXFLAGS = $(KEA_CXXFLAGS)
++
++if USE_STATIC_LINK
++AM_LDFLAGS = -static
++endif
++
++# Unit test data files need to get installed.
++EXTRA_DIST =
++
++CLEANFILES = *.gcno *.gcda
++
++TESTS_ENVIRONMENT = $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND)
++
++LOG_COMPILER = $(LIBTOOL)
++AM_LOG_FLAGS = --mode=execute
++
++TESTS =
++if HAVE_GTEST
++TESTS += hook_load_unittests
++
++hook_load_unittests_SOURCES = run_unittests.cc
++hook_load_unittests_SOURCES += load_unload_unittests.cc
++
++hook_load_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) $(LOG4CPLUS_INCLUDES)
++
++hook_load_unittests_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS) $(GTEST_LDFLAGS)
++
++hook_load_unittests_CXXFLAGS = $(AM_CXXFLAGS)
++
++hook_load_unittests_LDADD = $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la
++hook_load_unittests_LDADD += $(top_builddir)/src/lib/process/libkea-process.la
++hook_load_unittests_LDADD += $(top_builddir)/src/lib/eval/libkea-eval.la
++hook_load_unittests_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la
++hook_load_unittests_LDADD += $(top_builddir)/src/lib/stats/libkea-stats.la
++hook_load_unittests_LDADD += $(top_builddir)/src/lib/config/libkea-cfgclient.la
++hook_load_unittests_LDADD += $(top_builddir)/src/lib/http/libkea-http.la
++hook_load_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la
++hook_load_unittests_LDADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la
++hook_load_unittests_LDADD += $(top_builddir)/src/lib/database/libkea-database.la
++hook_load_unittests_LDADD += $(top_builddir)/src/lib/cc/libkea-cc.la
++hook_load_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
++hook_load_unittests_LDADD += $(top_builddir)/src/lib/dns/libkea-dns++.la
++hook_load_unittests_LDADD += $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la
++hook_load_unittests_LDADD += $(top_builddir)/src/lib/log/libkea-log.la
++hook_load_unittests_LDADD += $(top_builddir)/src/lib/util/libkea-util.la
++hook_load_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
++hook_load_unittests_LDADD += $(LOG4CPLUS_LIBS)
++hook_load_unittests_LDADD += $(CRYPTO_LIBS)
++hook_load_unittests_LDADD += $(BOOST_LIBS)
++hook_load_unittests_LDADD += $(GTEST_LDADD)
++endif
++noinst_PROGRAMS = $(TESTS)
+diff --git a/src/hooks/dhcp/ping_check/libloadtests/load_unload_unittests.cc b/src/hooks/dhcp/ping_check/libloadtests/load_unload_unittests.cc
+new file mode 100644
+index 0000000000..67275db617
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/libloadtests/load_unload_unittests.cc
+@@ -0,0 +1,107 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++/// @file This file contains tests which exercise the load and unload
++/// functions in the ddns tuning hook library. In order to test the load
++/// function, one must be able to pass it hook library parameters. The
++/// the only way to populate these parameters is by actually loading the
++/// library via HooksManager::loadLibraries().
++
++#include <config.h>
++
++#include <dhcpsrv/testutils/lib_load_test_fixture.h>
++#include <testutils/gtest_utils.h>
++
++#include <gtest/gtest.h>
++#include <errno.h>
++
++using namespace std;
++using namespace isc;
++using namespace isc::hooks;
++using namespace isc::data;
++using namespace isc::dhcp;
++using namespace isc::process;
++
++namespace {
++
++/// @brief Test fixture for testing loading and unloading the ddns tuning library
++class PingCheckLibLoadTest : public isc::test::LibLoadTest {
++public:
++ /// @brief Constructor
++ PingCheckLibLoadTest() : LibLoadTest(PING_CHECK_LIB_SO) {
++ }
++
++ /// @brief Destructor
++ virtual ~PingCheckLibLoadTest() {
++ }
++
++ /// @brief Registers hooks in the hook manager.
++ /// Normally this is done by the server core code (@c Dhcpv4Srv).
++ void registerHooks() {
++ hook_index_dhcp4_srv_configured_ = HooksManager::registerHook("dhcp4_srv_configured");
++ hook_index_lease4_offer_ = HooksManager::registerHook("lease4_offer");
++ }
++
++ /// @brief Checks that expected callouts are present.
++ void calloutsPresent() {
++ bool result;
++ ASSERT_NO_THROW_LOG(result = HooksManager::calloutsPresent(hook_index_dhcp4_srv_configured_));
++ EXPECT_TRUE(result);
++ ASSERT_NO_THROW_LOG(result = HooksManager::calloutsPresent(hook_index_lease4_offer_));
++ EXPECT_TRUE(result);
++ }
++
++ /// @brief Creates a valid set of ping-check hook parameters.
++ virtual ElementPtr validConfigParams() {
++ ElementPtr params = Element::createMap();
++ params->set("min-ping-requests", Element::create(3));
++ params->set("reply-timeout", Element::create(100));
++ params->set("enable-ping-check", Element::create(true));
++ params->set("ping-cltt-secs", Element::create(60));
++ params->set("ping-channel-threads", Element::create(1));
++ return (params);
++ }
++
++ /// @brief Hook index values.
++ int hook_index_dhcp4_srv_configured_;
++ int hook_index_lease4_offer_;
++};
++
++// Simple V4 test that checks the library can be loaded and unloaded several times.
++TEST_F(PingCheckLibLoadTest, validLoad4) {
++ validDaemonTest("kea-dhcp4", AF_INET, valid_params_);
++}
++
++// Simple test that checks the library cannot be loaded by invalid daemons.
++TEST_F(PingCheckLibLoadTest, invalidDaemonLoad) {
++ // V6 is invalid regardless of family.
++ invalidDaemonTest("kea-dhcp6", AF_INET, valid_params_);
++ invalidDaemonTest("kea-dhcp6", AF_INET6, valid_params_);
++
++ invalidDaemonTest("kea-ctrl-agent", AF_INET, valid_params_);
++ invalidDaemonTest("kea-dhcp-ddns", AF_INET, valid_params_);
++ invalidDaemonTest("bogus", AF_INET, valid_params_);
++}
++
++// Verifies that callout functions exist after loading the library.
++TEST_F(PingCheckLibLoadTest, verifyCallouts) {
++ // Set family and daemon's proc name and register hook points.
++ isc::dhcp::CfgMgr::instance().setFamily(AF_INET);
++ isc::process::Daemon::setProcName("kea-dhcp4");
++ registerHooks();
++
++ // Add library to config and load it.
++ ASSERT_NO_THROW_LOG(addLibrary(lib_so_name_, valid_params_));
++ ASSERT_NO_THROW_LOG(loadLibraries());
++
++ // Verify that expected callouts are present.
++ calloutsPresent();
++
++ // Unload the library.
++ ASSERT_NO_THROW_LOG(unloadLibraries());
++}
++
++} // end of anonymous namespace
+diff --git a/src/hooks/dhcp/ping_check/libloadtests/meson.build b/src/hooks/dhcp/ping_check/libloadtests/meson.build
+new file mode 100644
+index 0000000000..da8bf439c0
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/libloadtests/meson.build
+@@ -0,0 +1,21 @@
++if not TESTS_OPT.enabled()
++ subdir_done()
++endif
++
++dhcp_ping_check_libloadtests = executable(
++ 'dhcp-ping-check-libload-tests',
++ 'load_unload_unittests.cc',
++ 'run_unittests.cc',
++ cpp_args: [
++ f'-DPING_CHECK_LIB_SO="@TOP_BUILD_DIR@/src/hooks/dhcp/ping_check/libdhcp_ping_check.so"',
++ ],
++ dependencies: [GTEST_DEP, CRYPTO_DEP],
++ include_directories: [include_directories('.')] + INCLUDES,
++ link_with: LIBS_BUILT_SO_FAR,
++)
++test(
++ 'dhcp-ping-check-libloadtests',
++ dhcp_ping_check_libloadtests,
++ depends: [dhcp_ping_check_lib],
++ protocol: 'gtest',
++)
+diff --git a/src/hooks/dhcp/ping_check/libloadtests/run_unittests.cc b/src/hooks/dhcp/ping_check/libloadtests/run_unittests.cc
+new file mode 100644
+index 0000000000..d249e2362e
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/libloadtests/run_unittests.cc
+@@ -0,0 +1,19 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++#include <config.h>
++
++#include <log/logger_support.h>
++#include <gtest/gtest.h>
++
++int
++main(int argc, char* argv[]) {
++ ::testing::InitGoogleTest(&argc, argv);
++ isc::log::initLogger();
++ int result = RUN_ALL_TESTS();
++
++ return (result);
++}
+diff --git a/src/hooks/dhcp/ping_check/meson.build b/src/hooks/dhcp/ping_check/meson.build
+new file mode 100644
+index 0000000000..d3a1e70b49
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/meson.build
+@@ -0,0 +1,41 @@
++dhcp_ping_check_lib = shared_library(
++ 'dhcp_ping_check',
++ 'config_cache.cc',
++ 'icmp_msg.cc',
++ 'ping_channel.cc',
++ 'ping_check_callouts.cc',
++ 'ping_check_config.cc',
++ 'ping_check_log.cc',
++ 'ping_check_messages.cc',
++ 'ping_check_mgr.cc',
++ 'ping_context.cc',
++ 'ping_context_store.cc',
++ 'version.cc',
++ dependencies: [CRYPTO_DEP],
++ include_directories: [include_directories('.')] + INCLUDES,
++ install: true,
++ install_dir: HOOKS_PATH,
++ install_rpath: INSTALL_RPATH,
++ build_rpath: BUILD_RPATH,
++ link_with: LIBS_BUILT_SO_FAR,
++ name_suffix: 'so',
++)
++dhcp_ping_check_archive = static_library(
++ 'dhcp_ping_check',
++ objects: dhcp_ping_check_lib.extract_all_objects(recursive: false),
++)
++subdir('libloadtests')
++subdir('tests')
++
++if KEA_MSG_COMPILER.found()
++ target_gen_messages = run_target(
++ 'src-hooks-dhcp-ping_check-ping_check_messages',
++ command: [
++ CD_AND_RUN,
++ TOP_SOURCE_DIR,
++ KEA_MSG_COMPILER,
++ 'src/hooks/dhcp/ping_check/ping_check_messages.mes',
++ ],
++ )
++ TARGETS_GEN_MESSAGES += [target_gen_messages]
++endif
+diff --git a/src/hooks/dhcp/ping_check/ping_channel.cc b/src/hooks/dhcp/ping_check/ping_channel.cc
+new file mode 100644
+index 0000000000..6a6a88c038
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/ping_channel.cc
+@@ -0,0 +1,466 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++#include <config.h>
++#include <ping_channel.h>
++#include <ping_check_log.h>
++#include <dhcp/iface_mgr.h>
++#include <exceptions/exceptions.h>
++#include <util/multi_threading_mgr.h>
++#include <iostream>
++
++using namespace isc;
++using namespace isc::asiolink;
++using namespace isc::dhcp;
++using namespace isc::util;
++
++namespace ph = std::placeholders;
++
++namespace isc {
++namespace ping_check {
++
++uint32_t
++PingChannel::nextEchoInstanceNum() {
++ static uint32_t echo_instance_num = 0x00010000;
++ if (echo_instance_num == UINT32_MAX) {
++ echo_instance_num = 0x00010001;
++ } else {
++ ++echo_instance_num;
++ }
++
++ return (echo_instance_num);
++}
++
++PingChannel::PingChannel(IOServicePtr& io_service,
++ NextToSendCallback next_to_send_cb,
++ EchoSentCallback echo_sent_cb,
++ ReplyReceivedCallback reply_received_cb,
++ ShutdownCallback shutdown_cb)
++ : io_service_(io_service),
++ next_to_send_cb_(next_to_send_cb),
++ echo_sent_cb_(echo_sent_cb),
++ reply_received_cb_(reply_received_cb),
++ shutdown_cb_(shutdown_cb),
++ socket_(0), input_buf_(256),
++ reading_(false), sending_(false), stopping_(false), mutex_(new std::mutex),
++ single_threaded_(!MultiThreadingMgr::instance().getMode()),
++ watch_socket_(0), registered_write_fd_(-1), registered_read_fd_(-1) {
++ if (!io_service_) {
++ isc_throw(BadValue,
++ "PingChannel ctor - io_service cannot be empty");
++ }
++}
++
++PingChannel::~PingChannel() {
++ close();
++}
++
++void
++PingChannel::open() {
++ try {
++ MultiThreadingLock lock(*mutex_);
++ if (socket_ && socket_->isOpen()) {
++ return;
++ }
++
++ // For open(), the endpoint is only used to determine protocol,
++ // the address is irrelevant.
++ ICMPEndpoint ping_to_endpoint(IOAddress::IPV4_ZERO_ADDRESS());
++ SocketCallback socket_cb(
++ [](boost::system::error_code ec, size_t /*length */) {
++ isc_throw(Unexpected, "ICMPSocket open is synchronous, should not invoke cb: "
++ << ec.message());
++ }
++ );
++
++ socket_.reset(new PingSocket(io_service_));
++ socket_->open(&ping_to_endpoint, socket_cb);
++ reading_ = false;
++ sending_ = false;
++ stopping_ = false;
++
++ if (single_threaded_) {
++ // Open new watch socket.
++ watch_socket_.reset(new util::WatchSocket());
++
++ // Register the WatchSocket with IfaceMgr to signal data ready to write.
++ registered_write_fd_ = watch_socket_->getSelectFd();
++ IfaceMgr::instance().addExternalSocket(registered_write_fd_, IfaceMgr::SocketCallback());
++
++ // Register ICMPSocket with IfaceMgr to signal data ready to read.
++ registered_read_fd_ = socket_->getNative();
++ IfaceMgr::instance().addExternalSocket(registered_read_fd_, IfaceMgr::SocketCallback());
++ }
++
++ } catch (const std::exception& ex) {
++ isc_throw(Unexpected, "PingChannel::open failed:" << ex.what());
++ }
++
++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC, PING_CHECK_CHANNEL_SOCKET_OPENED);
++}
++
++bool
++PingChannel::isOpen() const {
++ MultiThreadingLock lock(*mutex_);
++ return (socket_ && socket_->isOpen());
++}
++
++void
++PingChannel::close() {
++ try {
++ MultiThreadingLock lock(*mutex_);
++
++ if (single_threaded_) {
++ // Unregister from IfaceMgr.
++ if (registered_write_fd_ != -1) {
++ IfaceMgr::instance().deleteExternalSocket(registered_write_fd_);
++ registered_write_fd_ = -1;
++ }
++
++ if (registered_read_fd_ != -1) {
++ IfaceMgr::instance().deleteExternalSocket(registered_read_fd_);
++ registered_read_fd_ = -1;
++ }
++
++ // Close watch socket.
++ if (watch_socket_) {
++ std::string error_string;
++ watch_socket_->closeSocket(error_string);
++ if (!error_string.empty()) {
++ LOG_ERROR(ping_check_logger, PING_CHECK_CHANNEL_WATCH_SOCKET_CLOSE_ERROR)
++ .arg(error_string);
++ }
++
++ watch_socket_.reset();
++ }
++ }
++
++ if (!socket_ || !socket_->isOpen()) {
++ return;
++ }
++
++ socket_->close();
++ } catch (const std::exception& ex) {
++ // On close error, log but do not throw.
++ LOG_ERROR(ping_check_logger, PING_CHECK_CHANNEL_SOCKET_CLOSE_ERROR)
++ .arg(ex.what());
++ }
++
++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC, PING_CHECK_CHANNEL_SOCKET_CLOSED);
++}
++
++void
++PingChannel::stopChannel() {
++ {
++ MultiThreadingLock lock(*mutex_);
++ if (stopping_) {
++ return;
++ }
++
++ stopping_ = true;
++ }
++
++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC, PING_CHECK_CHANNEL_STOP);
++ close();
++
++ if (shutdown_cb_) {
++ (shutdown_cb_)();
++ }
++}
++
++void
++PingChannel::asyncReceive(void* data, size_t length, size_t offset,
++ asiolink::IOEndpoint* endpoint, SocketCallback& callback) {
++ socket_->asyncReceive(data, length, offset, endpoint, callback);
++}
++
++void
++PingChannel::asyncSend(void* data, size_t length, asiolink::IOEndpoint* endpoint,
++ SocketCallback& callback) {
++ socket_->asyncSend(data, length, endpoint, callback);
++
++ if (single_threaded_) {
++ // Set IO ready marker so sender activity is visible to select() or poll().
++ watch_socket_->markReady();
++ }
++}
++
++void
++PingChannel::doRead() {
++ try {
++ MultiThreadingLock lock(*mutex_);
++ if (!canRead()) {
++ return;
++ }
++
++ reading_ = true;
++
++ // Create instance of the callback. It is safe to pass the
++ // local instance of the callback, because the underlying
++ // std functions make copies as needed.
++ SocketCallback cb(std::bind(&PingChannel::socketReadCallback,
++ shared_from_this(),
++ ph::_1, // error
++ ph::_2)); // bytes_transferred
++ asyncReceive(static_cast<void*>(getInputBufData()), getInputBufSize(),
++ 0, &reply_endpoint_, cb);
++ } catch (const std::exception& ex) {
++ // Normal IO failures should be passed to the callback. A failure here
++ // indicates the call to asyncReceive() itself failed.
++ LOG_ERROR(ping_check_logger, PING_CHECK_UNEXPECTED_READ_ERROR)
++ .arg(ex.what());
++ stopChannel();
++ }
++}
++
++void
++PingChannel::socketReadCallback(boost::system::error_code ec, size_t length) {
++ {
++ MultiThreadingLock lock(*mutex_);
++ if (stopping_) {
++ return;
++ }
++ }
++
++ if (ec) {
++ if (ec.value() == boost::asio::error::operation_aborted) {
++ // IO service has been stopped and the connection is probably
++ // going to be shutting down.
++ return;
++ } else if ((ec.value() == boost::asio::error::try_again) ||
++ (ec.value() == boost::asio::error::would_block)) {
++ // We got EWOULDBLOCK or EAGAIN which indicates that we may be able to
++ // read something from the socket on the next attempt. Just make sure
++ // we don't try to read anything now in case there is any garbage
++ // passed in length.
++ length = 0;
++ } else {
++ // Anything else is fatal for the socket.
++ LOG_ERROR(ping_check_logger, PING_CHECK_CHANNEL_SOCKET_READ_FAILED)
++ .arg(ec.message());
++ stopChannel();
++ return;
++ }
++ }
++
++ // Unpack the reply and pass it to the reply callback.
++ ICMPMsgPtr reply;
++ if (length > 0) {
++ {
++ try {
++ MultiThreadingLock lock(*mutex_);
++ reply = ICMPMsg::unpack(getInputBufData(), getInputBufSize());
++ if (reply->getType() == ICMPMsg::ECHO_REPLY) {
++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_DETAIL,
++ PING_CHECK_CHANNEL_ECHO_REPLY_RECEIVED)
++ .arg(reply->getSource())
++ .arg(reply->getId())
++ .arg(reply->getSequence());
++ }
++ } catch (const std::exception& ex) {
++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC,
++ PING_CHECK_CHANNEL_MALFORMED_PACKET_RECEIVED)
++ .arg(ex.what());
++ }
++ }
++ }
++
++ {
++ MultiThreadingLock lock(*mutex_);
++ reading_ = false;
++ }
++
++ if (reply) {
++ (reply_received_cb_)(reply);
++ }
++
++ // Start the next read.
++ doRead();
++}
++
++void
++PingChannel::startSend() {
++ MultiThreadingLock lock(*mutex_);
++ if (canSend()) {
++ // Post the call to sendNext to the IOService.
++ // This ensures its carried out on a thread
++ // associated with the channel's IOService
++ // not the thread invoking this function.
++ auto f = [](PingChannelPtr ptr) { ptr->sendNext(); };
++ io_service_->post(std::bind(f, shared_from_this()));
++ }
++}
++
++void
++PingChannel::startRead() {
++ MultiThreadingLock lock(*mutex_);
++ if (canRead()) {
++ // Post the call to doRead to the IOService.
++ // This ensures its carried out on a thread
++ // associated with the channel's IOService
++ // not the thread invoking this function.
++ auto f = [](PingChannelPtr ptr) { ptr->doRead(); };
++ io_service_->post(std::bind(f, shared_from_this()));
++ }
++}
++
++void
++PingChannel::sendNext() {
++ try {
++ MultiThreadingLock lock(*mutex_);
++ if (!canSend()) {
++ // Can't send right now, get out.
++ return;
++ }
++
++ // Fetch the next one to send.
++ IOAddress target("0.0.0.0");
++ if (!((next_to_send_cb_)(target))) {
++ // Nothing to send.
++ return;
++ }
++
++ // Have an target IP, build an ECHO REQUEST for it.
++ sending_ = true;
++ ICMPMsgPtr next_echo(new ICMPMsg());
++ next_echo->setType(ICMPMsg::ECHO_REQUEST);
++ next_echo->setDestination(target);
++
++ uint32_t instance_num = nextEchoInstanceNum();
++ next_echo->setId(static_cast<uint16_t>(instance_num >> 16));
++ next_echo->setSequence(static_cast<uint16_t>(instance_num & 0x0000FFFF));
++
++ // Get packed wire-form.
++ ICMPPtr echo_icmp = next_echo->pack();
++
++ // Create instance of the callback. It is safe to pass the
++ // local instance of the callback, because the underlying
++ // std functions make copies as needed.
++ SocketCallback cb(std::bind(&PingChannel::socketWriteCallback,
++ shared_from_this(),
++ next_echo,
++ ph::_1, // error
++ ph::_2)); // bytes_transferred
++
++ ICMPEndpoint target_endpoint(target);
++ asyncSend(echo_icmp.get(), sizeof(struct icmp), &target_endpoint, cb);
++ } catch (const std::exception& ex) {
++ // Normal IO failures should be passed to the callback. A failure here
++ // indicates the call to asyncSend() itself failed.
++ LOG_ERROR(ping_check_logger, PING_CHECK_UNEXPECTED_WRITE_ERROR)
++ .arg(ex.what());
++ stopChannel();
++ return;
++ }
++}
++
++void
++PingChannel::socketWriteCallback(ICMPMsgPtr echo, boost::system::error_code ec,
++ size_t length) {
++ {
++ MultiThreadingLock lock(*mutex_);
++ if (stopping_) {
++ return;
++ }
++ }
++
++ if (single_threaded_) {
++ try {
++ // Clear the IO ready marker.
++ watch_socket_->clearReady();
++ } catch (const std::exception& ex) {
++ // This can only happen if the WatchSocket's select_fd has been
++ // compromised which is a programmatic error. We'll log the error
++ // here, then continue on and process the IO result we were given.
++ // WatchSocket issue will resurface on the next send as a closed
++ // fd in markReady() rather than fail out of this callback.
++ LOG_ERROR(ping_check_logger, PING_CHECK_CHANNEL_WATCH_SOCKET_CLEAR_ERROR)
++ .arg(ex.what());
++ }
++ }
++
++ // Handle an error. Note we can't use a case statement as some values
++ // on some OSes are the same (e.g. try_again and would_block) which causes
++ // duplicate case compilation errors.
++ bool send_failed = false;
++ if (ec) {
++ auto error_value = ec.value();
++ if (error_value == boost::asio::error::operation_aborted) {
++ // IO service has been stopped and the connection is probably
++ // going to be shutting down.
++ return;
++ } else if ((error_value == boost::asio::error::try_again) ||
++ (error_value == boost::asio::error::would_block)) {
++ // We got EWOULDBLOCK or EAGAIN which indicates that we may be able to
++ // write something from the socket on the next attempt. Set the length
++ // to zero so we skip the completion callback.
++ length = 0;
++ } else if ((error_value == boost::asio::error::network_unreachable) ||
++ (error_value == boost::asio::error::host_unreachable) ||
++ (error_value == boost::asio::error::network_down)) {
++ // One of these implies an interface might be down, or there's no
++ // way to ping this network. Other networks might be working OK.
++ send_failed = true;
++ } else if (error_value == boost::asio::error::no_buffer_space) {
++ // Writing faster than the kernel will write them out.
++ send_failed = true;
++ } else if (error_value == boost::asio::error::access_denied) {
++ // Means the address we tried to ping is not allowed. Most likey a broadcast
++ // address.
++ send_failed = true;
++ } else {
++ // Anything else is fatal for the socket.
++ LOG_ERROR(ping_check_logger, PING_CHECK_CHANNEL_SOCKET_WRITE_FAILED)
++ .arg(ec.message());
++ stopChannel();
++ return;
++ }
++ }
++
++ {
++ MultiThreadingLock lock(*mutex_);
++ sending_ = false;
++ }
++
++ if (send_failed) {
++ // Invoke the callback with send failed. This instructs the manager
++ // to treat the address as free to use.
++ LOG_ERROR(ping_check_logger, PING_CHECK_CHANNEL_NETWORK_WRITE_ERROR)
++ .arg(echo->getDestination())
++ .arg(ec.message());
++ // Invoke the send completed callback.
++ (echo_sent_cb_)(echo, true);
++ } else if (length > 0) {
++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_DETAIL,
++ PING_CHECK_CHANNEL_ECHO_REQUEST_SENT)
++ .arg(echo->getDestination())
++ .arg(echo->getId())
++ .arg(echo->getSequence());
++ // Invoke the send completed callback.
++ (echo_sent_cb_)(echo, false);
++ }
++
++ // Schedule the next send.
++ sendNext();
++}
++
++size_t
++PingChannel::getInputBufSize() const {
++ return (input_buf_.size());
++}
++
++unsigned char*
++PingChannel::getInputBufData() {
++ if (input_buf_.empty()) {
++ isc_throw(InvalidOperation,
++ "PingChannel::getInputBufData() - cannot access empty buffer");
++ }
++
++ return (input_buf_.data());
++}
++
++} // end of namespace ping_check
++} // end of namespace isc
+diff --git a/src/hooks/dhcp/ping_check/ping_channel.h b/src/hooks/dhcp/ping_check/ping_channel.h
+new file mode 100644
+index 0000000000..ad798188e3
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/ping_channel.h
+@@ -0,0 +1,371 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++#ifndef PING_CHANNEL_H
++#define PING_CHANNEL_H
++
++#include <asiolink/asio_wrapper.h>
++#include <asiolink/io_address.h>
++#include <asiolink/io_service.h>
++#include <util/watch_socket.h>
++#include <icmp_msg.h>
++#include <icmp_socket.h>
++
++#include <boost/scoped_ptr.hpp>
++#include <boost/enable_shared_from_this.hpp>
++
++#include <iostream>
++#include <mutex>
++
++namespace isc {
++namespace ping_check {
++
++/// @brief Type of the function implementing a callback invoked by the
++/// @c SocketCallback functor.
++typedef std::function<void(boost::system::error_code ec, size_t length)> SocketCallbackFunction;
++
++/// @brief Functor associated with the socket object.
++///
++/// This functor calls a callback function specified in the constructor.
++class SocketCallback {
++public:
++ /// @brief Constructor.
++ ///
++ /// @param socket_callback Callback to be invoked by the functor upon
++ /// an event associated with the socket.
++ explicit inline SocketCallback(SocketCallbackFunction socket_callback)
++ : callback_(socket_callback) {
++ };
++
++ /// @brief Operator called when event associated with a socket occurs.
++ ///
++ /// This operator returns immediately when received @c boost::system::error_code
++ /// is equal to @c boost::asio::error::operation_aborted.
++ ///
++ /// @param ec Error code.
++ /// @param length Data length.
++ inline void operator()(boost::system::error_code ec, size_t length = 0) {
++ if (ec.value() == boost::asio::error::operation_aborted) {
++ return;
++ }
++
++ callback_(ec, length);
++ };
++
++private:
++ /// @brief Supplied callback.
++ SocketCallbackFunction callback_;
++};
++
++/// @brief Socket type for performing ICMP socket IO.
++typedef ICMPSocket<SocketCallback> PingSocket;
++
++/// @brief Defines a pointer to PingSocket.
++typedef boost::shared_ptr<PingSocket> PingSocketPtr;
++
++/// @brief Function type for callback that fetches next IOAddress to ping.
++typedef std::function<bool(asiolink::IOAddress& target)> NextToSendCallback;
++
++/// @brief Function type for callback to invoke upon ECHO send completion.
++typedef std::function<void(ICMPMsgPtr& echo, bool send_failed)> EchoSentCallback;
++
++/// @brief Function type for callback to invoke when an ICMP reply has been
++/// received.
++typedef std::function<void(ICMPMsgPtr& reply)> ReplyReceivedCallback;
++
++/// @brief Function type for callback to invoke when the channel has shutdown.
++typedef std::function<void()> ShutdownCallback;
++
++/// @brief Provides thread-safe ICMP ECHO REQUEST/ECHO REPLY service
++///
++/// PingChannel uses a @ref PingSocket to send out ECHO REQUESTs and
++/// receive ICMP replies. It is thread-safe and can be driven either
++/// with a single-threaded IOService or a multi-threaded
++/// IOServiceThreadPool. It uses series of callbacks to perpetually
++/// send requests to target addresses and feed back replies received:
++///
++/// -# next_to_send_cb_ - callback to invoke to fetch the next address to ping
++/// -# echo_sent_cb_ - callback to invoke when an ECHO REQUEST has been sent out
++/// -# reply_received_cb_ - callback to invoke when an ICMP reply has been received.
++/// -# channel_shutdown_cb_ - callback to invoke when the channel has shutdown
++///
++/// Callback handlers are supplied via the PingChannel constructor. Higher order
++/// functions are provided, that once instantiated, can be used by calling layers
++/// to control the channel (e.g. open the channel, initiate reading, initiate
++/// writing, and close the channel).
++///
++/// @note Callbacks handlers must be thread-safe if the channel is
++/// driven by an IOServiceThreadPool.
++///
++class PingChannel : public boost::enable_shared_from_this<PingChannel> {
++public:
++ /// @brief Constructor
++ ///
++ /// Instantiates the channel with its socket closed.
++ ///
++ /// @param io_service pointer to the IOService instance that will manage
++ /// the channel's IO. Must not be empty
++ /// @param next_to_send_cb callback to invoke to fetch the next IOAddress
++ /// to ping
++ /// @param echo_sent_cb callback to invoke when an ECHO send has completed
++ /// @param reply_received_cb callback to invoke when an ICMP reply has been
++ /// received. This callback is passed all inbound ICMP messages (e.g. ECHO
++ /// REPLY, UNREACHABLE, etc...)
++ /// @param shutdown_cb callback to invoke when the channel has shutdown due
++ /// to an error
++ ///
++ /// @throw BadValue if io_service is empty.
++ PingChannel(asiolink::IOServicePtr& io_service,
++ NextToSendCallback next_to_send_cb,
++ EchoSentCallback echo_sent_cb,
++ ReplyReceivedCallback reply_received_cb,
++ ShutdownCallback shutdown_cb = ShutdownCallback());
++
++ /// @brief Destructor
++ ///
++ /// Closes the socket if its open.
++ virtual ~PingChannel();
++
++ /// @brief Opens the socket for communications
++ ///
++ /// (Re)Creates the @ref PingSocket instance and opens it.
++ ///
++ /// @throw Unexpected if the open fails.
++ void open();
++
++ /// @brief Indicates whether or not the channel socket is open.
++ ///
++ /// @return true if the socket is open.
++ bool isOpen() const;
++
++ // @brief Schedules the next send.
++ //
++ // If the socket is not currently sending it posts a call to @c sendNext()
++ // to the channel's IOService.
++ virtual void startSend();
++
++ // @brief Schedules the next read.
++ //
++ // If the socket is not currently reading it posts a call to @c doRead()
++ // to the channel's IOService.
++ void startRead();
++
++ /// @brief Closes the channel's socket.
++ void close();
++
++ /// @brief Fetches the channel's IOService
++ ///
++ /// @return pointer to the IOService.
++ asiolink::IOServicePtr getIOService() {
++ return (io_service_);
++ }
++
++protected:
++ /// @brief Receive data on the socket asynchronously
++ ///
++ /// Calls the underlying socket's asyncReceive() method to read a
++ /// packet of data from a remote endpoint. Arrival of the data is signalled
++ /// via a call to the callback function.
++ ///
++ /// This virtual function is provided as means to inject errors during
++ /// read operations to facilitate testing.
++ ///
++ /// @param data buffer to receive incoming message
++ /// @param length length of the data buffer
++ /// @param offset offset into buffer where data is to be put
++ /// @param endpoint source of the communication
++ /// @param callback callback object
++ virtual void asyncReceive(void* data, size_t length, size_t offset,
++ asiolink::IOEndpoint* endpoint, SocketCallback& callback);
++
++ /// @brief Send data on the socket asynchronously
++ ///
++ /// Calls the underlying socket's asyncSend() method to send a
++ /// packet of data from a remote endpoint. Arrival of the data is signalled
++ /// via a call to the callback function.
++ ///
++ /// This virtual function is provided as means to inject errors during
++ /// write operations to facilitate testing.
++ ///
++ /// @param data buffer containing the data to send
++ /// @param length length of the data buffer
++ /// @param endpoint destination of the communication
++ /// @param callback callback object
++ virtual void asyncSend(void* data, size_t length, asiolink::IOEndpoint* endpoint,
++ SocketCallback& callback);
++
++protected:
++ /// @brief Initiates an asynchronous socket read.
++ ///
++ /// If the channel is able to read (is open, not stopping and not
++ /// currently reading) it invokes @ref PingSocket::asyncReceive()
++ /// otherwise it simply returns. If the call to asyncReceive() fails
++ /// it calls @c stopChannel() otherwise, when it completes it will
++ /// invoke @c socketReadCallback().
++ void doRead();
++
++ /// @brief Socket read completion callback
++ ///
++ /// Invoked when PingSocket::asyncRead() completes.
++ /// Upon read success and data received:
++ ///
++ /// -# Unpacks the wire data
++ /// -# Pass the resultant ICMPMsg to reply received callback
++ /// -# start next read
++ ///
++ /// On error conditions:
++ ///
++ /// -# Operation aborted: socket is shutting down, simply return
++ /// -# Operation would block/try again: start a new read
++ /// -# Any other error, shut down the channel
++ ///
++ /// @param ec error code indicating either success or the error encountered
++ /// @param length number of bytes read
++ void socketReadCallback(boost::system::error_code ec, size_t length);
++
++ /// @brief Initiates sending the next ECHO REQUEST
++ ///
++ /// If the channel is able to send (i.e is open, not stopping and not
++ /// currently writing):
++ /// -# Invoke next to send callback to fetch the next target IP address
++ /// -# If there is no next target, return
++ /// -# Construct the ECHO REQUEST for the target and pack it into wire form
++ /// -# Begin sending the request by passing to @c PingSocket::asyncSend()
++ /// -# If the asyncSend() call fails shutdown the channel, otherwise when
++ /// it completes it invokes @c socketWriteCallback().
++ virtual void sendNext();
++
++ /// @brief Socket write completion callback
++ ///
++ /// Invoked when PingSocket::asyncWrite() completes.
++ /// Upon write success:
++ ///
++ /// -# Pass the ECHO REQUEST (i.e. echo_sent) to echo sent callback
++ /// -# start next write
++ ///
++ /// On error conditions:
++ ///
++ /// -# Operation aborted: socket is shutting down, simply return
++ /// -# Operation would block/try again: start a new write
++ /// -# Any other error, shut down the channel
++ ///
++ /// @param echo_sent ECHO REQUEST that was written (or attempted to be
++ /// written)
++ /// @param ec error code indicating either success or the error encountered
++ /// @param length number of bytes written
++ void socketWriteCallback(ICMPMsgPtr echo_sent, boost::system::error_code ec,
++ size_t length);
++
++ /// @brief Closes the socket channel and invokes the shutdown callback.
++ ///
++ /// This function is invoked to notify the calling layer that the socket
++ /// has encountered an unrecoverable error and is stopping operations.
++ void stopChannel();
++
++ /// @brief returns the next unique ECHO instance number.
++ ///
++ /// This method generates and returns the next ECHO instance
++ /// number by incrementing the current value. It is a strictly
++ /// monotonously increasing value beginning at 0x00010001.
++ /// At roll over it resets to 0x00010001.
++ ///
++ /// Must be called in a thread-safe context
++ ///
++ /// @return the next unique instance number.
++ static uint32_t nextEchoInstanceNum();
++
++ /// @brief Indicates whether or not a send can be initiated.
++ ///
++ /// Must be called in a thread-safe context
++ ///
++ /// @return True if the socket is open, is not attempting to stop, and is
++ /// not currently sending.
++ bool canSend() {
++ return (socket_ && socket_->isOpen() && !stopping_ && !sending_);
++ }
++
++ /// @brief Indicates whether or not a read can be initiated.
++ ///
++ /// Must be called in a thread-safe context
++ ///
++ /// @return True if the socket is open, is not attempting to stop, and is
++ /// not currently reading.
++ bool canRead() {
++ return (socket_ && socket_->isOpen() && !stopping_ && !reading_);
++ }
++
++ /// @brief Returns input buffer size.
++ ///
++ /// Must be called in a thread-safe context
++ ///
++ /// @return size of the input buf
++ size_t getInputBufSize() const;
++
++ /// @brief Returns pointer to the first byte of the input buffer.
++ ///
++ /// Must be called in a thread-safe context
++ ///
++ /// @return pointer to the data buffer
++ /// @throw InvalidOperation if called when the buffer is empty.
++ unsigned char* getInputBufData();
++
++ /// @brief IOService instance the drives socket IO
++ asiolink::IOServicePtr io_service_;
++
++ /// @brief Callback to invoke to fetch the next address to ping.
++ NextToSendCallback next_to_send_cb_;
++
++ /// @brief Callback to invoke when an ECHO write has completed.
++ EchoSentCallback echo_sent_cb_;
++
++ /// @brief Callback to invoke when an ICMP reply has been received.
++ ReplyReceivedCallback reply_received_cb_;
++
++ /// @brief Callback to invoke when the channel has shutdown.
++ ShutdownCallback shutdown_cb_;
++
++ /// @brief Socket through which to ping.
++ PingSocketPtr socket_;
++
++ /// @brief Buffer to hold the contents for most recent socket read.
++ std::vector<uint8_t> input_buf_;
++
++ /// @brief Retains the endpoint from which the most recent reply was received.
++ ICMPEndpoint reply_endpoint_;
++
++ /// @brief Indicates whether or not the socket has a read in progress.
++ bool reading_;
++
++ /// @brief Indicates whether or not the socket has a write in progress.
++ bool sending_;
++
++ /// @brief Indicates whether or not the channel has been told to stop.
++ bool stopping_;
++
++ /// @brief The mutex used to protect internal state.
++ const boost::scoped_ptr<std::mutex> mutex_;
++
++ /// @brief True if channel was opened in single-threaded mode, false
++ /// otherwise.
++ bool single_threaded_;
++
++ /// @brief Pointer to WatchSocket instance supplying the "select-fd".
++ util::WatchSocketPtr watch_socket_;
++
++ /// @brief WatchSocket fd registered with IfaceMgr.
++ int registered_write_fd_;
++
++ /// @brief ICMPSocket fd registered with IfaceMgr.
++ int registered_read_fd_;
++};
++
++/// @brief Defines a smart pointer to PingChannel
++typedef boost::shared_ptr<PingChannel> PingChannelPtr;
++
++} // end of namespace ping_check
++} // end of namespace isc
++
++#endif
+diff --git a/src/hooks/dhcp/ping_check/ping_check.dox b/src/hooks/dhcp/ping_check/ping_check.dox
+new file mode 100644
+index 0000000000..a7fbe839c0
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/ping_check.dox
+@@ -0,0 +1,44 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++/**
++
++@mainpage Kea Ping Check Hooks Library
++
++Welcome to Kea Ping Check Hooks Library. This documentation is
++addressed at developers who are interested in internal operation of the
++library. This file provides information needed to understand and perhaps
++extend this library.
++
++This documentation is stand-alone: you should have read and
++understood <a href="https://reports.kea.isc.org/dev_guide/">Kea
++Developer's Guide</a> and in particular its section about hooks: <a
++href="https://reports.kea.isc.org/dev_guide/df/d46/hooksdgDevelopersGuide.html">
++Hooks Developer's Guide</a>.
++
++@section cbPingCheckOverview Overview
++The @c ping_check hooks library provides the ability for kea-dhcp4 to carry
++out an ICMP ECHO test of a candidate IP address prior to sending that address to
++a DHCPv4 client in a DHCPOFFER message.
++
++@section cbPingCheckInternals Library Internals
++
++In addition to the requisite @ref load() and @ref unload() functions, the library
++implements the following callouts:
++
++- @ref dhcp4_srv_configured() - schedules a (re)start of the ICMP IO layer
++- @ref lease4_offer() - handles requests from kea-dhcp4 core to initiate a ping check
++for a candidate lease
++
++The load() function instantiates an instance of @ref isc::ping_check::PingCheckMgr.
++This class is the top level object that provides configuration processing and supervises
++the execution of ping checks.
++
++@section cbPingCheckMTCompatibility Multi-Threading Compatibility
++
++The @c ping_check hooks library requires multi-threading.
++
++*/
+diff --git a/src/hooks/dhcp/ping_check/ping_check_callouts.cc b/src/hooks/dhcp/ping_check/ping_check_callouts.cc
+new file mode 100644
+index 0000000000..ae006359be
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/ping_check_callouts.cc
+@@ -0,0 +1,240 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++#include <config.h>
++
++#include <asiolink/io_service_mgr.h>
++#include <database/audit_entry.h>
++#include <dhcpsrv/cfgmgr.h>
++#include <ping_check_log.h>
++#include <ping_check_mgr.h>
++#include <hooks/hooks.h>
++#include <process/daemon.h>
++#include <string>
++
++namespace isc {
++namespace ping_check {
++
++/// @brief PingCheckMgr singleton
++PingCheckMgrPtr mgr;
++
++} // end of namespace ping_check
++} // end of namespace isc
++
++using namespace isc;
++using namespace isc::asiolink;
++using namespace isc::log;
++using namespace isc::data;
++using namespace isc::db;
++using namespace isc::dhcp;
++using namespace isc::ping_check;
++using namespace isc::hooks;
++using namespace isc::process;
++using namespace std;
++
++// Functions accessed by the hooks framework use C linkage to avoid the name
++// mangling that accompanies use of the C++ compiler as well as to avoid
++// issues related to namespaces.
++extern "C" {
++
++/// @brief dhcp4_srv_configured implementation.
++///
++/// @param handle callout handle.
++int dhcp4_srv_configured(CalloutHandle& handle) {
++ try {
++ SrvConfigPtr server_config;
++ handle.getArgument("server_config", server_config);
++ mgr->updateSubnetConfig(server_config);
++
++ NetworkStatePtr network_state;
++ handle.getArgument("network_state", network_state);
++
++ // Schedule a start of the services. This ensures we begin after
++ // the dust has settled and Kea MT mode has been firmly established.
++ mgr->startService(network_state);
++ IOServiceMgr::instance().registerIOService(mgr->getIOService());
++ } catch (const std::exception& ex) {
++ LOG_ERROR(ping_check_logger, PING_CHECK_DHCP4_SRV_CONFIGURED_FAILED)
++ .arg(ex.what());
++
++ handle.setStatus(isc::hooks::CalloutHandle::NEXT_STEP_DROP);
++ ostringstream os;
++ os << "Error: " << ex.what();
++ string error(os.str());
++ handle.setArgument("error", error);
++ return (1);
++ }
++
++ return (0);
++}
++
++/// @brief cb4_updated callout implementation.
++///
++/// If it detects that any subnets were altered by the update it
++/// replaces the subnet cache contents. If any of the subnets
++/// fail to parse, the error is logged and the function returns
++/// a non-zero value.
++///
++/// @param handle CalloutHandle.
++///
++/// @return 0 upon success, 1 otherwise
++int cb4_updated(CalloutHandle& handle) {
++ AuditEntryCollectionPtr audit_entries;
++ handle.getArgument("audit_entries", audit_entries);
++
++ auto const& object_type_idx = audit_entries->get<AuditEntryObjectTypeTag>();
++ auto range = object_type_idx.equal_range("dhcp4_subnet");
++ if (std::distance(range.first, range.second)) {
++ try {
++ // Server config has been committed, so use the current configuration.
++ mgr->updateSubnetConfig(CfgMgr::instance().getCurrentCfg());
++ } catch (const std::exception& ex) {
++ LOG_ERROR(ping_check_logger, PING_CHECK_CB4_UPDATE_FAILED)
++ .arg(ex.what());
++ return (1);
++ }
++ }
++
++ return (0);
++}
++
++/// @brief lease4_offer callout implementation.
++///
++/// @param handle callout handle.
++int lease4_offer(CalloutHandle& handle) {
++ CalloutHandle::CalloutNextStep status = handle.getStatus();
++ if (status == CalloutHandle::NEXT_STEP_DROP ||
++ status == CalloutHandle::NEXT_STEP_SKIP) {
++ return (0);
++ }
++
++ Pkt4Ptr query4;
++ Lease4Ptr lease4;
++ ParkingLotHandlePtr parking_lot;
++ try {
++ // Get all arguments available for the leases4_committed hook point.
++ // If any of these arguments is not available this is a programmatic
++ // error. An exception will be thrown which will be caught by the
++ // caller and logged.
++ handle.getArgument("query4", query4);
++
++ Lease4CollectionPtr leases4;
++ handle.getArgument("leases4", leases4);
++
++ uint32_t offer_lifetime;
++ handle.getArgument("offer_lifetime", offer_lifetime);
++
++ Lease4Ptr old_lease;
++ handle.getArgument("old_lease", old_lease);
++
++ if (query4->getType() != DHCPDISCOVER) {
++ isc_throw(InvalidOperation, "query4 is not a DHCPDISCOVER");
++ }
++
++ if (!leases4) {
++ isc_throw(InvalidOperation, "leases4 is null");
++ }
++
++ if (!leases4->empty()) {
++ lease4 = (*leases4)[0];
++ }
++
++ if (!lease4) {
++ isc_throw(InvalidOperation, "leases4 is empty, no lease to check");
++ }
++
++ // Fetch the parking lot. If it's empty the server is not employing
++ // parking, which is fine.
++ // Create a reference to the parked packet. This signals that we have a
++ // stake in unparking it.
++ parking_lot = handle.getParkingLotHandlePtr();
++ if (parking_lot) {
++ parking_lot->reference(query4);
++ }
++
++ // Get configuration based on the lease's subnet.
++ auto const& config = mgr->getScopedConfig(lease4);
++
++ // Call shouldPing() to determine if we should ping check or not.
++ // - status == PARK - ping check it
++ // - status == CONTINUE - check not needed, release DHCPOFFER to client
++ // - status == DROP - duplicate check, drop the duplicate DHCPOFFER
++ status = mgr->shouldPing(lease4, query4, old_lease, config);
++ handle.setStatus(status);
++ if (status == CalloutHandle::NEXT_STEP_PARK) {
++ mgr->startPing(lease4, query4, parking_lot, config);
++ } else {
++ // Dereference the parked packet. This releases our stake in it.
++ if (parking_lot) {
++ parking_lot->dereference(query4);
++ }
++ }
++
++ } catch (const std::exception& ex) {
++ LOG_ERROR(ping_check_logger, PING_CHECK_LEASE4_OFFER_FAILED)
++ .arg(query4 ? query4->getLabel() : "<no query>")
++ .arg(lease4 ? lease4->addr_.toText() : "<no lease>")
++ .arg(ex.what());
++ // Make sure we dereference.
++ if (parking_lot) {
++ parking_lot->dereference(query4);
++ }
++
++ return (1);
++ }
++
++ return (0);
++}
++
++/// @brief This function is called when the library is loaded.
++///
++/// @param handle library handle
++/// @return 0 when initialization is successful, 1 otherwise
++int load(LibraryHandle& handle) {
++ try {
++ // Make the hook library only loadable by kea-dhcp4.
++ const string& proc_name = Daemon::getProcName();
++ if (proc_name != "kea-dhcp4") {
++ isc_throw(isc::Unexpected, "Bad process name: " << proc_name
++ << ", expected kea-dhcp4");
++ }
++
++ // Instantiate the manager singleton.
++ mgr.reset(new PingCheckMgr());
++
++ // Configure the manager using the hook library's parameters.
++ ConstElementPtr json = handle.getParameters();
++ mgr->configure(json);
++ } catch (const exception& ex) {
++ LOG_ERROR(ping_check_logger, PING_CHECK_LOAD_ERROR)
++ .arg(ex.what());
++ return (1);
++ }
++
++ LOG_INFO(ping_check_logger, PING_CHECK_LOAD_OK);
++ return (0);
++}
++
++/// @brief This function is called when the library is unloaded.
++///
++/// @return always 0.
++int unload() {
++ if (mgr) {
++ IOServiceMgr::instance().unregisterIOService(mgr->getIOService());
++ mgr.reset();
++ }
++ LOG_INFO(ping_check_logger, PING_CHECK_UNLOAD);
++ return (0);
++}
++
++/// @brief This function is called to retrieve the multi-threading compatibility.
++///
++/// @return 1 which means compatible with multi-threading.
++int multi_threading_compatible() {
++ return (1);
++}
++
++} // end extern "C"
+diff --git a/src/hooks/dhcp/ping_check/ping_check_config.cc b/src/hooks/dhcp/ping_check/ping_check_config.cc
+new file mode 100644
+index 0000000000..a1c69da61e
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/ping_check_config.cc
+@@ -0,0 +1,98 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++#include <config.h>
++
++#include <ping_check_config.h>
++
++using namespace isc;
++using namespace isc::data;
++using namespace isc::dhcp;
++
++namespace isc {
++namespace ping_check {
++
++const data::SimpleKeywords
++PingCheckConfig::CONFIG_KEYWORDS =
++{
++ { "enable-ping-check", Element::boolean },
++ { "min-ping-requests", Element::integer },
++ { "reply-timeout", Element::integer },
++ { "ping-cltt-secs", Element::integer},
++ { "ping-channel-threads", Element::integer}
++};
++
++PingCheckConfig::PingCheckConfig() :
++ enable_ping_check_(true),
++ min_ping_requests_(1),
++ reply_timeout_(100),
++ ping_cltt_secs_(60),
++ ping_channel_threads_(0) {
++}
++
++void
++PingCheckConfig::parse(data::ConstElementPtr config) {
++ // Use a local instance to collect values. This way we
++ // avoid corrupting current values if there are any errors.
++ PingCheckConfig local;
++
++ // Note checkKeywords() will throw DhcpConfigError if there is a problem.
++ SimpleParser::checkKeywords(CONFIG_KEYWORDS, config);
++ ConstElementPtr value = config->get("enable-ping-check");
++ if (value) {
++ local.setEnablePingCheck(value->boolValue());
++ }
++
++ value = config->get("min-ping-requests");
++ if (value) {
++ int64_t val = value->intValue();
++ if (val <= 0) {
++ isc_throw(DhcpConfigError, "invalid min-ping-requests: '"
++ << val << "', must be greater than 0");
++ }
++
++ local.setMinPingRequests(static_cast<size_t>(val));
++ }
++
++ value = config->get("reply-timeout");
++ if (value) {
++ int64_t val = value->intValue();
++ if (val <= 0) {
++ isc_throw(DhcpConfigError, "invalid reply-timeout: '"
++ << val << "', must be greater than 0");
++ }
++
++ local.setReplyTimeout(static_cast<size_t>(val));
++ }
++
++ value = config->get("ping-cltt-secs");
++ if (value) {
++ int64_t val = value->intValue();
++ if (val < 0) {
++ isc_throw(DhcpConfigError, "invalid ping-cltt-secs: '"
++ << val << "', cannot be less than 0");
++ }
++
++ local.setPingClttSecs(static_cast<size_t>(val));
++ }
++
++ value = config->get("ping-channel-threads");
++ if (value) {
++ int64_t val = value->intValue();
++ if (val < 0) {
++ isc_throw(DhcpConfigError, "invalid ping-channel-threads: '"
++ << val << "', cannot be less than 0");
++ }
++
++ local.setPingChannelThreads(static_cast<size_t>(val));
++ }
++
++ // All values good, copy from local instance.
++ *this = local;
++}
++
++} // end of namespace ping_check
++} // end of namespace isc
+diff --git a/src/hooks/dhcp/ping_check/ping_check_config.h b/src/hooks/dhcp/ping_check/ping_check_config.h
+new file mode 100644
+index 0000000000..9fd23eba59
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/ping_check_config.h
+@@ -0,0 +1,134 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++#ifndef PING_CHECK_CONFIG_H
++#define PING_CHECK_CONFIG_H
++
++#include <cc/data.h>
++#include <cc/simple_parser.h>
++
++namespace isc {
++namespace ping_check {
++
++/// @brief Houses the Ping check configuration parameters for a single scope
++/// (e.g. global, subnet...);
++class PingCheckConfig {
++public:
++ /// @brief List of valid parameters and expected types.
++ static const data::SimpleKeywords CONFIG_KEYWORDS;
++
++ /// @brief Constructor
++ PingCheckConfig();
++
++ /// @brief Destructor
++ ~PingCheckConfig() = default;
++
++ /// @brief Extracts member values from an Element::map
++ ///
++ /// @param config map of configuration parameters
++ ///
++ /// @throw BadValue if invalid values are detected.
++ void parse(data::ConstElementPtr config);
++
++ /// @brief Fetches the value of enable-ping-check
++ ///
++ /// @return boolean value of enable-ping-check
++ bool getEnablePingCheck() const {
++ return (enable_ping_check_);
++ };
++
++ /// @brief Sets the value of enable-ping-check
++ ///
++ /// @param value new value for enable-ping-check
++ void setEnablePingCheck(bool value) {
++ enable_ping_check_ = value;
++ }
++
++ /// @brief Fetches the value of min-ping-requests
++ ///
++ /// @return integer value of min-ping-requests
++ uint32_t getMinPingRequests() const {
++ return (min_ping_requests_);
++ };
++
++ /// @brief Sets the value of min-ping-requests
++ ///
++ /// @param value new value for min-ping-requests
++ void setMinPingRequests(uint32_t value) {
++ min_ping_requests_ = value;
++ }
++
++ /// @brief Fetches the value of reply-timeout
++ ///
++ /// @return integer value of reply-timeout
++ uint32_t getReplyTimeout() const {
++ return (reply_timeout_);
++ }
++
++ /// @brief Sets the value of reply-timeout
++ ///
++ /// @param value new value for reply-timeout
++ void setReplyTimeout(uint32_t value) {
++ reply_timeout_ = value;
++ }
++
++ /// @brief Fetches the value of ping-cltt-secs
++ ///
++ /// @return integer value of ping-cltt-secs
++ uint32_t getPingClttSecs() const {
++ return (ping_cltt_secs_);
++ }
++
++ /// @brief Sets the value of ping-cltt-secs
++ ///
++ /// @param value new value for ping-cltt-secs
++ void setPingClttSecs(uint32_t value) {
++ ping_cltt_secs_ = value;
++ }
++
++ /// @brief Fetches the value of ping-channel-threads
++ ///
++ /// @return integer value of ping-channel-threads
++ uint32_t getPingChannelThreads() const {
++ return (ping_channel_threads_);
++ }
++
++ /// @brief Sets the value of ping-channel-threads
++ ///
++ /// @param value new value for ping-channel-threads
++ void setPingChannelThreads(uint32_t value) {
++ ping_channel_threads_ = value;
++ }
++
++private:
++ // @brief True if checking is enabled.
++ bool enable_ping_check_;
++
++ /// @brief minimum number of ECHO REQUESTs sent, without replies received,
++ /// required to declare an address free to offer.
++ uint32_t min_ping_requests_;
++
++ /// @brief maximum number of milliseconds to wait for an ECHO REPLY after
++ /// an ECHO REQUEST has been sent.
++ uint32_t reply_timeout_;
++
++ /// @brief minimum number of seconds that must elapse after the lease's CLTT
++ /// before a ping check will be conducted, when the client is the lease's
++ /// previous owner.
++ uint32_t ping_cltt_secs_;
++
++ /// @brief Number of threads to use if Kea core is multi-threaded.
++ /// Defaults to 0 (for now) which means follow core number of threads.
++ size_t ping_channel_threads_;
++};
++
++/// @brief Defines a shared pointer to a PingCheckConfig.
++typedef boost::shared_ptr<PingCheckConfig> PingCheckConfigPtr;
++
++} // end of namespace ping_check
++} // end of namespace isc
++
++#endif
+diff --git a/src/hooks/dhcp/ping_check/ping_check_log.cc b/src/hooks/dhcp/ping_check/ping_check_log.cc
+new file mode 100644
+index 0000000000..9e877ff9b5
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/ping_check_log.cc
+@@ -0,0 +1,17 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++#include <config.h>
++
++#include <ping_check_log.h>
++
++namespace isc {
++namespace ping_check {
++
++isc::log::Logger ping_check_logger("ping-check-hooks");
++
++} // namespace ping_check
++} // namespace isc
+diff --git a/src/hooks/dhcp/ping_check/ping_check_log.h b/src/hooks/dhcp/ping_check/ping_check_log.h
+new file mode 100644
+index 0000000000..22e0fca953
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/ping_check_log.h
+@@ -0,0 +1,23 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++#ifndef PING_CHECK_LOG_H
++#define PING_CHECK_LOG_H
++
++#include <log/logger_support.h>
++#include <log/macros.h>
++#include <log/log_dbglevels.h>
++#include <ping_check_messages.h>
++#include <iostream>
++
++namespace isc {
++namespace ping_check {
++
++extern isc::log::Logger ping_check_logger;
++
++} // end of namespace ping_check
++} // end of namespace isc
++#endif
+diff --git a/src/hooks/dhcp/ping_check/ping_check_messages.cc b/src/hooks/dhcp/ping_check/ping_check_messages.cc
+new file mode 100644
+index 0000000000..7dea2c2397
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/ping_check_messages.cc
+@@ -0,0 +1,99 @@
++// File created from src/hooks/dhcp/ping_check/ping_check_messages.mes
++
++#include <cstddef>
++#include <log/message_types.h>
++#include <log/message_initializer.h>
++
++extern const isc::log::MessageID PING_CHECK_CB4_UPDATE_FAILED = "PING_CHECK_CB4_UPDATE_FAILED";
++extern const isc::log::MessageID PING_CHECK_CHANNEL_ECHO_REPLY_RECEIVED = "PING_CHECK_CHANNEL_ECHO_REPLY_RECEIVED";
++extern const isc::log::MessageID PING_CHECK_CHANNEL_ECHO_REQUEST_SENT = "PING_CHECK_CHANNEL_ECHO_REQUEST_SENT";
++extern const isc::log::MessageID PING_CHECK_CHANNEL_MALFORMED_PACKET_RECEIVED = "PING_CHECK_CHANNEL_MALFORMED_PACKET_RECEIVED";
++extern const isc::log::MessageID PING_CHECK_CHANNEL_NETWORK_WRITE_ERROR = "PING_CHECK_CHANNEL_NETWORK_WRITE_ERROR";
++extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_CLOSED = "PING_CHECK_CHANNEL_SOCKET_CLOSED";
++extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_CLOSE_ERROR = "PING_CHECK_CHANNEL_SOCKET_CLOSE_ERROR";
++extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_OPENED = "PING_CHECK_CHANNEL_SOCKET_OPENED";
++extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_READ_FAILED = "PING_CHECK_CHANNEL_SOCKET_READ_FAILED";
++extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_WRITE_FAILED = "PING_CHECK_CHANNEL_SOCKET_WRITE_FAILED";
++extern const isc::log::MessageID PING_CHECK_CHANNEL_STOP = "PING_CHECK_CHANNEL_STOP";
++extern const isc::log::MessageID PING_CHECK_CHANNEL_WATCH_SOCKET_CLEAR_ERROR = "PING_CHECK_CHANNEL_WATCH_SOCKET_CLEAR_ERROR";
++extern const isc::log::MessageID PING_CHECK_CHANNEL_WATCH_SOCKET_CLOSE_ERROR = "PING_CHECK_CHANNEL_WATCH_SOCKET_CLOSE_ERROR";
++extern const isc::log::MessageID PING_CHECK_DHCP4_SRV_CONFIGURED_FAILED = "PING_CHECK_DHCP4_SRV_CONFIGURED_FAILED";
++extern const isc::log::MessageID PING_CHECK_DUPLICATE_CHECK = "PING_CHECK_DUPLICATE_CHECK";
++extern const isc::log::MessageID PING_CHECK_LEASE4_OFFER_FAILED = "PING_CHECK_LEASE4_OFFER_FAILED";
++extern const isc::log::MessageID PING_CHECK_LOAD_ERROR = "PING_CHECK_LOAD_ERROR";
++extern const isc::log::MessageID PING_CHECK_LOAD_OK = "PING_CHECK_LOAD_OK";
++extern const isc::log::MessageID PING_CHECK_MGR_CHANNEL_DOWN = "PING_CHECK_MGR_CHANNEL_DOWN";
++extern const isc::log::MessageID PING_CHECK_MGR_LEASE_FREE_TO_USE = "PING_CHECK_MGR_LEASE_FREE_TO_USE";
++extern const isc::log::MessageID PING_CHECK_MGR_NEXT_ECHO_SCHEDULED = "PING_CHECK_MGR_NEXT_ECHO_SCHEDULED";
++extern const isc::log::MessageID PING_CHECK_MGR_RECEIVED_ECHO_REPLY = "PING_CHECK_MGR_RECEIVED_ECHO_REPLY";
++extern const isc::log::MessageID PING_CHECK_MGR_RECEIVED_UNEXPECTED_ECHO_REPLY = "PING_CHECK_MGR_RECEIVED_UNEXPECTED_ECHO_REPLY";
++extern const isc::log::MessageID PING_CHECK_MGR_RECEIVED_UNEXPECTED_UNREACHABLE_MSG = "PING_CHECK_MGR_RECEIVED_UNEXPECTED_UNREACHABLE_MSG";
++extern const isc::log::MessageID PING_CHECK_MGR_RECEIVED_UNREACHABLE_MSG = "PING_CHECK_MGR_RECEIVED_UNREACHABLE_MSG";
++extern const isc::log::MessageID PING_CHECK_MGR_REPLY_RECEIVED_ERROR = "PING_CHECK_MGR_REPLY_RECEIVED_ERROR";
++extern const isc::log::MessageID PING_CHECK_MGR_REPLY_TIMEOUT_EXPIRED = "PING_CHECK_MGR_REPLY_TIMEOUT_EXPIRED";
++extern const isc::log::MessageID PING_CHECK_MGR_SEND_COMPLETED_ERROR = "PING_CHECK_MGR_SEND_COMPLETED_ERROR";
++extern const isc::log::MessageID PING_CHECK_MGR_STARTED = "PING_CHECK_MGR_STARTED";
++extern const isc::log::MessageID PING_CHECK_MGR_STARTED_SINGLE_THREADED = "PING_CHECK_MGR_STARTED_SINGLE_THREADED";
++extern const isc::log::MessageID PING_CHECK_MGR_START_PING_CHECK = "PING_CHECK_MGR_START_PING_CHECK";
++extern const isc::log::MessageID PING_CHECK_MGR_STOPPED = "PING_CHECK_MGR_STOPPED";
++extern const isc::log::MessageID PING_CHECK_MGR_STOPPING = "PING_CHECK_MGR_STOPPING";
++extern const isc::log::MessageID PING_CHECK_MGR_SUBNET_CONFIG_FAILED = "PING_CHECK_MGR_SUBNET_CONFIG_FAILED";
++extern const isc::log::MessageID PING_CHECK_PAUSE_FAILED = "PING_CHECK_PAUSE_FAILED";
++extern const isc::log::MessageID PING_CHECK_PAUSE_ILLEGAL = "PING_CHECK_PAUSE_ILLEGAL";
++extern const isc::log::MessageID PING_CHECK_PAUSE_PERMISSIONS_FAILED = "PING_CHECK_PAUSE_PERMISSIONS_FAILED";
++extern const isc::log::MessageID PING_CHECK_RESUME_FAILED = "PING_CHECK_RESUME_FAILED";
++extern const isc::log::MessageID PING_CHECK_UNEXPECTED_READ_ERROR = "PING_CHECK_UNEXPECTED_READ_ERROR";
++extern const isc::log::MessageID PING_CHECK_UNEXPECTED_WRITE_ERROR = "PING_CHECK_UNEXPECTED_WRITE_ERROR";
++extern const isc::log::MessageID PING_CHECK_UNLOAD = "PING_CHECK_UNLOAD";
++
++namespace {
++
++const char* values[] = {
++ "PING_CHECK_CB4_UPDATE_FAILED", "A subnet ping-check parameters failed to parse after being updated %1",
++ "PING_CHECK_CHANNEL_ECHO_REPLY_RECEIVED", "from address %1, id %2, sequence %3",
++ "PING_CHECK_CHANNEL_ECHO_REQUEST_SENT", "to address %1, id %2, sequence %3",
++ "PING_CHECK_CHANNEL_MALFORMED_PACKET_RECEIVED", "error occurred unpacking message %1, discarding it",
++ "PING_CHECK_CHANNEL_NETWORK_WRITE_ERROR", "occurred trying to ping %1, error %2",
++ "PING_CHECK_CHANNEL_SOCKET_CLOSED", "ICMP socket has been closed.",
++ "PING_CHECK_CHANNEL_SOCKET_CLOSE_ERROR", "an attempt to close the ICMP socket failed %1",
++ "PING_CHECK_CHANNEL_SOCKET_OPENED", "ICMP socket been opened successfully.",
++ "PING_CHECK_CHANNEL_SOCKET_READ_FAILED", "socket read completed with an error %1",
++ "PING_CHECK_CHANNEL_SOCKET_WRITE_FAILED", "socket write completed with an error %1",
++ "PING_CHECK_CHANNEL_STOP", "channel is stopping operations.",
++ "PING_CHECK_CHANNEL_WATCH_SOCKET_CLEAR_ERROR", "an attempt to clear the WatchSocket associated with",
++ "PING_CHECK_CHANNEL_WATCH_SOCKET_CLOSE_ERROR", "an attempt to close the WatchSocket associated with",
++ "PING_CHECK_DHCP4_SRV_CONFIGURED_FAILED", "dhcp4_srv_configured callout failed %1",
++ "PING_CHECK_DUPLICATE_CHECK", "Ping check already in progress for %1, initiated by %2",
++ "PING_CHECK_LEASE4_OFFER_FAILED", "lease4_offer callout failed for query %1, lease address %2, reason %3",
++ "PING_CHECK_LOAD_ERROR", "loading Ping Check hooks library failed %1",
++ "PING_CHECK_LOAD_OK", "Ping Check hooks library loaded successfully.",
++ "PING_CHECK_MGR_CHANNEL_DOWN", "Ping Channel has shutdown, ping checking will be skipped",
++ "PING_CHECK_MGR_LEASE_FREE_TO_USE", "address %1 is free to use for %2",
++ "PING_CHECK_MGR_NEXT_ECHO_SCHEDULED", "for %1, scheduling ECHO_REQUEST %2 of %3",
++ "PING_CHECK_MGR_RECEIVED_ECHO_REPLY", "from %1, id %2, sequence %3",
++ "PING_CHECK_MGR_RECEIVED_UNEXPECTED_ECHO_REPLY", "from %1, id %2, sequence %3 received after reply-timeout expired",
++ "PING_CHECK_MGR_RECEIVED_UNEXPECTED_UNREACHABLE_MSG", "for %1, id %2, sequence %3 received after reply-timeout expired",
++ "PING_CHECK_MGR_RECEIVED_UNREACHABLE_MSG", "for %1, id %2, sequence %3",
++ "PING_CHECK_MGR_REPLY_RECEIVED_ERROR", "an error occurred processing an ICMP reply message %1",
++ "PING_CHECK_MGR_REPLY_TIMEOUT_EXPIRED", "for %1, ECHO REQUEST %2 of %3, reply-timeout %4",
++ "PING_CHECK_MGR_SEND_COMPLETED_ERROR", "an error occurred in the send completion callback %1",
++ "PING_CHECK_MGR_STARTED", "ping channel operations are running, number of threads %1",
++ "PING_CHECK_MGR_STARTED_SINGLE_THREADED", "single-threaded ping channel operations are running",
++ "PING_CHECK_MGR_START_PING_CHECK", "for %1, initiated by %2",
++ "PING_CHECK_MGR_STOPPED", "channel operations have stopped",
++ "PING_CHECK_MGR_STOPPING", "ping channel operations are stopping",
++ "PING_CHECK_MGR_SUBNET_CONFIG_FAILED", "user-context for subnet id %1, contains invalid ping-check %2",
++ "PING_CHECK_PAUSE_FAILED", "Pausing ping channel operations failed %1",
++ "PING_CHECK_PAUSE_ILLEGAL", "Pausing ping channel operations not allowed %1",
++ "PING_CHECK_PAUSE_PERMISSIONS_FAILED", "Permissions check for ping-channel pause failed %1",
++ "PING_CHECK_RESUME_FAILED", "Resuming ping channel operations failed %1",
++ "PING_CHECK_UNEXPECTED_READ_ERROR", "could not start next socket read %1",
++ "PING_CHECK_UNEXPECTED_WRITE_ERROR", "could not start next socket write %1",
++ "PING_CHECK_UNLOAD", "Ping Check hooks library has been unloaded",
++ NULL
++};
++
++const isc::log::MessageInitializer initializer(values);
++
++} // Anonymous namespace
++
+diff --git a/src/hooks/dhcp/ping_check/ping_check_messages.h b/src/hooks/dhcp/ping_check/ping_check_messages.h
+new file mode 100644
+index 0000000000..9326c699e8
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/ping_check_messages.h
+@@ -0,0 +1,50 @@
++// File created from src/hooks/dhcp/ping_check/ping_check_messages.mes
++
++#ifndef PING_CHECK_MESSAGES_H
++#define PING_CHECK_MESSAGES_H
++
++#include <log/message_types.h>
++
++extern const isc::log::MessageID PING_CHECK_CB4_UPDATE_FAILED;
++extern const isc::log::MessageID PING_CHECK_CHANNEL_ECHO_REPLY_RECEIVED;
++extern const isc::log::MessageID PING_CHECK_CHANNEL_ECHO_REQUEST_SENT;
++extern const isc::log::MessageID PING_CHECK_CHANNEL_MALFORMED_PACKET_RECEIVED;
++extern const isc::log::MessageID PING_CHECK_CHANNEL_NETWORK_WRITE_ERROR;
++extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_CLOSED;
++extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_CLOSE_ERROR;
++extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_OPENED;
++extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_READ_FAILED;
++extern const isc::log::MessageID PING_CHECK_CHANNEL_SOCKET_WRITE_FAILED;
++extern const isc::log::MessageID PING_CHECK_CHANNEL_STOP;
++extern const isc::log::MessageID PING_CHECK_CHANNEL_WATCH_SOCKET_CLEAR_ERROR;
++extern const isc::log::MessageID PING_CHECK_CHANNEL_WATCH_SOCKET_CLOSE_ERROR;
++extern const isc::log::MessageID PING_CHECK_DHCP4_SRV_CONFIGURED_FAILED;
++extern const isc::log::MessageID PING_CHECK_DUPLICATE_CHECK;
++extern const isc::log::MessageID PING_CHECK_LEASE4_OFFER_FAILED;
++extern const isc::log::MessageID PING_CHECK_LOAD_ERROR;
++extern const isc::log::MessageID PING_CHECK_LOAD_OK;
++extern const isc::log::MessageID PING_CHECK_MGR_CHANNEL_DOWN;
++extern const isc::log::MessageID PING_CHECK_MGR_LEASE_FREE_TO_USE;
++extern const isc::log::MessageID PING_CHECK_MGR_NEXT_ECHO_SCHEDULED;
++extern const isc::log::MessageID PING_CHECK_MGR_RECEIVED_ECHO_REPLY;
++extern const isc::log::MessageID PING_CHECK_MGR_RECEIVED_UNEXPECTED_ECHO_REPLY;
++extern const isc::log::MessageID PING_CHECK_MGR_RECEIVED_UNEXPECTED_UNREACHABLE_MSG;
++extern const isc::log::MessageID PING_CHECK_MGR_RECEIVED_UNREACHABLE_MSG;
++extern const isc::log::MessageID PING_CHECK_MGR_REPLY_RECEIVED_ERROR;
++extern const isc::log::MessageID PING_CHECK_MGR_REPLY_TIMEOUT_EXPIRED;
++extern const isc::log::MessageID PING_CHECK_MGR_SEND_COMPLETED_ERROR;
++extern const isc::log::MessageID PING_CHECK_MGR_STARTED;
++extern const isc::log::MessageID PING_CHECK_MGR_STARTED_SINGLE_THREADED;
++extern const isc::log::MessageID PING_CHECK_MGR_START_PING_CHECK;
++extern const isc::log::MessageID PING_CHECK_MGR_STOPPED;
++extern const isc::log::MessageID PING_CHECK_MGR_STOPPING;
++extern const isc::log::MessageID PING_CHECK_MGR_SUBNET_CONFIG_FAILED;
++extern const isc::log::MessageID PING_CHECK_PAUSE_FAILED;
++extern const isc::log::MessageID PING_CHECK_PAUSE_ILLEGAL;
++extern const isc::log::MessageID PING_CHECK_PAUSE_PERMISSIONS_FAILED;
++extern const isc::log::MessageID PING_CHECK_RESUME_FAILED;
++extern const isc::log::MessageID PING_CHECK_UNEXPECTED_READ_ERROR;
++extern const isc::log::MessageID PING_CHECK_UNEXPECTED_WRITE_ERROR;
++extern const isc::log::MessageID PING_CHECK_UNLOAD;
++
++#endif // PING_CHECK_MESSAGES_H
+diff --git a/src/hooks/dhcp/ping_check/ping_check_messages.mes b/src/hooks/dhcp/ping_check/ping_check_messages.mes
+new file mode 100644
+index 0000000000..21d407bedf
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/ping_check_messages.mes
+@@ -0,0 +1,229 @@
++# Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++#
++# This Source Code Form is subject to the terms of the Mozilla Public
++# License, v. 2.0. If a copy of the MPL was not distributed with this
++# file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++% PING_CHECK_CB4_UPDATE_FAILED A subnet ping-check parameters failed to parse after being updated %1
++This error message is emitted when an error occurs trying to parse a subnet
++ping-check parameters after the subnet was updated via configuration backend.
++This implies one or more of the parameters is invalid and must be corrected.
++
++% PING_CHECK_CHANNEL_ECHO_REPLY_RECEIVED from address %1, id %2, sequence %3
++Logged at debug log level 50.
++This debug message is issued when an ECHO REPLY has been received on
++the ping channel's ICMP socket.
++
++% PING_CHECK_CHANNEL_ECHO_REQUEST_SENT to address %1, id %2, sequence %3
++Logged at debug log level 50.
++This debug message is issued when an ECHO REQUEST has been written to the
++ping channel's ICMP socket.
++
++% PING_CHECK_CHANNEL_MALFORMED_PACKET_RECEIVED error occurred unpacking message %1, discarding it
++Logged at debug log level 40.
++This debug message is emitted when an ICMP packet has been received
++that could not be unpacked.
++
++% PING_CHECK_CHANNEL_NETWORK_WRITE_ERROR occurred trying to ping %1, error %2
++This error message occurs when an asynchronous write on the ICMP socket
++failed trying to send on the ping target's network. This may mean an interface
++is down or there is a configuration error. The lease address to ping and the
++type of the error are provided in the arguments.
++
++% PING_CHECK_CHANNEL_SOCKET_CLOSED ICMP socket has been closed.
++Logged at debug log level 40.
++This debug message is emitted when the ICMP socket for carrying out
++ping checks has been closed.
++
++% PING_CHECK_CHANNEL_SOCKET_CLOSE_ERROR an attempt to close the ICMP socket failed %1
++This error message is emitted when an unexpected error occurred
++while closing the ping check ICMP socket. The error detail is
++provided as an argument of the log message.
++
++% PING_CHECK_CHANNEL_SOCKET_OPENED ICMP socket been opened successfully.
++Logged at debug log level 40.
++This debug message is emitted when the ICMP socket for carrying out
++ping checks has been successfully opened.
++
++% PING_CHECK_CHANNEL_SOCKET_READ_FAILED socket read completed with an error %1
++This error message occurs when an asynchronous read on the ICMP socket
++failed. The details of the error are provided as an argument of the log
++message.
++
++% PING_CHECK_CHANNEL_SOCKET_WRITE_FAILED socket write completed with an error %1
++This error message occurs when an asynchronous write on the ICMP socket
++failed. The details of the error are provided as an argument of the log
++message.
++
++% PING_CHECK_CHANNEL_STOP channel is stopping operations.
++Logged at debug log level 40.
++This debug message indicates that the channel is stopping operations and
++closing the ICMP socket. The reason for stopping should be apparent in
++preceding log messages.
++
++% PING_CHECK_CHANNEL_WATCH_SOCKET_CLEAR_ERROR an attempt to clear the WatchSocket associated with
++the single-threaded ping-channel failed %1
++This error message is emitted when an unexpected error occurred
++while clearing the ready marker of the WatchSocket associated with
++the ping check channel. This can only occur when running in
++single-threaded mode. The error detail is provided as an argument
++of the log message.
++
++% PING_CHECK_CHANNEL_WATCH_SOCKET_CLOSE_ERROR an attempt to close the WatchSocket associated with
++the single-threaded ping-channel failed %1
++This error message is emitted when an unexpected error occurred
++while closing the WatchSocket associated with the ping check channel.
++This can only occur when running in single-threaded mode.
++The error detail is provided as an argument of the log message.
++
++% PING_CHECK_DHCP4_SRV_CONFIGURED_FAILED dhcp4_srv_configured callout failed %1
++This error message indicates an error during the Ping Check hook
++library dhcp4_srv_configured callout. The details of the error are
++provided as argument of the log message.
++
++% PING_CHECK_DUPLICATE_CHECK Ping check already in progress for %1, initiated by %2
++Logged at debug log level 40.
++This debug message is emitted when a duplicate request to test an address
++is received. When this occurs the duplicate test will be skipped and
++the associated DHCPOFFER will be dropped.
++
++% PING_CHECK_LEASE4_OFFER_FAILED lease4_offer callout failed for query %1, lease address %2, reason %3
++This error message indicates an error during the Ping Check hook
++library lease4_offer callout. The details of the error are
++provided as argument of the log message.
++
++% PING_CHECK_LOAD_ERROR loading Ping Check hooks library failed %1
++This error message indicates an error during loading the Ping Check
++hooks library. The details of the error are provided as argument of
++the log message.
++
++% PING_CHECK_LOAD_OK Ping Check hooks library loaded successfully.
++This info message indicates that the Ping Check hooks library has
++been loaded successfully.
++
++% PING_CHECK_MGR_CHANNEL_DOWN Ping Channel has shutdown, ping checking will be skipped
++This error message is emitted when the underlying ICMP channel
++has stopped due to an unrecoverable error. DHCP service may continue
++to function but without performing ping checks. Prior log messages should
++provide details.
++
++% PING_CHECK_MGR_LEASE_FREE_TO_USE address %1 is free to use for %2
++Logged at debug log level 40.
++This debug message is emitted when ping check has deemed an
++address is free to use. The log arguments detail the lease address
++checked and the query which initiated the check.
++
++% PING_CHECK_MGR_NEXT_ECHO_SCHEDULED for %1, scheduling ECHO_REQUEST %2 of %3
++Logged at debug log level 50.
++This debug message is emitted when the minimum number of ECHO REQUESTs
++is greater than 1 and the next ECHO REQUEST for a given lease address has
++been scheduled.
++
++% PING_CHECK_MGR_RECEIVED_ECHO_REPLY from %1, id %2, sequence %3
++Logged at debug log level 40.
++This debug message is emitted when an ECHO REPLY message has been received.
++The log argument details the source IP address, id, and sequence number of
++the ECHO REPLY.
++
++% PING_CHECK_MGR_RECEIVED_UNEXPECTED_ECHO_REPLY from %1, id %2, sequence %3 received after reply-timeout expired
++Logged at debug log level 50.
++This debug message is emitted when an ECHO REPLY has been received after the
++reply-timeout has expired and is no longer of interest. This may be an errant
++ECHO REPLY or it may indicate that the reply-timeout value is too short. The
++log argument details the source IP address, id, and sequence number of the reply.
++
++% PING_CHECK_MGR_RECEIVED_UNEXPECTED_UNREACHABLE_MSG for %1, id %2, sequence %3 received after reply-timeout expired
++Logged at debug log level 50.
++This debug message is emitted when an UNREACHABLE message has been received
++after the reply-timeout has expired and is no longer of interest. This may
++be an errant message or it may indicate that the reply-timeout value is
++too short.
++
++% PING_CHECK_MGR_RECEIVED_UNREACHABLE_MSG for %1, id %2, sequence %3
++Logged at debug log level 50.
++This debug message is emitted when an UNREACHABLE message has been received.
++The log argument details the target IP address, id, and sequence number from
++the embedded ECHO REQUEST.
++
++% PING_CHECK_MGR_REPLY_RECEIVED_ERROR an error occurred processing an ICMP reply message %1
++This debug message is emitted when an error occurred while processing an inbound
++ICMP message. The log argument describes the specific error.
++
++% PING_CHECK_MGR_REPLY_TIMEOUT_EXPIRED for %1, ECHO REQUEST %2 of %3, reply-timeout %4
++Logged at debug log level 50.
++This debug message is emitted when no reply is received to an
++ECHO REQUEST before the configured timeout value, `reply-timeout`
++was reached. The log arguments provides details.
++
++% PING_CHECK_MGR_SEND_COMPLETED_ERROR an error occurred in the send completion callback %1
++This error message is emitted when an unexpected error occurred after the completion of
++a successful write to the PingChannel socket. The log argument describes the
++specific error.
++
++% PING_CHECK_MGR_STARTED ping channel operations are running, number of threads %1
++This message is emitted when the ping check channel has been opened
++and is ready to process requests. The log argument includes the number of
++threads in the channel's thread pool.
++
++% PING_CHECK_MGR_STARTED_SINGLE_THREADED single-threaded ping channel operations are running
++This message is emitted when the ping check channel has been opened
++and is ready to process requests in single-threaded mode.
++
++% PING_CHECK_MGR_START_PING_CHECK for %1, initiated by %2
++Logged at debug log level 40.
++This debug message is emitted when a ping check for an address
++has been initiated. The log arguments detail the lease address to
++ping and the query which initiated the check.
++
++% PING_CHECK_MGR_STOPPED channel operations have stopped
++This message is emitted when the ping check channel operations
++have been stopped.
++
++% PING_CHECK_MGR_STOPPING ping channel operations are stopping
++Logged at debug log level 40.
++This debug message is emitted when the ping check channel is stopping
++operations, typically due to configuration event or server shutdown.
++
++% PING_CHECK_MGR_SUBNET_CONFIG_FAILED user-context for subnet id %1, contains invalid ping-check %2
++This error message indicates that a subnet was updated via subnet commands
++and its 'user-context' contains invalid 'ping-check' configuration. The
++server will log the error once and then use global ping-check parameters
++for the subnet until the configuration is corrected.
++
++% PING_CHECK_PAUSE_FAILED Pausing ping channel operations failed %1
++This error message is emitted when an unexpected error occurred while
++attempting to pause the ping channel's thread pool. This error is highly
++unlikely and indicates a programmatic issue that should be reported as
++defect.
++
++% PING_CHECK_PAUSE_ILLEGAL Pausing ping channel operations not allowed %1
++This error message is emitted when attempting to pause the ping channel's
++thread pool. This indicates that a channel thread attempted to use a critical
++section which would result in a dead-lock. This error is highly unlikely
++and indicates a programmatic issue that should be reported as a defect.
++
++% PING_CHECK_PAUSE_PERMISSIONS_FAILED Permissions check for ping-channel pause failed %1
++This error message is emitted when an unexpected error occurred while
++validating an attempt to pause the ping channel's thread pool. This error
++is highly unlikely and indicates a programmatic issue that should be
++reported as a defect.
++
++% PING_CHECK_RESUME_FAILED Resuming ping channel operations failed %1
++This error message is emitted when an unexpected error occurred while
++attempting to resume operation of the ping channel's thread pool. This
++error is highly unlikely and indicates a programmatic issue that should
++be reported as defect.
++
++% PING_CHECK_UNEXPECTED_READ_ERROR could not start next socket read %1
++This error message occurs when initiating an asynchronous read on the ICMP
++socket failed in an unexpected fashion. The details of the error are provided
++as an argument of the log message.
++
++% PING_CHECK_UNEXPECTED_WRITE_ERROR could not start next socket write %1
++This error message occurs when initiating an asynchronous write on the ICMP
++socket failed in an unexpected fashion. The details of the error are provided
++as an argument of the log message.
++
++% PING_CHECK_UNLOAD Ping Check hooks library has been unloaded
++This info message indicates that the Ping Check hooks library has been
++unloaded.
+diff --git a/src/hooks/dhcp/ping_check/ping_check_mgr.cc b/src/hooks/dhcp/ping_check/ping_check_mgr.cc
+new file mode 100644
+index 0000000000..cb4f2ee1dc
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/ping_check_mgr.cc
+@@ -0,0 +1,798 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++#include <config.h>
++
++#include <ping_check_mgr.h>
++#include <ping_check_log.h>
++#include <dhcpsrv/cfgmgr.h>
++#include <hooks/hooks_manager.h>
++#include <util/multi_threading_mgr.h>
++#include <util/chrono_time_utils.h>
++
++using namespace isc;
++using namespace isc::asiolink;
++using namespace isc::dhcp;
++using namespace isc::data;
++using namespace isc::hooks;
++using namespace isc::util;
++using namespace std;
++using namespace std::chrono;
++
++namespace ph = std::placeholders;
++
++namespace isc {
++namespace ping_check {
++
++PingCheckMgr::PingCheckMgr()
++ : io_service_(new IOService()), thread_pool_(),
++ store_(new PingContextStore()),
++ channel_(),
++ config_cache_(new ConfigCache()),
++ mutex_(new mutex()),
++ suspended_(false) {
++}
++
++PingCheckMgr::PingCheckMgr(uint32_t num_threads,
++ uint32_t min_echos,
++ uint32_t reply_timeout)
++ : io_service_(new IOService()), thread_pool_(),
++ store_(new PingContextStore()),
++ channel_(),
++ config_cache_(new ConfigCache()),
++ mutex_(new mutex()),
++ suspended_(false) {
++ PingCheckConfigPtr config(new PingCheckConfig());
++ config->setMinPingRequests(min_echos);
++ config->setReplyTimeout(reply_timeout);
++ config->setPingChannelThreads(num_threads);
++ config_cache_->setGlobalConfig(config);
++}
++
++PingCheckMgr::~PingCheckMgr() {
++ stop();
++}
++
++void
++PingCheckMgr::configure(ConstElementPtr params) {
++ if (!params) {
++ isc_throw(dhcp::DhcpConfigError, "params must not be null");
++ return;
++ }
++
++ if (params->getType() != Element::map) {
++ isc_throw(dhcp::DhcpConfigError, "params must be an Element::map");
++ return;
++ }
++
++ PingCheckConfigPtr config(new PingCheckConfig());
++ config->parse(params);
++ config_cache_->setGlobalConfig(config);
++}
++
++void
++PingCheckMgr::updateSubnetConfig(SrvConfigPtr server_config) {
++ // Iterate over subnets and cache configurations for each.
++ ConfigCachePtr local_cache(new ConfigCache());
++ local_cache->setGlobalConfig(config_cache_->getGlobalConfig());
++ auto const& subnets = server_config->getCfgSubnets4()->getAll();
++ for (auto const& subnet : (*subnets)) {
++ auto user_context = subnet->getContext();
++ local_cache->parseAndCacheConfig(subnet->getID(), user_context);
++ }
++
++ // No errors above, replace the existing cache.
++ config_cache_ = local_cache;
++}
++
++const PingCheckConfigPtr
++PingCheckMgr::getGlobalConfig() const {
++ return (config_cache_->getGlobalConfig());
++}
++
++const PingCheckConfigPtr
++PingCheckMgr::getScopedConfig(Lease4Ptr& lease) {
++ if (!lease) {
++ // This really shouldn't happen.
++ isc_throw(InvalidOperation, "PingCheckConfig::getScopedConfig() - lease cannot be empty");
++ }
++
++ auto subnet_id = lease->subnet_id_;
++
++ // If the cache is stale, update it. We do this to catch subnets that have been updated
++ // via subnet_cmds.
++ auto server_config = CfgMgr::instance().getCurrentCfg();
++ auto const& subnet = server_config->getCfgSubnets4()->getBySubnetId(subnet_id);
++ if (!subnet) {
++ // This really shouldn't happen.
++ isc_throw(InvalidOperation, "PingCheckMgr::getScopedConfig() - "
++ "no subnet for id: " << subnet_id
++ << ", for lease address: " << lease->addr_);
++ }
++
++ // If cache is stale flush it and we'll lazy init subnets as we see them.
++ if (subnet->getModificationTime() > config_cache_->getLastFlushTime()) {
++ config_cache_->flush();
++ }
++
++ // If we don't find an entry for this subnet then we haven't seen it
++ // before so parse and cache it. If the subnet doesn't specify ping-check
++ // we cache an empty entry.
++ PingCheckConfigPtr config;
++ if (!config_cache_->findConfig(subnet_id, config)) {
++ auto user_context = subnet->getContext();
++ try {
++ config = config_cache_->parseAndCacheConfig(subnet_id, user_context);
++ } catch (const std::exception& ex) {
++ // We emit and error and then cache an empty entry. This causes us
++ // to log the error once and then default to global settings afterward.
++ // This avoids us relentlessly logging and failing. Remember this
++ // is happening because a subnet was updated with an invalid context via
++ // subnet-cmd.
++ LOG_ERROR(ping_check_logger, PING_CHECK_MGR_SUBNET_CONFIG_FAILED)
++ .arg(subnet_id)
++ .arg(ex.what());
++ config_cache_->cacheConfig(subnet_id, config);
++ }
++ }
++
++ // Return subnet's ping-check config if it specified one, otherwise
++ // return the global config.
++ return (config ? config : config_cache_->getGlobalConfig());
++}
++
++void
++PingCheckMgr::startPing(dhcp::Lease4Ptr& lease, dhcp::Pkt4Ptr& query, hooks::ParkingLotHandlePtr& parking_lot,
++ const PingCheckConfigPtr& config) {
++ if (checkSuspended()) {
++ // Server should not be submitting requests.
++ isc_throw(InvalidOperation, "PingCheckMgr::startPing() - DHCP service is suspended!");
++ }
++
++ if (!channel_ || !channel_->isOpen()) {
++ isc_throw(InvalidOperation, "PingCheckMgr::startPing() - channel isn't open");
++ }
++
++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC,
++ PING_CHECK_MGR_START_PING_CHECK)
++ .arg(lease->addr_)
++ .arg(query->getLabel());
++
++ // Adds a context to the store
++ store_->addContext(lease, query, config->getMinPingRequests(),
++ config->getReplyTimeout(), parking_lot);
++
++ // Posts a call to channel's startSend() and startRead(). This will kick-start perpetual
++ // write and read cycles if they are not already running.
++ if (channel_) {
++ channel_->startSend();
++ channel_->startRead();
++ }
++}
++
++void
++PingCheckMgr::startPing(dhcp::Lease4Ptr& lease, dhcp::Pkt4Ptr& query, hooks::ParkingLotHandlePtr& parking_lot) {
++ startPing(lease, query, parking_lot, getGlobalConfig());
++}
++
++bool
++PingCheckMgr::nextToSend(IOAddress& next) {
++ if (checkSuspended()) {
++ return (false);
++ }
++
++ PingContextPtr context = store_->getNextToSend();
++ if (!context) {
++ return (false);
++ }
++
++ next = context->getTarget();
++ // Transition to sending.
++ context->setState(PingContext::SENDING);
++ store_->updateContext(context);
++
++ return (true);
++}
++
++void
++PingCheckMgr::sendCompleted(const ICMPMsgPtr& echo, bool send_failed) {
++ if (checkSuspended()) {
++ return;
++ }
++
++ try {
++ if (!echo) {
++ isc_throw(BadValue, "PingCheckMgr::sendCompleted() - echo is empty");
++ }
++
++ if (echo->getType() != ICMPMsg::ECHO_REQUEST) {
++ isc_throw(BadValue, "PingCheckMgr::sendCompleted() - message type: "
++ << echo->getType() << " is not an ECHO_REQUEST");
++ }
++
++ // Update the context associated with this ECHO_REQUEST.
++ PingContextPtr context = store_->getContextByAddress(echo->getDestination());
++ if (!context) {
++ isc_throw(Unexpected, "PingCheckMgr::sendCompleted() "
++ " no context found for: " << echo->getDestination());
++ }
++
++ if (send_failed) {
++ // Recoverable error occurred which means we can't get to the target's
++ // network (interface down?). Treat this the same as TARGET UNREACHABLE.
++ finishFree(context);
++ } else {
++ // Transition the context to WAITING_FOR_REPLY.
++ context->beginWaitingForReply();
++ store_->updateContext(context);
++ }
++
++ // Update the expiration timer if necessary.
++ setNextExpiration();
++ } catch (const std::exception& ex) {
++ LOG_ERROR(ping_check_logger, PING_CHECK_MGR_SEND_COMPLETED_ERROR)
++ .arg(ex.what());
++ }
++}
++
++void
++PingCheckMgr::replyReceived(const ICMPMsgPtr& reply) {
++ if (checkSuspended()) {
++ return;
++ }
++
++ try {
++ if (!reply) {
++ isc_throw(BadValue, "PingCheckMgr::replyReceived() - echo is empty");
++ }
++
++ switch (reply->getType()) {
++ case ICMPMsg::ECHO_REPLY:
++ handleEchoReply(reply);
++ break;
++ case ICMPMsg::TARGET_UNREACHABLE:
++ // Extract embedded ECHO REQUEST
++ handleTargetUnreachable(reply);
++ break;
++ default:
++ // Ignore anything else.
++ return;
++ }
++
++ setNextExpiration();
++ } catch (const std::exception& ex) {
++ LOG_ERROR(ping_check_logger, PING_CHECK_MGR_REPLY_RECEIVED_ERROR)
++ .arg(ex.what());
++ }
++}
++
++void
++PingCheckMgr::handleEchoReply(const ICMPMsgPtr& echo_reply) {
++ // Update the context associated with this ECHO_REQUEST.
++ PingContextPtr context = store_->getContextByAddress(echo_reply->getSource());
++ if (!context) {
++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_DETAIL,
++ PING_CHECK_MGR_RECEIVED_UNEXPECTED_ECHO_REPLY)
++ .arg(echo_reply->getSource())
++ .arg(echo_reply->getId())
++ .arg(echo_reply->getSequence());
++ return;
++ }
++
++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC,
++ PING_CHECK_MGR_RECEIVED_ECHO_REPLY)
++ .arg(echo_reply->getSource())
++ .arg(echo_reply->getId())
++ .arg(echo_reply->getSequence());
++
++ context->setState(PingContext::TARGET_IN_USE);
++ store_->updateContext(context);
++
++ // If parking is employed, unpark the query from the parking lot,
++ // and set the offer_address_in_use argument in the callout handle
++ // to true, indicating to the server that the lease should be declined
++ // and the DHCPOFFER discarded.
++ auto parking_lot = context->getParkingLot();
++ if (parking_lot) {
++ auto query = context->getQuery();
++ auto callout_handle = query->getCalloutHandle();
++ callout_handle->setArgument("offer_address_in_use", true);
++ parking_lot->unpark(query);
++ }
++
++ // Remove the context from the store.
++ store_->deleteContext(context);
++}
++
++void
++PingCheckMgr::handleTargetUnreachable(const ICMPMsgPtr& unreachable) {
++ // Unpack the embedded ECHO REQUEST.
++ ICMPMsgPtr embedded_echo;
++ auto payload = unreachable->getPayload();
++ embedded_echo = ICMPMsg::unpack(payload.data(), payload.size());
++
++ // Fetch the context associated with the ECHO_REQUEST.
++ PingContextPtr context = store_->getContextByAddress(embedded_echo->getDestination());
++ if (!context) {
++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_DETAIL,
++ PING_CHECK_MGR_RECEIVED_UNEXPECTED_UNREACHABLE_MSG)
++ .arg(embedded_echo->getDestination())
++ .arg(embedded_echo->getId())
++ .arg(embedded_echo->getSequence());
++ return;
++ }
++
++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_DETAIL,
++ PING_CHECK_MGR_RECEIVED_UNREACHABLE_MSG)
++ .arg(embedded_echo->getDestination())
++ .arg(embedded_echo->getId())
++ .arg(embedded_echo->getSequence());
++
++ // Render the address usable.
++ finishFree(context);
++}
++
++void
++PingCheckMgr::finishFree(const PingContextPtr& context) {
++ context->setState(PingContext::TARGET_FREE);
++ store_->updateContext(context);
++
++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC,
++ PING_CHECK_MGR_LEASE_FREE_TO_USE)
++ .arg(context->getTarget())
++ .arg(context->getQuery()->getLabel());
++
++ // If parking is employed, unpark the query from the parking lot,
++ // and set the offer_address_in_use argument in the callout handle
++ // to false, indicating to the server that the lease is available
++ // and the DHCPOFFER should be sent to the client.
++ auto parking_lot = context->getParkingLot();
++ if (parking_lot) {
++ auto query = context->getQuery();
++ auto callout_handle = query->getCalloutHandle();
++ callout_handle->setArgument("offer_address_in_use", false);
++ parking_lot->unpark(context->getQuery());
++ }
++
++ // Remove the context from the store.
++ store_->deleteContext(context);
++}
++
++void
++PingCheckMgr::channelShutdown() {
++ LOG_ERROR(ping_check_logger, PING_CHECK_MGR_CHANNEL_DOWN);
++ if (io_service_) {
++ // As this is a callback that may be invoked by a channel
++ // thread we post a call to stopService() rather than call
++ // it directly.
++ io_service_->post([&]() { stopService(true); });
++ }
++}
++
++size_t
++PingCheckMgr::processExpiredSince(const TimeStamp& since /* = PingContext::now() */) {
++ auto expired_pings = store_->getExpiredSince(since);
++ size_t more_pings = 0;
++ for (auto const& context : *(expired_pings)) {
++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_DETAIL,
++ PING_CHECK_MGR_REPLY_TIMEOUT_EXPIRED)
++ .arg(context->getTarget())
++ .arg(context->getEchosSent())
++ .arg(context->getMinEchos())
++ .arg(context->getReplyTimeout());
++
++ if (context->getEchosSent() < context->getMinEchos()) {
++ doNextEcho(context);
++ ++more_pings;
++ } else {
++ finishFree(context);
++ }
++ }
++
++ return (more_pings);
++}
++
++void
++PingCheckMgr::doNextEcho(const PingContextPtr& context) {
++ // Position to do another ping by re-entering WAITING_TO_SEND
++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_DETAIL,
++ PING_CHECK_MGR_NEXT_ECHO_SCHEDULED)
++ .arg(context->getTarget())
++ .arg(context->getEchosSent() + 1)
++ .arg(context->getMinEchos());
++
++ context->beginWaitingToSend();
++ store_->updateContext(context);
++}
++
++TimeStamp
++PingCheckMgr::getNextExpiry() {
++ MultiThreadingLock lock(*mutex_);
++ return (next_expiry_);
++}
++
++void
++PingCheckMgr::setNextExpiration() {
++ MultiThreadingLock lock(*mutex_);
++ if (checkSuspendedInternal()) {
++ return;
++ }
++
++ setNextExpirationInternal();
++}
++
++void
++PingCheckMgr::setNextExpirationInternal() {
++ // Find the context that expires soonest.
++ PingContextPtr context = store_->getExpiresNext();
++ if (context) {
++ // if the context's expiry is sooner than current expiry
++ // reschedule expiration timer
++ if ((next_expiry_ == PingContext::EMPTY_TIME()) ||
++ (context->getNextExpiry() < next_expiry_)) {
++ auto now = PingContext::now();
++ auto timeout = duration_cast<milliseconds>(context->getNextExpiry() - now);
++ /// @todo For now we'll impose a 2 ms minimum to avoid thrashing the timer.
++ timeout = (timeout > milliseconds(2) ? timeout : milliseconds(2));
++ next_expiry_ = now + timeout;
++ expiration_timer_->setup(std::bind(&PingCheckMgr::expirationTimedOut,
++ shared_from_this()),
++ timeout.count(), IntervalTimer::ONE_SHOT);
++ }
++ } else {
++ // Nothing waiting to expire. Cancel the timer.
++ cancelExpirationTimerInternal();
++ }
++}
++
++void
++PingCheckMgr::cancelExpirationTimer() {
++ MultiThreadingLock lock(*mutex_);
++ cancelExpirationTimerInternal();
++}
++
++void
++PingCheckMgr::cancelExpirationTimerInternal() {
++ if (expiration_timer_) {
++ expiration_timer_->cancel();
++ next_expiry_ = PingContext::EMPTY_TIME();
++ }
++}
++
++void
++PingCheckMgr::expirationTimedOut() {
++ MultiThreadingLock lock(*mutex_);
++ if (checkSuspendedInternal()) {
++ return;
++ }
++
++ // Process everything that has expired since current time.
++ auto more_pings = processExpiredSince();
++
++ // Update the expiration timer.
++ next_expiry_ = PingContext::EMPTY_TIME();
++ setNextExpirationInternal();
++
++ // In the event there was nothing left to process when timed out,
++ // poke the channel to make sure things are moving.
++ if (more_pings && channel_) {
++ channel_->startSend();
++ channel_->startRead();
++ }
++}
++
++CalloutHandle::CalloutNextStep
++PingCheckMgr::shouldPing(Lease4Ptr& lease, Pkt4Ptr& query,
++ Lease4Ptr& old_lease,
++ const PingCheckConfigPtr& config) {
++
++ // If ping-check is disabled or the channel isn't open,
++ // drop the query from parking and release the offer to the client.
++ if (!config->getEnablePingCheck() || !channel_ || !channel_->isOpen()) {
++ return (CalloutHandle::CalloutNextStep::NEXT_STEP_CONTINUE);
++ }
++
++ // If we're already running check on this address then drop the
++ // query from parking and discard the offer.
++ if (store_->getContextByAddress(lease->addr_)) {
++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC,
++ PING_CHECK_DUPLICATE_CHECK)
++ .arg(lease->addr_)
++ .arg(query->getLabel());
++ return (CalloutHandle::CalloutNextStep::NEXT_STEP_DROP);
++ }
++
++ // If there's a previous lease that belongs to this client and
++ // it was touched by the client less than ping-cltt-secs ago then
++ // no check is needed. Drop the query from parking and release the
++ // offer to the client,
++ if (old_lease && (old_lease->addr_ == lease->addr_)) {
++ if (old_lease->belongsToClient(lease->hwaddr_, lease->client_id_)) {
++ auto now = time(0);
++ if ((now - old_lease->cltt_) < config->getPingClttSecs()) {
++ return (CalloutHandle::CalloutNextStep::NEXT_STEP_CONTINUE);
++ }
++ }
++ }
++
++ // Leave it parked and do the ping check.
++ return (CalloutHandle::CalloutNextStep::NEXT_STEP_PARK);
++}
++
++void
++PingCheckMgr::startService(NetworkStatePtr network_state) {
++ network_state_ = network_state;
++ io_service_->post([&]() { start(); });
++}
++
++bool
++PingCheckMgr::checkSuspended() {
++ MultiThreadingLock lock(*mutex_);
++ return (checkSuspendedInternal());
++}
++
++bool
++PingCheckMgr::checkSuspendedInternal() {
++ if (!network_state_ || network_state_->isServiceEnabled()) {
++ suspended_ = false;
++ } else {
++ if (!suspended_) {
++ suspended_ = true;
++
++ // Flush the context store, dropping parked queries.
++ flush(false);
++ }
++ }
++
++ return (suspended_);
++}
++
++void
++PingCheckMgr::stopService(bool finish_free) {
++ // Pause the thread pool while we flush the store.
++ pause();
++
++ // Flush the context store. If finish_free is true
++ // the flush will treat the remaining context lease
++ // addresses as free to use and unpark them. This
++ // will cause the server to send out the associated
++ // OFFERs. If it's false we just drop them from
++ // the parking lot.
++ flush(finish_free);
++
++ // Stop the thread pool, destroy the channel and the like.
++ stop();
++}
++
++void
++PingCheckMgr::start() {
++ if (MultiThreadingMgr::instance().isTestMode()) {
++ return;
++ }
++ if (!MultiThreadingMgr::instance().getMode()) {
++ startSingleThreaded();
++ return;
++ }
++
++ // We must be in multi-threading mode.
++ // Add critical section callbacks.
++ MultiThreadingMgr::instance().addCriticalSectionCallbacks("PING_CHECK",
++ std::bind(&PingCheckMgr::checkPermissions, this),
++ std::bind(&PingCheckMgr::pause, this),
++ std::bind(&PingCheckMgr::resume, this));
++
++ // Punt if we're already started.
++ if (thread_pool_ && thread_pool_->isStopped()) {
++ isc_throw(InvalidOperation, "PingCheckMgr already started!");
++ }
++
++ try {
++ auto config = config_cache_->getGlobalConfig();
++ auto use_threads = (config->getPingChannelThreads() ? config->getPingChannelThreads()
++ : MultiThreadingMgr::instance().getThreadPoolSize());
++ thread_pool_.reset(new IoServiceThreadPool(IOServicePtr(), use_threads, true));
++ IOServicePtr pool_ios = thread_pool_->getIOService();
++ channel_ = createChannel(pool_ios);
++ channel_->open();
++ expiration_timer_.reset(new IntervalTimer(pool_ios));
++ thread_pool_->run();
++ LOG_INFO(ping_check_logger, PING_CHECK_MGR_STARTED)
++ .arg(use_threads);
++ } catch (const std::exception& ex) {
++ channel_.reset();
++ thread_pool_.reset();
++ isc_throw(Unexpected, "PingCheckMgr::start failed:" << ex.what());
++ }
++}
++
++void
++PingCheckMgr::startSingleThreaded() {
++ try {
++ auto config = config_cache_->getGlobalConfig();
++ channel_ = createChannel(io_service_);
++ channel_->open();
++ expiration_timer_.reset(new IntervalTimer(io_service_));
++ LOG_INFO(ping_check_logger, PING_CHECK_MGR_STARTED_SINGLE_THREADED);
++ } catch (const std::exception& ex) {
++ channel_.reset();
++ isc_throw(Unexpected, "PingCheckMgr::startSingleThreaded() failed:" << ex.what());
++ }
++}
++
++PingChannelPtr
++PingCheckMgr::createChannel(IOServicePtr io_service) {
++ return (PingChannelPtr(new PingChannel(io_service,
++ std::bind(&PingCheckMgr::nextToSend,
++ this, ph::_1),
++ std::bind(&PingCheckMgr::sendCompleted,
++ this, ph::_1, ph::_2),
++ std::bind(&PingCheckMgr::replyReceived,
++ this, ph::_1),
++ std::bind(&PingCheckMgr::channelShutdown,
++ this))));
++}
++
++void
++PingCheckMgr::checkPermissions() {
++ // Since this function is used as CS callback all exceptions must be
++ // suppressed, unlikely though they may be.
++ try {
++ if (thread_pool_) {
++ thread_pool_->checkPausePermissions();
++ }
++ } catch (const isc::MultiThreadingInvalidOperation& ex) {
++ LOG_ERROR(ping_check_logger, PING_CHECK_PAUSE_ILLEGAL)
++ .arg(ex.what());
++ // The exception needs to be propagated to the caller of the
++ // @ref MultiThreadingCriticalSection constructor.
++ throw;
++ } catch (const std::exception& ex) {
++ LOG_ERROR(ping_check_logger, PING_CHECK_PAUSE_PERMISSIONS_FAILED)
++ .arg(ex.what());
++ }
++}
++
++void
++PingCheckMgr::pause() {
++ if (!MultiThreadingMgr::instance().getMode()) {
++ return;
++ }
++
++ // Since this function is used as CS callback all exceptions must be
++ // suppressed, unlikely though they may be.
++ try {
++ // Cancel the expiration timer.
++ cancelExpirationTimer();
++
++ // Pause the thread pool.
++ if (thread_pool_) {
++ thread_pool_->pause();
++ }
++ } catch (const std::exception& ex) {
++ LOG_ERROR(ping_check_logger, PING_CHECK_PAUSE_FAILED)
++ .arg(ex.what());
++ }
++}
++
++void
++PingCheckMgr::resume() {
++ if (!MultiThreadingMgr::instance().getMode()) {
++ return;
++ }
++
++ // Since this function is used as CS callback all exceptions must be
++ // suppressed, unlikely though they may be.
++ try {
++ if (thread_pool_) {
++ thread_pool_->run();
++ }
++
++ // Restore the expiration timer.
++ setNextExpiration();
++ } catch (const std::exception& ex) {
++ LOG_ERROR(ping_check_logger, PING_CHECK_RESUME_FAILED)
++ .arg(ex.what());
++ }
++}
++
++void
++PingCheckMgr::stop() {
++ LOG_DEBUG(ping_check_logger, isc::log::DBGLVL_TRACE_BASIC, PING_CHECK_MGR_STOPPING);
++
++ // Cancel the expiration timer.
++ cancelExpirationTimer();
++
++ if (channel_) {
++ channel_->close();
++ }
++
++ if (thread_pool_) {
++ // Remove critical section callbacks.
++ MultiThreadingMgr::instance().removeCriticalSectionCallbacks("PING_CHECK");
++
++ // Stop the thread pool.
++ thread_pool_->stop();
++
++ thread_pool_->getIOService()->stopAndPoll();
++
++ // Ditch the thread_pool
++ thread_pool_.reset();
++ }
++ // Ditch the timer. It must be destroyed before the thread pool because in
++ // MT it holds a reference to the pool's IOService.
++ expiration_timer_.reset();
++
++ // Get rid of the channel.
++ channel_.reset();
++
++ if (io_service_) {
++ io_service_->stopAndPoll();
++ }
++
++ LOG_INFO(ping_check_logger, PING_CHECK_MGR_STOPPED);
++}
++
++bool
++PingCheckMgr::isRunning() {
++ // In ST mode, running is an open channel.
++ if (!MultiThreadingMgr::instance().getMode()) {
++ return (channel_ && channel_->isOpen());
++ }
++
++ if (thread_pool_) {
++ return (thread_pool_->isRunning());
++ }
++
++ return (false);
++}
++
++bool
++PingCheckMgr::isStopped() {
++ // In ST mode, stopped equates to no channel.
++ if (!MultiThreadingMgr::instance().getMode()) {
++ return (!channel_);
++ }
++
++ if (thread_pool_) {
++ return (thread_pool_->isStopped());
++ }
++
++ return (true);
++}
++
++bool
++PingCheckMgr::isPaused() {
++ if (thread_pool_) {
++ return (thread_pool_->isPaused());
++ }
++
++ return (false);
++}
++
++void
++PingCheckMgr::flush(bool finish_free /* = false */) {
++ if (!store_) {
++ return;
++ }
++
++ // Fetch them all.
++ auto contexts = store_->getAll();
++ for (auto const& context : *contexts) {
++ if (finish_free) {
++ finishFree(context);
++ } else {
++ auto parking_lot = context->getParkingLot();
++ if (parking_lot) {
++ parking_lot->drop(context->getQuery());
++ }
++ }
++ }
++
++ store_->clear();
++}
++
++} // end of namespace ping_check
++} // end of namespace isc
+diff --git a/src/hooks/dhcp/ping_check/ping_check_mgr.h b/src/hooks/dhcp/ping_check/ping_check_mgr.h
+new file mode 100644
+index 0000000000..42d11c1b48
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/ping_check_mgr.h
+@@ -0,0 +1,436 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++#ifndef PING_CHECK_MGR_H
++#define PING_CHECK_MGR_H
++
++#include <asiolink/interval_timer.h>
++#include <asiolink/io_address.h>
++#include <asiolink/io_service.h>
++#include <asiolink/io_service_thread_pool.h>
++#include <cc/data.h>
++#include <cc/simple_parser.h>
++#include <dhcpsrv/srv_config.h>
++#include <hooks/callout_handle.h>
++#include <dhcp/pkt4.h>
++#include <dhcpsrv/lease.h>
++#include <dhcpsrv/network_state.h>
++#include <ping_context_store.h>
++#include <ping_channel.h>
++#include <config_cache.h>
++
++#include <boost/enable_shared_from_this.hpp>
++
++#include <mutex>
++
++namespace isc {
++namespace ping_check {
++
++/// @brief Defines a pointer to a PingContextStore.
++typedef boost::shared_ptr<PingContextStore> PingContextStorePtr;
++
++/// @brief Ping Check Manager.
++///
++/// PinCheckMgr carries out the higher order management of requests for ping
++/// checks from the server. It is a singleton, instantiated when the library
++/// is loaded. It is responsible for:
++/// 1. Parsing and applying configuration.
++/// 2. Maintaining in-memory store of current ping requests (PingContextStore).
++/// 3. Creating and managing the PingChannel through which individual ICMP ECHO/REPLY
++/// cycles are conducted.
++/// 4. When in multi-threaded mode, it creates an IOServiceThread and synchronizes
++/// its state with Kea core MT.
++class PingCheckMgr : public boost::enable_shared_from_this<PingCheckMgr> {
++public:
++ /// @brief Constructor.
++ explicit PingCheckMgr();
++
++ /// @brief Constructor.
++ ///
++ /// This constructor is used in testing. It permits setting some basic behavior
++ /// parameters directly, rather than requiring calls to @c configure().
++ ///
++ /// @param num_threads number of threads to use in the thread pool (0 means follow
++ /// core thread pool size).
++ /// @param min_echos minimum number of ECHO REQUESTs sent without replies
++ /// received required to declare an address free to offer. Defaults to 1,
++ /// must be greater than zero.
++ /// @param reply_timeout maximum number of milliseconds to wait for an
++ /// ECHO REPLY after an ECHO REQUEST has been sent. Defaults to 100.
++ PingCheckMgr(uint32_t num_threads,
++ uint32_t min_echos = 1,
++ uint32_t reply_timeout = 100);
++
++ /// @brief Destructor.
++ virtual ~PingCheckMgr();
++
++ /// @brief Configure the PingCheckMgr.
++ ///
++ /// @param params map containing the hook library parameters.
++ /// @throw BadValue and similar exceptions on error.
++ void configure(data::ConstElementPtr params);
++
++ /// @brief Update the cache of subnet ping check configurations.
++ ///
++ /// Iterates over the subnets in the given server configuration,
++ /// and caches their ping-check configuration.
++ ///
++ /// @param server_config Server configuration containing the
++ /// configured subnets to process.
++ void updateSubnetConfig(dhcp::SrvConfigPtr server_config);
++
++ /// @brief Creates a ping channel instance.
++ ///
++ /// @param io_service IOService that will drive the channel.
++ ///
++ /// @return pointer to the newly created channel.
++ virtual PingChannelPtr createChannel(asiolink::IOServicePtr io_service);
++
++ /// @brief Initiates a ping check for a given lease and its associated
++ /// DHCPDISCOVER packet.
++ ///
++ /// Adds a context to the store and posts a call to @c PingChannel::startSend().
++ ///
++ /// @param lease lease whose address needs to be ping checked.
++ /// @param query parked DHCPDISCOVER associated with the lease.
++ /// @param parking_lot parking lot in which query is parked. If empty,
++ /// parking is assumed to not be employed.
++ /// @param config configuration parameters to employ.
++ void startPing(dhcp::Lease4Ptr& lease, dhcp::Pkt4Ptr& query,
++ hooks::ParkingLotHandlePtr& parking_lot,
++ const PingCheckConfigPtr& config);
++
++ /// @brief Initiates a ping check for a given lease and its associated
++ /// DHCPDISCOVER packet.
++ ///
++ /// Convenience method used in unit tests which uses global
++ /// configuration parameters only.
++ ///
++ /// @param lease lease whose address needs to be ping checked.
++ /// @param query parked DHCPDISCOVER associated with the lease.
++ /// @param parking_lot parking lot in which query is parked. If empty,
++ /// parking is assumed to not be employed.
++ void startPing(dhcp::Lease4Ptr& lease, dhcp::Pkt4Ptr& query,
++ hooks::ParkingLotHandlePtr& parking_lot);
++
++ /// @brief Callback passed to PingChannel to use to retrieve the next
++ /// address to check.
++ ///
++ /// Fetches the context which has been in the WAITING_TO_SEND state the
++ /// longest and returns its lease address.
++ ///
++ /// @param[out] next upon return it will contain the next target address.
++ /// Contents are only meaningful if the function returns true.
++ ///
++ /// @return True another target address exists, false otherwise.
++ virtual bool nextToSend(asiolink::IOAddress& next);
++
++ /// @brief Callback passed to PingChannel to invoke when an ECHO REQUEST
++ /// send has completed.
++ ///
++ /// If the send completed successfully we'll transition the context to
++ /// WAITING_FOR_REPLY, update the context in the store, and the update
++ /// next expiration.
++ ///
++ /// If the send failed, this implies that a recoverable error occurred, such
++ /// as a interface being down and thus, there is currently no way to send
++ /// the ping to the target network. We'll treat this the same as an ICMP
++ /// TARGET_UNREACHABLE and release the OFFER by calling @c finishFree().
++ ///
++ /// @param echo ICMP echo message that as sent.
++ /// @param send_failed True if the send completed with a non-fatal error,
++ /// false otherwise.
++ virtual void sendCompleted(const ICMPMsgPtr& echo, bool send_failed);
++
++ /// @brief Callback passed to PingChannel to invoke when an ICMP
++ /// reply has been received.
++ ///
++ /// If the reply type is an ECHO REQUEST, it is passed to
++ /// handleEchoRequest(), if it is an UNREACHABLE message it
++ /// is passed to handleTargetUnreachable(), any other message
++ /// type is dropped on the floor and the function returns.
++ /// Upon handler completion, it calls setNextExpiration() to
++ /// update the expiration timer.
++ ///
++ /// @param reply ICMP message that was received.
++ virtual void replyReceived(const ICMPMsgPtr& reply);
++
++ /// @brief Process an ECHO REPLY message.
++ ///
++ /// @param echo_reply ICMP ECHO REPLY message to process.
++ void handleEchoReply(const ICMPMsgPtr& echo_reply);
++
++ /// @brief Process an UNREACHABLE message.
++ ///
++ /// @param unreachable ICMP UNREACHABLE message to process.
++ void handleTargetUnreachable(const ICMPMsgPtr& unreachable);
++
++ /// @brief Processes a context whose address has been deemed free to use.
++ ///
++ /// -# Moves the context to TARGET_FREE state
++ /// -# Updates the context in the store
++ /// -# Unparks the query which will release the DHCPOFFER to the client
++ /// -# Invokes the target free callback (do we still need this?)
++ /// -# Deletes the store from the context
++ ///
++ /// @param context context to process.
++ void finishFree(const PingContextPtr& context);
++
++ /// @brief Position a context to do another ping test.
++ ///
++ /// -# Moves the context to WAITING_SEND_STATE
++ /// -# Updates the context in the store
++ ///
++ /// @param context context to process.
++ void doNextEcho(const PingContextPtr& context);
++
++ /// @brief Callback passed to PingChannel to invoke when it shuts down.
++ ///
++ /// Logs the shutdown and then posts a call to @c stopService() to the
++ /// main IOService.
++ virtual void channelShutdown();
++
++ /// @brief Performs expiration processing for contexts whose WAITING_FOR_REPLY
++ /// states expired prior to a given point in time.
++ ///
++ /// expired_pings = store_->getExpiredSince(since)
++ /// for context : expired_pings {
++ /// unpark context->getQuery()
++ /// store_->deleteContext(context)
++ /// }
++ ///
++ /// @param since point in time to select against. Defaults to current time.
++ /// @return number of contexts scheduled for another ping, zero if none.
++ virtual size_t processExpiredSince(const TimeStamp& since = PingContext::now());
++
++ /// @brief Fetches the time at which expiration timer will next expire.
++ ///
++ /// @return TimeStamp containing the next expiration time.
++ TimeStamp getNextExpiry();
++
++ /// @brief Updates the expiration timer (thread safe).
++ ///
++ /// PingContextPtr next = pings->getExpiresNext()
++ /// if next
++ /// reschedule expiration timer for next->getNextExpiry();
++ /// else
++ /// cancel expiration timer
++ virtual void setNextExpiration();
++
++ /// @brief Updates the expiration timer.
++ ///
++ /// PingContextPtr next = pings->getExpiresNext()
++ /// if next
++ /// reschedule expiration timer for next->getNextExpiry();
++ /// else
++ /// cancel expiration timer
++ virtual void setNextExpirationInternal();
++
++ /// @brief Cancels the expiration timer (thread safe).
++ void cancelExpirationTimer();
++
++ /// @brief Cancels the expiration timer.
++ void cancelExpirationTimerInternal();
++
++ /// @brief Callback passed to expiration timer to invoke on timeout.
++ virtual void expirationTimedOut();
++
++ /// @brief Determines whether or not a lease should be ping checked.
++ ///
++ /// Employs the following logic to determine if a ping-check should
++ /// be conducted:
++ ///
++ /// If there's a previous lease that belongs to this client and
++ /// it was touched by the client less than ping-cltt-secs ago,
++ /// then send the offer to the client without ping checking.
++ ///
++ /// Otherwise a ping-check is called for, leave the query parked.
++ ///
++ /// @param lease prospective lease to check.
++ /// @param query DHCPDISCOVER associated with the lease.
++ /// @param old_lease pre-existing lease for this client (if one).
++ /// @param config configuration parameters to employ.
++ ///
++ /// @return CalloutNextStep indicating what should happen next:
++ /// - status == PARK - ping check it
++ /// - status == CONTINUE - check not needed, release DHCPOFFER to client
++ /// - status == DROP - duplicate check, drop the duplicate DHCPOFFER
++ virtual hooks::CalloutHandle::CalloutNextStep shouldPing(dhcp::Lease4Ptr& lease,
++ dhcp::Pkt4Ptr& query,
++ dhcp::Lease4Ptr& old_lease,
++ const PingCheckConfigPtr& config);
++
++ /// @brief Check if the current thread can perform thread pool state
++ /// transition.
++ ///
++ /// @throw MultiThreadingInvalidOperation if the state transition is done on
++ /// any of the worker threads.
++ void checkPermissions();
++
++ /// @brief Performs a deferred start by posting an invocation of @c start()
++ /// to the given IOService.
++ ///
++ /// @param network_state pointer to server's networks state object.
++ void startService(dhcp::NetworkStatePtr network_state);
++
++ /// @brief Shuts down the manager's channel, flushes the store.
++ ///
++ /// This function gracefully winds down operation:
++ ///
++ /// 1. Pauses the thread pool.
++ /// 2. Flushes the context store, either finishing all contexts as free
++ /// or just dropping them from parking, depending on finish_free parameter.
++ /// 3. Stop the thread pool, shutdown the channel.
++ ///
++ /// @param finish_free if true finishFree() will be invoke on all remaining
++ /// contexts in the store, otherwise their queries are simply dropped from
++ /// the parking lot.
++ void stopService(bool finish_free = false);
++
++ /// @brief Start PingChannel operations.
++ ///
++ /// Will start multi-threaded if core MT is enabled, or calls
++ /// @c startSingleThreaded() if core MT is disabled. Creates
++ /// a thread pool with its own IOService, uses that IOService
++ /// when creating the channel.
++ void start();
++
++ /// @brief Start single-threaded PingChannel operations.
++ ///
++ /// Does not create a thread pool. Uses main thread's IOService
++ /// when creating the channel.
++ void startSingleThreaded();
++
++ /// @brief Pause PingChannel operations.
++ ///
++ /// In multi-threaded mode this pauses the thread pool threads, in
++ /// single-threaded mode it does nothing.
++ void pause();
++
++ /// @brief Resume PingChannel operations.
++ ///
++ /// In multi-threaded mode this resumes the thread pool threads, in
++ /// single-threaded mode it does nothing.
++ void resume();
++
++ /// @brief Flushes the ping context store.
++ ///
++ /// This function iterates over the contexts in the store and then
++ /// either invokes finishFree() or drops their queries from parking
++ /// depending upon finish_free parameter. It assumes the operations
++ /// have ceased (i.e. thread pool is not running).
++ ///
++ /// @param finish_free if true finishFree() will be invoke on all remaining
++ /// contexts in the store, otherwise their queries are simply dropped from
++ /// the parking lot.
++ void flush(bool finish_free = false);
++
++ /// @brief Stop PingChannel operations.
++ void stop();
++
++ /// @brief Indicates if the thread pool is running.
++ ///
++ /// @return True if the thread pool exists and it is in the RUNNING state in
++ /// multi-threaded mode, true if the channel exists and is open in single-threaded
++ /// mode, false otherwise.
++ bool isRunning();
++
++ /// @brief Indicates if the thread pool is stopped.
++ ///
++ /// @return True if the thread pool does not exist or it is in the STOPPED
++ /// state in multi-threaded mode, true if the channel does not exist in
++ /// single-threaded mode, false otherwise.
++ bool isStopped();
++
++ /// @brief Indicates if the thread pool is paused.
++ ///
++ /// @return True if the thread pool exists and it is in the PAUSED state,
++ /// false otherwise. Always returns false in single-threaded mode.
++ bool isPaused();
++
++ /// @brief Checks if operations are currently suspended due to NetworkState.
++ ///
++ /// Thread-safe wrapper around checkSuspendedInternal().
++ ///
++ /// @return True if operations are suspended, false otherwise.
++ bool checkSuspended();
++
++ /// @brief Checks if operations are currently suspended due to NetworkState.
++ ///
++ /// If DHCP service is enabled, operations are not suspended and the function
++ /// returns false. Otherwise operations, if not already suspended, are suspended
++ /// by flushing the PingContext store and the function returns true. The queries
++ /// for flushed contexts are dropped from parking and thus their offers discarded.
++ ///
++ /// @return True if operations are suspended, false otherwise.
++ bool checkSuspendedInternal();
++
++ /// @brief Fetches the current, global configuration parameters.
++ ///
++ /// @return PingCheckConfig reference containing the current configuration.
++ const PingCheckConfigPtr getGlobalConfig() const;
++
++ /// @brief Fetches the current, scoped configuration parameters.
++ ///
++ /// @param lease lease for which the parameters are desired.
++ ///
++ /// @return PingCheckConfig reference containing the current configuration.
++ const PingCheckConfigPtr getScopedConfig(dhcp::Lease4Ptr& lease);
++
++ /// @brief Get the hook I/O service.
++ ///
++ /// @return the hook I/O service.
++ isc::asiolink::IOServicePtr getIOService() {
++ return (io_service_);
++ }
++
++ /// @brief Set the hook I/O service.
++ ///
++ /// @param io_service the hook I/O service.
++ void setIOService(isc::asiolink::IOServicePtr io_service) {
++ io_service_ = io_service;
++ }
++
++protected:
++
++ /// @brief The hook I/O service.
++ isc::asiolink::IOServicePtr io_service_;
++
++ /// @brief Thread pool used when running multi-threaded.
++ asiolink::IoServiceThreadPoolPtr thread_pool_;
++
++ /// @brief In-memory store of PingContexts.
++ PingContextStorePtr store_;
++
++ /// @brief Channel that conducts ICMP messaging.
++ PingChannelPtr channel_;
++
++ /// @brief Warehouses parsed global and subnet configuration.
++ ConfigCachePtr config_cache_;
++
++ /// @brief Tracks whether or not the server is processing DHCP packets.
++ dhcp::NetworkStatePtr network_state_;
++
++ /// @brief TimeStamp of the next expiration event.
++ TimeStamp next_expiry_;
++
++ /// @brief Timer which tracks the next expiration event.
++ asiolink::IntervalTimerPtr expiration_timer_;
++
++ /// @brief The mutex used to protect internal state.
++ const boost::scoped_ptr<std::mutex> mutex_;
++
++ /// @brief Indicates whether or not operations have been suspended.
++ bool suspended_;
++};
++
++/// @brief Defines a shared pointer to a PingCheckMgr.
++typedef boost::shared_ptr<PingCheckMgr> PingCheckMgrPtr;
++
++} // end of namespace ping_check
++} // end of namespace isc
++
++#endif
+diff --git a/src/hooks/dhcp/ping_check/ping_context.cc b/src/hooks/dhcp/ping_check/ping_context.cc
+new file mode 100644
+index 0000000000..45e896f948
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/ping_context.cc
+@@ -0,0 +1,237 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++#include <config.h>
++
++#include <ping_context.h>
++#include <ping_check_log.h>
++#include <exceptions/exceptions.h>
++#include <util/chrono_time_utils.h>
++#include <iostream>
++
++using namespace std;
++using namespace isc;
++using namespace isc::asiolink;
++using namespace isc::dhcp;
++using namespace isc::hooks;
++using namespace std::chrono;
++
++namespace isc {
++namespace ping_check {
++
++PingContext::PingContext(Lease4Ptr& lease, Pkt4Ptr& query,
++ uint32_t min_echos /* = 1 */,
++ uint32_t reply_timeout /* = 100 */,
++ ParkingLotHandlePtr& parking_lot /* = EMPTY_LOT() */)
++ : min_echos_(min_echos),
++ reply_timeout_(reply_timeout),
++ echos_sent_(0),
++ last_echo_sent_time_(EMPTY_TIME()),
++ send_wait_start_(EMPTY_TIME()),
++ next_expiry_(EMPTY_TIME()),
++ created_time_(PingContext::now()),
++ lease_(lease),
++ query_(query),
++ state_(NEW),
++ parking_lot_(parking_lot) {
++ if (!lease_) {
++ isc_throw(BadValue, "PingContext ctor - lease cannot be empty");
++ }
++
++ if (!query_) {
++ isc_throw(BadValue, "PingContext ctor - query cannot be empty");
++ }
++
++ if (getTarget() == IOAddress::IPV4_ZERO_ADDRESS()) {
++ isc_throw(BadValue, "PingContext ctor - target address cannot be 0.0.0.0");
++ }
++
++ if (min_echos_ == 0) {
++ isc_throw(BadValue, "PingContext ctor - min_echos must be greater than 0");
++ }
++
++ if (reply_timeout_ == 0) {
++ isc_throw(BadValue, "PingContext ctor - reply_timeout must be greater than 0");
++ }
++}
++
++PingContext::State
++PingContext::stringToState(const std::string& state_str) {
++ if (state_str == "NEW") {
++ return (NEW);
++ }
++
++ if (state_str == "WAITING_TO_SEND") {
++ return (WAITING_TO_SEND);
++ }
++
++ if (state_str == "SENDING") {
++ return (SENDING);
++ }
++
++ if (state_str == "WAITING_FOR_REPLY") {
++ return (WAITING_FOR_REPLY);
++ }
++
++ if (state_str == "TARGET_FREE") {
++ return (TARGET_FREE);
++ }
++
++ if (state_str == "TARGET_IN_USE") {
++ return (TARGET_IN_USE);
++ }
++
++ isc_throw(BadValue, "Invalid PingContext::State: '" << state_str << "'");
++}
++
++TimeStamp
++PingContext::now() {
++ return (time_point_cast<milliseconds>(std::chrono::system_clock::now()));
++}
++
++std::string
++PingContext::stateToString(const PingContext::State& state) {
++ std::string label = "";
++ switch (state) {
++ case NEW:
++ label = "NEW";
++ break;
++ case WAITING_TO_SEND:
++ label = "WAITING_TO_SEND";
++ break;
++ case SENDING:
++ label = "SENDING";
++ break;
++ case WAITING_FOR_REPLY:
++ label = "WAITING_FOR_REPLY";
++ break;
++ case TARGET_FREE:
++ label = "TARGET_FREE";
++ break;
++ case TARGET_IN_USE:
++ label = "TARGET_IN_USE";
++ break;
++ }
++
++ return (label);
++}
++
++const IOAddress& PingContext::getTarget() const {
++ return (lease_->addr_);
++}
++
++uint32_t
++PingContext::getMinEchos() const {
++ return (min_echos_);
++}
++
++void
++PingContext::setMinEchos(uint32_t value) {
++ min_echos_ = value;
++}
++
++uint32_t
++PingContext::getReplyTimeout() const {
++ return (reply_timeout_);
++}
++
++void
++PingContext::setReplyTimeout(uint32_t value) {
++ reply_timeout_ = value;
++}
++
++uint32_t
++PingContext::getEchosSent() const {
++ return (echos_sent_);
++}
++
++void
++PingContext::setEchosSent(uint32_t value) {
++ echos_sent_ = value;
++}
++
++const TimeStamp&
++PingContext::getLastEchoSentTime() const {
++ return (last_echo_sent_time_);
++}
++
++void
++PingContext::setLastEchoSentTime(const TimeStamp& value) {
++ last_echo_sent_time_ = value;
++}
++
++const TimeStamp&
++PingContext::getSendWaitStart() const {
++ return (send_wait_start_);
++}
++
++bool
++PingContext::isWaitingToSend() const {
++ return (state_ == WAITING_TO_SEND);
++}
++
++void
++PingContext::setSendWaitStart(const TimeStamp& value) {
++ send_wait_start_ = value;
++}
++
++const TimeStamp&
++PingContext::getNextExpiry() const {
++ return (next_expiry_);
++}
++
++bool
++PingContext::isWaitingForReply() const {
++ return (state_ == WAITING_FOR_REPLY);
++}
++
++void
++PingContext::setNextExpiry(const TimeStamp& value) {
++ next_expiry_ = value;
++}
++
++const TimeStamp&
++PingContext::getCreatedTime() const {
++ return (created_time_);
++}
++
++PingContext::State
++PingContext::getState() const {
++ return (state_);
++}
++
++void
++PingContext::setState(const PingContext::State& value) {
++ state_ = value;
++}
++
++Pkt4Ptr
++PingContext::getQuery() const {
++ return (query_);
++}
++
++Lease4Ptr
++PingContext::getLease() const {
++ return (lease_);
++}
++
++void
++PingContext::beginWaitingToSend(const TimeStamp& begin_time /* = now() */) {
++ state_ = WAITING_TO_SEND;
++ send_wait_start_ = begin_time;
++}
++
++void
++PingContext::beginWaitingForReply(const TimeStamp& begin_time /* = now() */) {
++ ++echos_sent_;
++ last_echo_sent_time_ = begin_time;
++ next_expiry_ = begin_time + milliseconds(reply_timeout_);
++ state_ = WAITING_FOR_REPLY;
++}
++
++} // end of namespace ping_check
++} // end of namespace isc
++
+diff --git a/src/hooks/dhcp/ping_check/ping_context.h b/src/hooks/dhcp/ping_check/ping_context.h
+new file mode 100644
+index 0000000000..2c5b704a04
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/ping_context.h
+@@ -0,0 +1,280 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++#ifndef PING_CONTEXT_H
++#define PING_CONTEXT_H
++
++#include <dhcp/pkt4.h>
++#include <dhcpsrv/lease.h>
++#include <hooks/parking_lots.h>
++
++#include <chrono>
++
++namespace isc {
++namespace ping_check {
++
++/// @brief Specifies the type for time stamps.
++using TimeStamp = std::chrono::time_point<std::chrono::system_clock>;
++
++/// @brief Embodies the life cycle of a ping check test for a single address
++/// for a single DHCPDISCOVER.
++///
++/// The class uses a state-model to direct the tasks needed to execute one
++/// or more ECHO REQUEST SEND/WAIT FOR REPLY cycles until the address is
++/// either deemed free to offer or in-use and should not be offered. The
++/// number of cycles conducted is dictated by the minimum number of echos
++/// (@c min_echos_) and whether or not either an ECHO REPLY or DESTINATION
++/// UNREACHABLE are received.
++class PingContext {
++public:
++
++ /// @brief Defines PingContext life cycle states
++ enum State {
++ NEW, // Newly created
++ WAITING_TO_SEND, // Waiting to send next ECHO REQUEST
++ SENDING, // Next ECHO REQUEST is being sent
++ WAITING_FOR_REPLY, // ECHO REQUEST sent, Waiting for reply or timeout
++ TARGET_FREE, // Target has been deemed free to offer.
++ TARGET_IN_USE // Target has been deemed in-use, do not offer
++ };
++
++ /// @brief Converts a string to State
++ ///
++ /// @param state_str Upper case string label to convert
++ /// @return State value corresponding to the given string
++ ///
++ /// @throw BadValue if the string is not a valid state label
++ static State stringToState(const std::string& state_str);
++
++ /// @brief Converts a State to a string
++ ///
++ /// @param state State to convert
++ /// @return string label corresponding to the given state
++ static std::string stateToString(const State& state);
++
++ /// @brief Constructor
++ ///
++ /// @param lease pointer to the lease whose address needs to be checked
++ /// @param query DHCPDISCOVER that instigated the check
++ /// @param min_echos minimum number of ECHO REQUESTs sent without replies
++ /// received required to declare an address free to offer. Defaults to 1,
++ /// must be greater than zero.
++ /// @param reply_timeout maximum number of milliseconds to wait for an
++ /// ECHO REPLY after an ECHO REQUEST has been sent. Defaults to 100,
++ /// must be greater than 0.
++ /// @param parking_lot parking lot in which the query is parked. Defaults
++ /// to an empty pointer.
++ ///
++ /// @throw BadValue if either lease or query are empty, or if the lease
++ /// address is 0.0.0.0
++ PingContext(isc::dhcp::Lease4Ptr& lease, isc::dhcp::Pkt4Ptr& query,
++ uint32_t min_echos = 1, uint32_t reply_timeout = 100,
++ isc::hooks::ParkingLotHandlePtr& parking_lot = EMPTY_LOT());
++
++ /// @brief Destructor
++ virtual ~PingContext() = default;
++
++ /// @brief Fetches the current timestamp (UTC/milliseconds precision)
++ ///
++ /// @return current time as a TimeStamp
++ static TimeStamp now();
++
++ /// @brief Fetches an empty timestamp
++ ///
++ /// @return an empty TimeStamp
++ static const TimeStamp& EMPTY_TIME() {
++ static TimeStamp empty_time;
++ return (empty_time);
++ }
++
++ /// @brief Fetches the minimum timestamp
++ ///
++ /// @return the minimum timestamp
++ static const TimeStamp& MIN_TIME() {
++ static TimeStamp min_time = std::chrono::system_clock::time_point::min();
++ return (min_time);
++ }
++
++ /// @brief Fetches an empty parking lot handle
++ ///
++ /// @return an empty ParkingLotHandlePtr
++ static hooks::ParkingLotHandlePtr& EMPTY_LOT() {
++ static hooks::ParkingLotHandlePtr empty_lot(0);
++ return (empty_lot);
++ }
++
++ /// @brief Fetches the IP address that is under test.
++ ///
++ /// @return IP address as an IOAddress
++ const isc::asiolink::IOAddress& getTarget() const;
++
++ /// @brief Fetches the minimum number of ECHO REQUESTs
++ ///
++ /// @return minimum number of echos as a uint32_t
++ uint32_t getMinEchos() const;
++
++ /// @brief Sets the minimum number of ECHO REQUESTs
++ ///
++ /// @param value new value, must be greater than 0
++ ///
++ /// @throw BadValue if the given value is 0
++ void setMinEchos(uint32_t value);
++
++ /// @brief Fetches the reply timeout (milliseconds)
++ ///
++ /// @return reply timeout as a unit32_t
++ uint32_t getReplyTimeout() const;
++
++ /// @brief Sets the reply timeout
++ ///
++ /// @param value new value in milliseconds, must be greater than 0
++ ///
++ /// @throw BadValue if the given value is 0.
++ void setReplyTimeout(uint32_t value);
++
++ /// @brief Fetches the number of ECHO REQUESTs sent.
++ ///
++ /// @return number of echos sent as a unit32_t
++ uint32_t getEchosSent() const;
++
++ /// @brief Sets the number of ECHO REQUESTs sent.
++ ///
++ /// @param value new value
++ void setEchosSent(uint32_t value);
++
++ /// @brief Fetches the timestamp of when the most recent ECHO REQUEST
++ /// was sent
++ ///
++ /// @return time the last echo was sent as a TimeStamp
++ const TimeStamp& getLastEchoSentTime() const;
++
++ /// @brief Sets the timestamp the most recent ECHO REQUEST was sent
++ ///
++ /// @param value new value
++ void setLastEchoSentTime(const TimeStamp& value);
++
++ /// @brief Fetches the time the context went into WAITING_TO_SEND state
++ ///
++ /// The value returned is only meaningful when the context state is WAITING_TO_SEND.
++ ///
++ /// @return send waits start time as a TimeStamp
++ const TimeStamp& getSendWaitStart() const;
++
++ /// @brief Sets the send wait start timestamp
++ ///
++ /// @param value new value
++ void setSendWaitStart(const TimeStamp& value);
++
++ /// @brief Returns true if state is WAITING_TO_SEND
++ ///
++ /// @return True if the context is in WAITING_TO_SEND state
++ bool isWaitingToSend() const;
++
++ /// @brief Fetches the time at which the WAITING_FOR_REPLY state expires(ed)
++ ///
++ /// The value returned is only meaningful when the context state is WAITING_FOR_REPLY.
++ ///
++ /// @return expiration
++ const TimeStamp& getNextExpiry() const;
++
++ /// @brief Sets the timestamp which specifies the time at which the WAITING_FOR_REPLY state expires
++ /// @param value new value
++ void setNextExpiry(const TimeStamp& value);
++
++ /// @brief Returns true if state is WAITING_FOR_REPLY
++ ///
++ /// @return True if the context is in WAITING_TO_REPLY state
++ bool isWaitingForReply() const;
++
++ /// @brief Fetches the time at which the context was created
++ ///
++ /// @return creation time as a TimeStamp
++ const TimeStamp& getCreatedTime() const;
++
++ /// @brief Fetches the current state.
++ ///
++ /// @return current state as PingContext::State
++ State getState() const;
++
++ /// @brief Sets the state.
++ ///
++ /// @param value new state value
++ void setState(const State& value);
++
++ /// @brief Returns the query that instigated this check
++ ///
++ /// @return query as a Pkt4Ptr
++ isc::dhcp::Pkt4Ptr getQuery() const;
++
++ /// @brief Returns the candidate lease whose address is the target to check
++ ///
++ /// @return lease under test as a Lease4Ptr
++ isc::dhcp::Lease4Ptr getLease() const;
++
++ /// @brief Enters WAITING_TO_SEND state
++ ///
++ /// @param begin_time timestamp of when the state began. Defaults to
++ /// time now. Provided for testing purposes.
++ void beginWaitingToSend(const TimeStamp& begin_time = PingContext::now());
++
++ /// @brief Enters WAITING_TO_REPLY state
++ ///
++ /// @param begin_time timestamp of when the state began. Defaults to
++ /// time now. Provided for testing purposes.
++ void beginWaitingForReply(const TimeStamp& begin_time = PingContext::now());
++
++ /// @brief Fetches the parking lot used for this context.
++ ///
++ /// @return Pointer to the parking lot handle or empty if parking is not
++ /// employed.
++ isc::hooks::ParkingLotHandlePtr getParkingLot() {
++ return (parking_lot_);
++ };
++
++private:
++ /// @brief Minimum number of echos to send without receiving a reply
++ /// before giving up
++ uint32_t min_echos_ = 0;
++
++ /// @brief Amount of time (likely in ms) to wait for an echo reply
++ uint32_t reply_timeout_ = 0;
++
++ /// @brief Number of echos sent since instantiation
++ uint32_t echos_sent_ = 0;
++
++ /// @brief Timestamp the most recent echo send completed
++ TimeStamp last_echo_sent_time_;
++
++ /// @brief Timestamp of entry into waiting_to_send
++ TimeStamp send_wait_start_;
++
++ /// @brief Timestamp the most recent echo times out
++ TimeStamp next_expiry_;
++
++ /// @brief Time context was created
++ TimeStamp created_time_;
++
++ /// @brief Candidate lease to check
++ isc::dhcp::Lease4Ptr lease_;
++
++ /// @brief DHCPDISCOVER packet that instigated this check.
++ isc::dhcp::Pkt4Ptr query_;
++
++ /// @brief Current state of this context
++ State state_;
++
++ /// @brief Parking lot where the associated query is parked.
++ /// If empty parking is not being employed.
++ isc::hooks::ParkingLotHandlePtr parking_lot_;
++};
++
++/// @brief Defines a shared pointer to a PingContext.
++typedef boost::shared_ptr<PingContext> PingContextPtr;
++
++} // end of namespace ping_check
++} // end of namespace isc
++
++#endif
+diff --git a/src/hooks/dhcp/ping_check/ping_context_store.cc b/src/hooks/dhcp/ping_check/ping_context_store.cc
+new file mode 100644
+index 0000000000..35712d5afe
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/ping_context_store.cc
+@@ -0,0 +1,144 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++#include <config.h>
++
++#include <ping_context_store.h>
++#include <util/multi_threading_mgr.h>
++
++using namespace std;
++using namespace isc;
++using namespace isc::asiolink;
++using namespace isc::dhcp;
++using namespace isc::hooks;
++using namespace isc::util;
++using namespace std::chrono;
++
++namespace isc {
++namespace ping_check {
++
++PingContextPtr
++PingContextStore::addContext(Lease4Ptr& lease, Pkt4Ptr& query,
++ uint32_t min_echos, uint32_t reply_timeout,
++ ParkingLotHandlePtr& parking_lot) {
++
++ MultiThreadingLock lock(*mutex_);
++ PingContextPtr context;
++ try {
++ context.reset(new PingContext(lease, query, min_echos, reply_timeout, parking_lot));
++ } catch (const std::exception& ex) {
++ isc_throw(BadValue, "PingContextStore::addContext failed: " << ex.what());
++ }
++
++ context->beginWaitingToSend();
++ auto ret = pings_.insert(context);
++ if (ret.second == false) {
++ isc_throw(DuplicateContext, "PingContextStore::addContex: context already exists for: "
++ << lease->addr_);
++ }
++
++ return (context);
++}
++
++void
++PingContextStore::updateContext(const PingContextPtr& context) {
++ MultiThreadingLock lock(*mutex_);
++ auto& index = pings_.get<AddressIndexTag>();
++ auto context_iter = index.find(context->getTarget());
++ if (context_iter == index.end()) {
++ isc_throw(InvalidOperation, "PingContextStore::updateContext failed for address: "
++ << context->getTarget() << ", not in store");
++ }
++
++ // Use replace() to re-index contexts.
++ index.replace(context_iter, PingContextPtr(new PingContext(*context)));
++}
++
++void
++PingContextStore::deleteContext(const PingContextPtr& context) {
++ MultiThreadingLock lock(*mutex_);
++ auto& index = pings_.get<AddressIndexTag>();
++ auto context_iter = index.find(context->getTarget());
++ if (context_iter == index.end()) {
++ // Not there, just return.
++ return;
++ }
++
++ // Remove the context from the store.
++ pings_.erase(context_iter);
++}
++
++PingContextPtr
++PingContextStore::getContextByAddress(const IOAddress& address) {
++ MultiThreadingLock lock(*mutex_);
++ auto const& index = pings_.get<AddressIndexTag>();
++ auto context_iter = index.find(address);
++ return (context_iter == index.end() ? PingContextPtr()
++ : PingContextPtr(new PingContext(**context_iter)));
++}
++
++PingContextPtr
++PingContextStore::getContextByQuery(Pkt4Ptr& query) {
++ MultiThreadingLock lock(*mutex_);
++ auto const& index = pings_.get<QueryIndexTag>();
++ auto context_iter = index.find(query);
++ return (context_iter == index.end() ? PingContextPtr()
++ : PingContextPtr(new PingContext(**context_iter)));
++}
++
++PingContextPtr
++PingContextStore::getNextToSend() {
++ MultiThreadingLock lock(*mutex_);
++ auto const& index = pings_.get<NextToSendIndexTag>();
++ auto context_iter = index.lower_bound(boost::make_tuple(true, PingContext::MIN_TIME()));
++ return (context_iter == index.end() ? PingContextPtr()
++ : PingContextPtr(new PingContext(**context_iter)));
++}
++
++PingContextPtr
++PingContextStore::getExpiresNext() {
++ MultiThreadingLock lock(*mutex_);
++ auto const& index = pings_.get<ExpirationIndexTag>();
++ auto context_iter = index.lower_bound(boost::make_tuple(true, PingContext::now() + milliseconds(1)));
++ return (context_iter == index.end() ? PingContextPtr()
++ : PingContextPtr(new PingContext(**context_iter)));
++}
++
++PingContextCollectionPtr
++PingContextStore::getExpiredSince(const TimeStamp& since) {
++ MultiThreadingLock lock(*mutex_);
++ auto const& index = pings_.get<ExpirationIndexTag>();
++ auto lower_limit = index.lower_bound(boost::make_tuple(true, PingContext::MIN_TIME()));
++ auto upper_limit = index.upper_bound(boost::make_tuple(true, since));
++
++ PingContextCollectionPtr collection(new PingContextCollection());
++ for (auto context_iter = lower_limit; context_iter != upper_limit; ++context_iter) {
++ PingContextPtr context(new PingContext(**context_iter));
++ collection->push_back(context);
++ }
++
++ return (collection);
++}
++
++PingContextCollectionPtr
++PingContextStore::getAll() {
++ MultiThreadingLock lock(*mutex_);
++ auto const& index = pings_.get<AddressIndexTag>();
++ PingContextCollectionPtr collection(new PingContextCollection());
++ for (auto const& context_iter : index) {
++ collection->push_back(PingContextPtr(new PingContext(*context_iter)));
++ }
++
++ return (collection);
++}
++
++void PingContextStore::clear() {
++ MultiThreadingLock lock(*mutex_);
++ pings_.clear();
++}
++
++} // end of namespace ping_check
++} // end of namespace isc
+diff --git a/src/hooks/dhcp/ping_check/ping_context_store.h b/src/hooks/dhcp/ping_check/ping_context_store.h
+new file mode 100644
+index 0000000000..3a7664bfca
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/ping_context_store.h
+@@ -0,0 +1,240 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++#ifndef PING_CONTEXT_STORE_H
++#define PING_CONTEXT_STORE_H
++
++#include <asiolink/io_address.h>
++#include <ping_context.h>
++
++#include <boost/multi_index/indexed_by.hpp>
++#include <boost/multi_index/member.hpp>
++#include <boost/multi_index/mem_fun.hpp>
++#include <boost/multi_index/ordered_index.hpp>
++#include <boost/multi_index_container.hpp>
++#include <boost/multi_index/composite_key.hpp>
++#include <boost/scoped_ptr.hpp>
++
++#include <mutex>
++#include <vector>
++
++namespace isc {
++namespace ping_check {
++
++/// @brief Exception thrown when an attempt was made to add a duplicate context
++class DuplicateContext : public Exception {
++public:
++ DuplicateContext(const char* file, size_t line, const char* what) :
++ isc::Exception(file, line, what) {}
++};
++
++/// @brief Tag for index by target address.
++struct AddressIndexTag { };
++
++/// @brief Tag for index by the query packet.
++struct QueryIndexTag { };
++
++/// @brief Tag for index by send wait start time.
++struct NextToSendIndexTag { };
++
++/// @brief Tag for index by expiration time.
++struct ExpirationIndexTag { };
++
++/// @brief Tag for index by state.
++struct StateIndexTag { };
++
++/// @brief A multi index container holding pointers to PingContexts.
++///
++/// The contexts in the container may be accessed using different indexes:
++/// - using an IPv4 address,
++/// - using a query packet
++/// - using a send wait start time
++/// - using an expiration time
++/// - using a context state
++///
++/// Indexes can be accessed using the index number (from 0 to 2) or a
++/// name tag. It is recommended to use the tags to access indexes as
++/// they do not depend on the order of indexes in the container.
++typedef boost::multi_index_container<
++ // It holds pointers to Lease6 objects.
++ PingContextPtr,
++ boost::multi_index::indexed_by<
++ // Specification of the first index starts here.
++ // This index sorts PingContexts by IPv4 addresses represented as
++ // IOAddress objects.
++ /// @todo Does it need to be ordered or only unique?
++ boost::multi_index::ordered_unique<
++ boost::multi_index::tag<AddressIndexTag>,
++ boost::multi_index::const_mem_fun<PingContext, const isc::asiolink::IOAddress&,
++ &PingContext::getTarget>
++ >,
++
++ // Specification of the second index starts here.
++ // This index sorts contexts by query.
++ boost::multi_index::ordered_unique<
++ boost::multi_index::tag<QueryIndexTag>,
++ boost::multi_index::const_mem_fun<PingContext, isc::dhcp::Pkt4Ptr,
++ &PingContext::getQuery>
++ >,
++
++ // Specification of the third index starts here.
++ // This index sorts contexts by send_wait_start.
++ boost::multi_index::ordered_non_unique<
++ boost::multi_index::tag<NextToSendIndexTag>,
++ boost::multi_index::composite_key<
++ PingContext,
++ // The boolean value specifying if context is waiting to send
++ boost::multi_index::const_mem_fun<PingContext, bool,
++ &PingContext::isWaitingToSend>,
++ // Context expiration time.
++ boost::multi_index::const_mem_fun<PingContext, const TimeStamp&,
++ &PingContext::getSendWaitStart>
++ >
++ >,
++
++ // Specification of the fourth index starts here.
++ // This index sorts contexts by next_expiry.
++ boost::multi_index::ordered_non_unique<
++ boost::multi_index::tag<ExpirationIndexTag>,
++ boost::multi_index::composite_key<
++ PingContext,
++ // The boolean value specifying if context is waiting for a reply
++ boost::multi_index::const_mem_fun<PingContext, bool,
++ &PingContext::isWaitingForReply>,
++ // Context expiration time.
++ boost::multi_index::const_mem_fun<PingContext, const TimeStamp&,
++ &PingContext::getNextExpiry>
++ >
++ >,
++
++ // Specification of the fifth index starts here.
++ // This index sorts contexts by State.
++ boost::multi_index::ordered_non_unique<
++ boost::multi_index::tag<StateIndexTag>,
++ boost::multi_index::const_mem_fun<PingContext, PingContext::State,
++ &PingContext::getState>
++ >
++ >
++> PingContextContainer;
++
++/// @brief Type for a collection of PingContextPtrs.
++typedef std::vector<PingContextPtr> PingContextCollection;
++/// @brief Type for a pointer to a collection of PingContextPtrs.
++typedef boost::shared_ptr<PingContextCollection> PingContextCollectionPtr;
++
++/// @brief Maintains an in-memory store of PingContexts
++///
++/// Provides essential CRUD functions for managing a collection of
++/// PingContexts. Additionally there are finders that can return
++/// contexts by target IP address, instigating query, WAITING_TO_SEND
++/// start time, WAITING_FOR_REPLY expiration time, and context state.
++/// All finders return copies of the contexts found, rather than the
++/// stored context itself.
++class PingContextStore {
++public:
++
++ /// @brief Constructor
++ PingContextStore() : pings_(), mutex_(new std::mutex) {
++ }
++
++ /// @brief Destructor
++ ~PingContextStore() = default;
++
++ /// @brief Creates a new PingContext and adds it to the store
++ ///
++ /// @param lease lease whose address is to be ping checked
++ /// @param query query that instigated the lease
++ /// @param min_echos minimum number of ECHO REQUESTs sent without replies
++ /// received required to declare an address free to offer. Must be
++ /// greater than zero.
++ /// @param reply_timeout maximum number of milliseconds to wait for an
++ /// ECHO REPLY after an ECHO REQUEST has been sent. Must be greater than 0.
++ /// @param parking_lot parking lot in which query is parked. If empty,
++ /// parking is assumed to not be employed.
++ ///
++ /// @return pointer to the newly created context
++ /// @throw DuplicateContext is a context for the lease address already
++ /// exists in the store.
++ PingContextPtr addContext(isc::dhcp::Lease4Ptr& lease,
++ isc::dhcp::Pkt4Ptr& query,
++ uint32_t min_echos,
++ uint32_t reply_timeout,
++ isc::hooks::ParkingLotHandlePtr& parking_lot
++ = PingContext::EMPTY_LOT());
++
++ /// @brief Updates a context in the store.
++ ///
++ /// The context is assumed to already exist in the store.
++ ///
++ /// @param context context to update.
++ ///
++ /// @throw InvalidOperation if PingContext does not exist in the store.
++ void updateContext(const PingContextPtr& context);
++
++ /// @brief Removes the context from the store.
++ ///
++ /// If the context does not exist in the store, it simply returns.
++ ///
++ /// @param context context to delete.
++ void deleteContext(const PingContextPtr& context);
++
++ /// @brief Fetches the context with a given target address
++ ///
++ /// @param address target IP address for which to search
++ ///
++ /// @return pointer to the matching PingContext or an empty pointer if
++ /// not found.
++ PingContextPtr getContextByAddress(const isc::asiolink::IOAddress& address);
++
++ /// @brief Fetches the context with a given query packet
++ ///
++ /// @param query query for which to search
++ ///
++ /// @return pointer to the matching PingContext or an empty pointer if
++ /// not found.
++ PingContextPtr getContextByQuery(isc::dhcp::Pkt4Ptr& query);
++
++ /// @brief Fetches the context in WAITING_TO_SEND with the oldest send wait
++ /// start time.
++ ///
++ /// @return pointer to the matching PingContext or an empty pointer if
++ /// not found.
++ PingContextPtr getNextToSend();
++
++ /// @brief Fetches the context in WAITING_FOR_REPLY with the oldest expiration
++ /// time that has not already passed (i.e. is still in the future)
++ ///
++ /// @return pointer to the matching PingContext or an empty pointer if
++ /// not found.
++ PingContextPtr getExpiresNext();
++
++ /// @brief Fetches the contexts in WAITING_FOR_REPLY that expired since a given time
++ ///
++ /// @param since timestamp to search by. Defaults to current time.
++ ///
++ /// @return a collection of the matching contexts, ordered by expiration time.
++ PingContextCollectionPtr getExpiredSince(const TimeStamp& since = PingContext::now());
++
++ /// @brief Fetches all of the contexts (in order by target)
++ ///
++ /// @return a collection of all contexts in the store.
++ PingContextCollectionPtr getAll();
++
++ /// @brief Removes all contexts from the store.
++ void clear();
++
++private:
++ /// @brief Container instance.
++ PingContextContainer pings_;
++
++ /// @brief The mutex used to protect internal state.
++ const boost::scoped_ptr<std::mutex> mutex_;
++};
++
++} // end of namespace ping_check
++} // end of namespace isc
++
++#endif
+diff --git a/src/hooks/dhcp/ping_check/tests/.gitignore b/src/hooks/dhcp/ping_check/tests/.gitignore
+new file mode 100644
+index 0000000000..7e12f9e5be
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/tests/.gitignore
+@@ -0,0 +1 @@
++ping_check_unittests
+diff --git a/src/hooks/dhcp/ping_check/tests/Makefile.am b/src/hooks/dhcp/ping_check/tests/Makefile.am
+new file mode 100644
+index 0000000000..a8c2ea4d92
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/tests/Makefile.am
+@@ -0,0 +1,70 @@
++SUBDIRS = .
++
++AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
++AM_CPPFLAGS += -I$(top_builddir)/src/hooks/dhcp/ping_check -I$(top_srcdir)/src/hooks/dhcp/ping_check
++AM_CPPFLAGS += $(BOOST_INCLUDES) $(CRYPTO_CFLAGS) $(CRYPTO_INCLUDES)
++AM_CPPFLAGS += -DPING_CHECK_LIB_SO=\"$(abs_top_builddir)/src/hooks/dhcp/ping_check/.libs/libdhcp_ping_check.so\"
++AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\"
++
++AM_CXXFLAGS = $(KEA_CXXFLAGS)
++
++if USE_STATIC_LINK
++AM_LDFLAGS = -static
++endif
++
++# Unit test data files need to get installed.
++EXTRA_DIST =
++
++CLEANFILES = *.gcno *.gcda
++
++TESTS_ENVIRONMENT = $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND)
++
++LOG_COMPILER = $(LIBTOOL)
++AM_LOG_FLAGS = --mode=execute
++
++TESTS =
++if HAVE_GTEST
++TESTS += ping_check_unittests
++
++ping_check_unittests_SOURCES = run_unittests.cc
++ping_check_unittests_SOURCES += icmp_endpoint_unittests.cc
++ping_check_unittests_SOURCES += icmp_socket_unittests.cc
++ping_check_unittests_SOURCES += ping_context_unittests.cc
++ping_check_unittests_SOURCES += ping_context_store_unittests.cc
++ping_check_unittests_SOURCES += icmp_msg_unittests.cc
++ping_check_unittests_SOURCES += ping_test_utils.h
++ping_check_unittests_SOURCES += ping_channel_unittests.cc
++ping_check_unittests_SOURCES += ping_check_mgr_unittests.cc
++ping_check_unittests_SOURCES += ping_check_config_unittests.cc
++ping_check_unittests_SOURCES += config_cache_unittests.cc
++
++ping_check_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) $(LOG4CPLUS_INCLUDES)
++
++ping_check_unittests_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS) $(GTEST_LDFLAGS)
++
++ping_check_unittests_CXXFLAGS = $(AM_CXXFLAGS)
++
++ping_check_unittests_LDADD = $(top_builddir)/src/hooks/dhcp/ping_check/libping_check.la
++ping_check_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la
++ping_check_unittests_LDADD += $(top_builddir)/src/lib/process/libkea-process.la
++ping_check_unittests_LDADD += $(top_builddir)/src/lib/eval/libkea-eval.la
++ping_check_unittests_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la
++ping_check_unittests_LDADD += $(top_builddir)/src/lib/stats/libkea-stats.la
++ping_check_unittests_LDADD += $(top_builddir)/src/lib/config/libkea-cfgclient.la
++ping_check_unittests_LDADD += $(top_builddir)/src/lib/http/libkea-http.la
++ping_check_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la
++ping_check_unittests_LDADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la
++ping_check_unittests_LDADD += $(top_builddir)/src/lib/database/libkea-database.la
++ping_check_unittests_LDADD += $(top_builddir)/src/lib/cc/libkea-cc.la
++ping_check_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
++ping_check_unittests_LDADD += $(top_builddir)/src/lib/dns/libkea-dns++.la
++ping_check_unittests_LDADD += $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la
++ping_check_unittests_LDADD += $(top_builddir)/src/lib/log/libkea-log.la
++ping_check_unittests_LDADD += $(top_builddir)/src/lib/util/libkea-util.la
++ping_check_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
++ping_check_unittests_LDADD += $(LOG4CPLUS_LIBS)
++ping_check_unittests_LDADD += $(CRYPTO_LIBS)
++ping_check_unittests_LDADD += $(BOOST_LIBS)
++ping_check_unittests_LDADD += $(GTEST_LDADD)
++endif
++noinst_PROGRAMS = $(TESTS)
+diff --git a/src/hooks/dhcp/ping_check/tests/config_cache_unittests.cc b/src/hooks/dhcp/ping_check/tests/config_cache_unittests.cc
+new file mode 100644
+index 0000000000..f4e48d6591
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/tests/config_cache_unittests.cc
+@@ -0,0 +1,245 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++/// @file This file contains tests which verify the subnet ping-check
++/// configuration cache.
++
++#include <config.h>
++#include <config_cache.h>
++#include <dhcpsrv/cfgmgr.h>
++#include <hooks/callout_manager.h>
++#include <hooks/hooks.h>
++#include <testutils/gtest_utils.h>
++#include <testutils/multi_threading_utils.h>
++
++#include <boost/date_time/posix_time/posix_time.hpp>
++#include <gtest/gtest.h>
++#include <sstream>
++
++using namespace std;
++using namespace isc;
++using namespace isc::data;
++using namespace isc::dhcp;
++using namespace isc::hooks;
++using namespace isc::ping_check;
++using namespace isc::test;
++using namespace boost::posix_time;
++
++namespace {
++
++/// @brief ConfigCache derivation that allows flush time to be modified.
++class TestConfigCache : public ConfigCache {
++public:
++ /// @brief Constructor
++ TestConfigCache() {
++ }
++
++ /// @brief Destructor
++ virtual ~TestConfigCache() {
++ }
++
++ /// @brief Adjusts the last flush time by the given amount.
++ ///
++ /// @param offset signed value in seconds to add to cache's last
++ /// flush time value
++ void tweakLastFlushTime(int offset) {
++ setModificationTime(getLastFlushTime() + seconds(offset));
++ }
++};
++
++/// @brief Test fixture for testing ConfigCache.
++class ConfigCacheTest : public ::testing::Test {
++public:
++ /// @brief Constructor
++ ConfigCacheTest() {
++ isc::util::MultiThreadingMgr::instance().setMode(false);
++ }
++
++ /// @brief Destructor
++ virtual ~ConfigCacheTest() {
++ }
++
++ /// @brief Verifies construction of a ConfigCache.
++ void testConstruction() {
++ // We use a BaseStampedElement to get the current time to ensure we
++ // are using the same time perspective (currently local) as StampedElements do.
++ BaseStampedElement now;
++ ptime start_time = now.getModificationTime();
++
++ // Create a new cache.
++ TestConfigCache configs;
++ EXPECT_EQ(configs.size(), 0);
++
++ // Verify that last_flush_time_ has been set and that the
++ // cache has no entries.
++ ptime last_flush_time = configs.getLastFlushTime();
++ EXPECT_GE(last_flush_time, start_time);
++
++ // Verify that looking for an entry in an empty cache
++ // gracefully finds nothing.
++ PingCheckConfigPtr fetched_config;
++ EXPECT_FALSE(configs.findConfig(999, fetched_config));
++ EXPECT_FALSE(fetched_config);
++ }
++
++ /// @brief Verifies that invalid user-context config is rejected gracefully.
++ void testInvalidConfig() {
++ // Create a new cache.
++ TestConfigCache configs;
++ EXPECT_EQ(configs.size(), 0);
++
++ // An invalid keyword should fail.
++ std::string json =
++ R"({
++ "ping-check" : {
++ "bogus" : 777
++ }
++ })";
++
++ ConstElementPtr user_context;
++ ASSERT_NO_THROW_LOG(user_context = Element::fromJSON(json));
++
++ ASSERT_THROW_MSG(configs.parseAndCacheConfig(1, user_context), DhcpConfigError,
++ "spurious 'bogus' parameter");
++
++ EXPECT_EQ(configs.size(), 0);
++ }
++
++ /// @brief Verifies that valid user-context supplied config are cached correctly.
++ void testValidConfig() {
++ // Create a new cache.
++ TestConfigCache configs;
++ EXPECT_EQ(configs.size(), 0);
++
++ // A valid config should get cached.
++ std::string json =
++ R"({
++ "ping-check" : {
++ "enable-ping-check" : false,
++ "min-ping-requests" : 2,
++ "reply-timeout" : 375,
++ "ping-cltt-secs" : 120,
++ "ping-channel-threads" : 6
++ }
++ })";
++
++ ConstElementPtr user_context;
++ ASSERT_NO_THROW_LOG(user_context = Element::fromJSON(json));
++
++ // Verify that we cache a valid config.
++ PingCheckConfigPtr config;
++ ASSERT_NO_THROW_LOG(config = configs.parseAndCacheConfig(1, user_context));
++ ASSERT_TRUE(config);
++ EXPECT_EQ(configs.size(), 1);
++
++ // Verify we can retrieve the cached config.
++ PingCheckConfigPtr fetched_config;
++ ASSERT_TRUE(configs.findConfig(1, fetched_config));
++ EXPECT_EQ(fetched_config, config);
++ }
++
++ /// @brief Verifies that an empty config pointer can be cached.
++ void testConfigCacheEmptyConfig() {
++ // Create a new cache.
++ TestConfigCache configs;
++ EXPECT_EQ(configs.size(), 0);
++
++ // Verify that we can cache an empty config pointer.
++ PingCheckConfigPtr no_config;
++ ASSERT_NO_THROW_LOG(configs.cacheConfig(1, no_config));
++ EXPECT_EQ(configs.size(), 1);
++
++ // Verify we can retrieve the cached empty config pointer.
++ PingCheckConfigPtr fetched_config;
++ ASSERT_TRUE(configs.findConfig(1, fetched_config));
++ ASSERT_FALSE(fetched_config);
++ }
++
++ /// @brief Verifies that the cache can be cleared correctly.
++ void testFlushCache() {
++ // Create a new cache.
++ TestConfigCache configs;
++ EXPECT_EQ(configs.size(), 0);
++
++ ptime last_flush_time = configs.getLastFlushTime();
++
++ // Now let's wind the clock back on last_flush_time.
++ configs.tweakLastFlushTime(-1000);
++ EXPECT_LT(configs.getLastFlushTime(), last_flush_time);
++ last_flush_time = configs.getLastFlushTime();
++
++ // Make a simple valid config.
++ std::string json =
++ R"({
++ "ping-check": {
++ "enable-ping-check" : true
++ }
++ })";
++
++ ConstElementPtr user_context;
++ ASSERT_NO_THROW_LOG(user_context = Element::fromJSON(json));
++
++ for (int id = 1; id < 5; ++id) {
++ PingCheckConfigPtr config;
++ ASSERT_NO_THROW_LOG(config = configs.parseAndCacheConfig(id, user_context));
++ ASSERT_TRUE(config);
++ EXPECT_EQ(configs.size(), id);
++ }
++
++ // Verify we can explicitly clear the cache. Should be no entries
++ // and last_flush_time should be updated.
++ configs.flush();
++ EXPECT_GT(configs.getLastFlushTime(), last_flush_time);
++ EXPECT_EQ(configs.size(), 0);
++ }
++};
++
++TEST_F(ConfigCacheTest, construction) {
++ testConstruction();
++}
++
++TEST_F(ConfigCacheTest, constructionMultiThreading) {
++ MultiThreadingTest mt;
++ testConstruction();
++}
++
++TEST_F(ConfigCacheTest, invalidConfig) {
++ testInvalidConfig();
++}
++
++TEST_F(ConfigCacheTest, invalidConfigMultiThreading) {
++ MultiThreadingTest mt;
++ testInvalidConfig();
++}
++
++TEST_F(ConfigCacheTest, validConfig) {
++ testValidConfig();
++}
++
++TEST_F(ConfigCacheTest, validConfigMultiThreading) {
++ MultiThreadingTest mt;
++ testValidConfig();
++}
++
++TEST_F(ConfigCacheTest, configCacheEmptyConfig) {
++ testConfigCacheEmptyConfig();
++}
++
++TEST_F(ConfigCacheTest, configCacheEmptyConfigMultiThreading) {
++ MultiThreadingTest mt;
++ testConfigCacheEmptyConfig();
++}
++
++TEST_F(ConfigCacheTest, flushCache) {
++ testFlushCache();
++}
++
++TEST_F(ConfigCacheTest, flushCacheMultiThreading) {
++ MultiThreadingTest mt;
++ testFlushCache();
++}
++
++} // end of anonymous namespace
+diff --git a/src/hooks/dhcp/ping_check/tests/icmp_endpoint_unittests.cc b/src/hooks/dhcp/ping_check/tests/icmp_endpoint_unittests.cc
+new file mode 100644
+index 0000000000..e9ed8dcb9b
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/tests/icmp_endpoint_unittests.cc
+@@ -0,0 +1,44 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++#include <config.h>
++#include <asiolink/asio_wrapper.h>
++#include <asiolink/io_address.h>
++#include <icmp_endpoint.h>
++
++#include <gtest/gtest.h>
++
++#include <string>
++
++using namespace isc::asiolink;
++using namespace isc::ping_check;
++using namespace std;
++
++// This test checks that the endpoint can manage its own internal
++// boost::asio::ip::icmp::endpoint object for IPv4.
++TEST(ICMPEndpointTest, v4Address) {
++ const string test_address("192.0.2.1");
++
++ IOAddress address(test_address);
++ ICMPEndpoint endpoint(address);
++
++ EXPECT_TRUE(address == endpoint.getAddress());
++ EXPECT_EQ(static_cast<short>(IPPROTO_ICMP), endpoint.getProtocol());
++ EXPECT_EQ(AF_INET, endpoint.getFamily());
++}
++
++// This test checks that the endpoint can manage its own internal
++// boost::asio::ip::icmp::endpoint object for IPv6.
++TEST(ICMPEndpointTest, v6Address) {
++ const string test_address("2001:db8::1235");
++
++ IOAddress address(test_address);
++ ICMPEndpoint endpoint(address);
++
++ EXPECT_TRUE(address == endpoint.getAddress());
++ EXPECT_EQ(static_cast<short>(IPPROTO_ICMPV6), endpoint.getProtocol());
++ EXPECT_EQ(AF_INET6, endpoint.getFamily());
++}
+diff --git a/src/hooks/dhcp/ping_check/tests/icmp_msg_unittests.cc b/src/hooks/dhcp/ping_check/tests/icmp_msg_unittests.cc
+new file mode 100644
+index 0000000000..36c7056840
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/tests/icmp_msg_unittests.cc
+@@ -0,0 +1,172 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++/// @file This file contains tests which exercise the ICMPMsg class.
++
++#include <config.h>
++#include <icmp_msg.h>
++#include <asiolink/io_address.h>
++#include <testutils/gtest_utils.h>
++#include <util/str.h>
++
++#include <gtest/gtest.h>
++#include <list>
++
++using namespace std;
++using namespace isc;
++using namespace isc::asiolink;
++using namespace isc::ping_check;
++
++namespace {
++
++// Verifies accessors.
++TEST(ICMPMsgTest, basics) {
++ ICMPMsgPtr msg(new ICMPMsg());
++
++ msg->setType(ICMPMsg::ECHO_REPLY);
++ EXPECT_EQ(ICMPMsg::ECHO_REPLY, msg->getType());
++
++ msg->setCode(77);
++ EXPECT_EQ(77, msg->getCode());
++
++ msg->setChecksum(0x8899);
++ EXPECT_EQ(0x8899, msg->getChecksum());
++
++ msg->setId(0x1122);
++ EXPECT_EQ(0x1122, msg->getId());
++
++ msg->setSequence(0x3344);
++ EXPECT_EQ(0x3344, msg->getSequence());
++
++ msg->setSource(IOAddress("192.0.2.1"));
++ EXPECT_EQ(IOAddress("192.0.2.1"), msg->getSource());
++
++ msg->setDestination(IOAddress("192.0.2.2"));
++ EXPECT_EQ(IOAddress("192.0.2.2"), msg->getDestination());
++
++ std::vector<uint8_t> payload{ 0x55, 0x66, 0x77, 0x88, 0x99 };
++ msg->setPayload(payload.data(), payload.size());
++ EXPECT_EQ(payload, msg->getPayload());
++}
++
++// Verifies that a valid ECHO REPLY message can be unpacked.
++TEST(ICMPMsgTest, unpackValidEchoReply) {
++ // Create wire data for a valid ECHO REPLY.
++ std::string echo_reply =
++ "45:00:00:30:73:8a:00:00:40:01:a0:ff:b2:10:01:19:b2:10:01:0a:"
++ "00:00:33:11:55:66:77:88:"
++ "00:00:00:00:00:00:00:00:"
++ "00:00:00:00:00:00:00:00:"
++ "00:00:00:00";
++
++ std::vector<uint8_t> wire_data;
++ ASSERT_NO_THROW_LOG(util::str::decodeSeparatedHexString(echo_reply, ":", wire_data));
++
++ // Unpack the wire data.
++ ICMPMsgPtr msg;
++ ASSERT_NO_THROW_LOG(msg = ICMPMsg::unpack(wire_data.data(), wire_data.size()));
++ ASSERT_TRUE(msg);
++
++ // Verify the reply contents.
++ EXPECT_EQ(ICMPMsg::ECHO_REPLY, msg->getType());
++ EXPECT_EQ(0, msg->getCode());
++ EXPECT_EQ(0x3311, msg->getChecksum());
++ EXPECT_EQ(0x5566, msg->getId());
++ EXPECT_EQ(0x7788, msg->getSequence());
++ EXPECT_EQ(IOAddress("178.16.1.25"), msg->getSource());
++ EXPECT_EQ(IOAddress("178.16.1.10"), msg->getDestination());
++
++ std::vector<uint8_t> payload{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
++ EXPECT_EQ(payload, msg->getPayload());
++}
++
++// Verifies that a valid DESTINATION UNREACHABLE message can be unpacked.
++TEST(ICMPMsgTest, unpackValidUnreachable) {
++ // Valid destination unreachable message. Payload is the original
++ // ECHO request.
++ std::string unreachable =
++ "45:c0:00:4c:31:b3:00:00:40:01:e2:09:b2:10:01:0a:b2:10:01:0a:"
++ "03:01:fc:fe:00:00:00:00:"
++ "45:00:00:30:e3:e2:40:00:40:01:f0:5c:"
++ "b2:10:01:0a:b2:10:01:63:08:00:2b:11:"
++ "55:66:77:88:00:00:00:00:00:00:00:00:"
++ "00:00:00:00:00:00:00:00:00:00:00:00";
++
++ // Create the wire data.
++ std::vector<uint8_t> wire_data;
++ ASSERT_NO_THROW_LOG(util::str::decodeSeparatedHexString(unreachable, ":", wire_data));
++
++ // Unpack the outer message.
++ ICMPMsgPtr msg;
++ ASSERT_NO_THROW_LOG(msg = ICMPMsg::unpack(wire_data.data(), wire_data.size()));
++ ASSERT_TRUE(msg);
++
++ // Verify its contents.
++ EXPECT_EQ(ICMPMsg::TARGET_UNREACHABLE, msg->getType());
++ EXPECT_EQ(1, msg->getCode());
++ EXPECT_EQ(0xfcfe, msg->getChecksum());
++ EXPECT_EQ(0, msg->getId());
++ EXPECT_EQ(0, msg->getSequence());
++ EXPECT_EQ(IOAddress("178.16.1.10"), msg->getSource());
++ EXPECT_EQ(IOAddress("178.16.1.10"), msg->getDestination());
++
++ // Now unpack the original ECHO from the outer message payload.
++ std::vector<uint8_t> payload(wire_data.begin() + 28, wire_data.end());
++ EXPECT_EQ(payload, msg->getPayload());
++
++ ICMPMsgPtr payload_msg;
++ ASSERT_NO_THROW_LOG(payload_msg = ICMPMsg::unpack(payload.data(), payload.size()));
++ ASSERT_TRUE(payload_msg);
++
++ // Verify the original ECHO contents.
++ EXPECT_EQ(ICMPMsg::ECHO_REQUEST, payload_msg->getType());
++ EXPECT_EQ(0, payload_msg->getCode());
++ EXPECT_EQ(0x2b11, payload_msg->getChecksum());
++ EXPECT_EQ(0x5566, payload_msg->getId());
++ EXPECT_EQ(0x7788, payload_msg->getSequence());
++ EXPECT_EQ(IOAddress("178.16.1.10"), payload_msg->getSource());
++ EXPECT_EQ(IOAddress("178.16.1.99"), payload_msg->getDestination());
++}
++
++// Verifies the malformed packets are detected.
++TEST(ICMPMsgTest, unpackInValidPackets) {
++ // Contains a test scenario.
++ struct Scenario {
++ // Wire data to submit to unpack.
++ std::string wire_data_;
++ // Expected exception message.
++ std::string error_msg_;
++ };
++
++ // List of scenarios to test.
++ std::list<Scenario> scenarios = {
++ {
++ // Truncated IP header
++ "45:c0:00:4c:31:b3:00:00:40:01:e2:09:b2",
++ "ICMPMsg::unpack - truncated ip header, length: 13"
++ },
++ {
++ // Truncated packet
++ "45:c0:00:4c:31:b3:00:00:40:01:e2:09:b2:10:01:0a:b2:10:01:0a:"
++ "03:01:fc:fe:00:00:00:00:"
++ "45:00:00:30:e3:e2:40:00:40:01:f0:5c",
++ "ICMPMsg::truncated packet? length: 40, hlen: 20"
++ }
++
++ };
++
++ // Iterate over scenarios.
++ for (auto const& scenario : scenarios) {
++ // Create the wire data.
++ std::vector<uint8_t> wire_data;
++ ASSERT_NO_THROW_LOG(util::str::decodeSeparatedHexString(scenario.wire_data_, ":", wire_data));
++ ASSERT_THROW_MSG(ICMPMsg::unpack(wire_data.data(), wire_data.size()), BadValue, scenario.error_msg_);
++ }
++}
++
++/// @todo YOU NEED some round trip tests that test packing!
++
++} // end of anonymous namespace
+diff --git a/src/hooks/dhcp/ping_check/tests/icmp_socket_unittests.cc b/src/hooks/dhcp/ping_check/tests/icmp_socket_unittests.cc
+new file mode 100644
+index 0000000000..2394b360ca
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/tests/icmp_socket_unittests.cc
+@@ -0,0 +1,380 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++/// \brief Test of ICMPSocket
++///
++/// Tests the functionality of a ICMPSocket by working through an open-send-
++/// receive-close sequence and checking that the asynchronous notifications
++/// work.
++
++#include <config.h>
++#include <asiolink/asio_wrapper.h>
++#include <asiolink/interval_timer.h>
++#include <asiolink/io_address.h>
++#include <asiolink/io_service.h>
++#include <icmp_socket.h>
++#include <icmp_msg.h>
++#include <exceptions/exceptions.h>
++#include <util/buffer.h>
++#include <testutils/gtest_utils.h>
++
++#include <boost/shared_ptr.hpp>
++#include <boost/enable_shared_from_this.hpp>
++#include <boost/date_time/posix_time/posix_time.hpp>
++#include <gtest/gtest.h>
++
++#include <string>
++#include <arpa/inet.h>
++#include <netinet/in.h>
++#include <sys/types.h>
++#include <sys/socket.h>
++#include <algorithm>
++#include <cstdlib>
++#include <cstddef>
++#include <list>
++#include <vector>
++#include <unistd.h>
++
++#include <netinet/ip.h>
++#include <netinet/ip_icmp.h>
++
++using namespace isc;
++using namespace boost::asio;
++using namespace boost::posix_time;
++using namespace isc::asiolink;
++using namespace isc::ping_check;
++using namespace isc::util;
++using namespace std;
++
++namespace ph = std::placeholders;
++
++namespace {
++
++/// @brief Test timeout (ms).
++const long TEST_TIMEOUT = 10000;
++
++/// @brief Type of the function implementing a callback invoked by the
++/// @c SocketCallback functor.
++typedef std::function<void(boost::system::error_code ec, size_t length)>
++ SocketCallbackFunction;
++
++/// @brief Callback class for socket IO operations
++///
++/// An instance of this object is passed to the asynchronous I/O functions
++/// and the operator() method is called when when an asynchronous I/O
++/// completes. The arguments to the completion callback are stored for later
++/// retrieval.
++class SocketCallback {
++public:
++
++ /// @brief Structure that houses callback invocation data.
++ struct PrivateData {
++ PrivateData() :
++ error_code_(), length_(0), called_(false), name_("")
++ {}
++
++ boost::system::error_code error_code_; ///< Completion error code
++ size_t length_; ///< Number of bytes transferred
++ bool called_; ///< Set true when callback called
++ std::string name_; ///< Which of the objects this is
++ };
++
++ /// @brief Constructor
++ ///
++ /// Constructs the object. It also creates the data member pointed to by
++ /// a shared pointer. When used as a callback object, this is copied as it
++ /// is passed into the asynchronous function. This means that there are two
++ /// objects and inspecting the one we passed in does not tell us anything.
++ ///
++ /// Therefore we use a boost::shared_ptr. When the object is copied, the
++ /// shared pointer is copied, which leaves both objects pointing to the same
++ /// data.
++ ///
++ /// @param which Which of the two callback objects this is
++ explicit SocketCallback(const std::string& which) : data_(new PrivateData())
++ {
++ setName(which);
++ }
++
++ /// @brief Destructor
++ ///
++ /// No code needed, destroying the shared pointer destroys the private data.
++ virtual ~SocketCallback()
++ {}
++
++ /// @brief Clears the current values of invocation data members.
++ void clear() {
++ setCode(0);
++ setLength(0);
++ setCalled(false);
++ }
++
++ /// @brief Callback Function
++ ///
++ /// Called when an asynchronous I/O completes, this stores the
++ /// completion error code and the number of bytes transferred.
++ ///
++ /// @param ec I/O completion error code passed to callback function.
++ /// @param length Number of bytes transferred
++ virtual void operator()(boost::system::error_code ec, size_t length = 0) {
++ data_->error_code_ = ec;
++ setLength(length);
++ setCalled(true);
++ }
++
++ /// @brief Get I/O completion error code
++ int getCode() {
++ return (data_->error_code_.value());
++ }
++
++ /// @brief Set I/O completion code
++ ///
++ /// @param code New value of completion code
++ void setCode(int code) {
++ data_->error_code_ = boost::system::error_code(code, boost::system::error_code().category());
++ }
++
++ /// @brief Get number of bytes transferred in I/O
++ size_t getLength() const {
++ return (data_->length_);
++ }
++
++ /// @brief Set number of bytes transferred in I/O
++ ///
++ /// @param length New value of length parameter
++ void setLength(size_t length) {
++ data_->length_ = length;
++ }
++
++ /// @brief Get flag to say when callback was called
++ bool getCalled() const {
++ return (data_->called_);
++ }
++
++ /// @brief Set flag to say when callback was called
++ ///
++ /// @param called New value of called parameter
++ void setCalled(bool called) {
++ data_->called_ = called;
++ }
++
++ /// @brief Return instance of callback name
++ std::string getName() const {
++ return (data_->name_);
++ }
++
++ /// @brief Set callback name
++ ///
++ /// @param name New value of the callback name
++ void setName(const std::string& name) {
++ data_->name_ = name;
++ }
++
++private:
++ boost::shared_ptr<PrivateData> data_; ///< Pointer to private data
++};
++
++/// @brief Socket and pointer types for sending and receiving ICMP echos.
++typedef ICMPSocket<SocketCallback> PingSocket;
++typedef boost::shared_ptr<PingSocket> PingSocketPtr;
++
++/// @brief Simple test fixture for testing ICMPSocket.
++class ICMPSocketTest : public ::testing::Test {
++public:
++ /// @brief Constructor.
++ ICMPSocketTest()
++ : io_service_(new IOService()), test_timer_(io_service_) {
++ test_timer_.setup(std::bind(&ICMPSocketTest::timeoutHandler, this, true),
++ TEST_TIMEOUT, IntervalTimer::ONE_SHOT);
++ }
++
++ /// @brief Destructor.
++ virtual ~ICMPSocketTest() {
++ test_timer_.cancel();
++ io_service_->stopAndPoll();
++ }
++
++ /// @brief Indicates if current user is not root
++ ///
++ /// @return True if neither the uid or the effective
++ /// uid is root.
++ static bool notRoot() {
++ return (getuid() != 0 && geteuid() != 0);
++ }
++
++ /// @brief Callback function invoke upon test timeout.
++ ///
++ /// It stops the IO service and reports test timeout.
++ ///
++ /// @param fail_on_timeout Specifies if test failure should be reported.
++ void timeoutHandler(const bool fail_on_timeout) {
++ if (fail_on_timeout) {
++ ADD_FAILURE() << "Timeout occurred while running the test!";
++ }
++ io_service_->stop();
++ }
++
++ /// @brief IOService instance used by thread pools.
++ IOServicePtr io_service_;
++
++ /// @brief Asynchronous timer service to detect timeouts.
++ IntervalTimer test_timer_;
++
++ /// @brief Returns pointer to the first byte of the input buffer.
++ ///
++ /// @throw InvalidOperation if called when the buffer is empty.
++ uint8_t* getInputBufData() {
++ if (input_buf_.empty()) {
++ isc_throw(InvalidOperation, "TcpConnection::getInputBufData() - cannot access empty buffer");
++ }
++
++ return (input_buf_.data());
++ }
++
++ /// @brief Returns input buffer size.
++ size_t getInputBufSize() const {
++ return (input_buf_.size());
++ }
++
++ /// @brief Set the capacity of the input buffer
++ ///
++ /// @param buf_size maximum number of bytes allowed in the buffer
++ void resizeInputBuf(size_t buf_size) {
++ input_buf_.resize(buf_size);
++ }
++
++ /// @brief Buffer for a single socket read.
++ std::vector<uint8_t> input_buf_;
++};
++
++
++// Verifies that an ICMP socket can be opened and closed.
++TEST_F(ICMPSocketTest, openClose) {
++ SKIP_IF(notRoot());
++
++ // For open the endpoint is only used to determine protocol, the address is irrelevant.
++ ICMPEndpoint ping_to_endpoint(IOAddress::IPV4_ZERO_ADDRESS());
++
++ PingSocket socket(io_service_);
++ SocketCallback socket_cb("open");
++
++ // Verify the socket is closed.
++ ASSERT_FALSE(socket.isOpen());
++
++ // Open the socket.
++ ASSERT_NO_THROW_LOG(socket.open(&ping_to_endpoint, socket_cb));
++
++ // Verify the socket is open.
++ ASSERT_TRUE(socket.isOpen());
++ // Since open() is synchronous the callback should not have been invoked.
++ ASSERT_FALSE(socket_cb.getCalled());
++
++ // Opening an already open should be harmless.
++ ASSERT_NO_THROW_LOG(socket.open(&ping_to_endpoint, socket_cb));
++ ASSERT_TRUE(socket.isOpen());
++
++ // Close the socket.
++ ASSERT_NO_THROW_LOG(socket.close());
++ ASSERT_FALSE(socket.isOpen());
++
++ // Closing a closed socket should be harmless.
++ ASSERT_NO_THROW_LOG(socket.close());
++ ASSERT_FALSE(socket.isOpen());
++}
++
++// Verifies that an ICMP socket can send and receive ICMP messages.
++TEST_F(ICMPSocketTest, sendReceive) {
++ SKIP_IF(notRoot());
++
++ PingSocket socket(io_service_);
++
++ // For open the endpoint is only used to determine protocol, the address is irrelevant.
++ ICMPEndpoint endpoint(IOAddress::IPV4_ZERO_ADDRESS());
++
++ // Open the socket.
++ SocketCallback open_cb("open");
++ ASSERT_NO_THROW_LOG(socket.open(&endpoint, open_cb));
++
++ // Build a ping.
++ struct icmp echo;
++ memset(&echo, 0, sizeof(echo));
++ echo.icmp_type = ICMPMsg::ECHO_REQUEST;
++ echo.icmp_id = htons(0x1122);
++ echo.icmp_seq = htons(0x3344);
++ echo.icmp_cksum = htons(~(socket.calcChecksum((const uint8_t*)&echo, sizeof(echo))));
++
++ // Send it to the loopback.
++ IOAddress ping_to_addr("127.0.0.1");
++ SocketCallback send_cb("send");
++ ICMPEndpoint ping_to_endpoint(ping_to_addr);
++ ASSERT_NO_THROW_LOG(socket.asyncSend(&echo, sizeof(echo), &ping_to_endpoint, send_cb));
++
++ // Run the send handler.
++ io_service_->runOne();
++
++ // Callback should have been invoked without an error code.
++ ASSERT_TRUE(send_cb.getCalled());
++ ASSERT_EQ(0, send_cb.getCode());
++ // Verify we sent the whole message.
++ ASSERT_EQ(send_cb.getLength(), sizeof(echo));
++
++ // Call asyncReceive until we get our reply.
++ resizeInputBuf(1500);
++ ICMPEndpoint reply_endpoint;
++ SocketCallback receive_cb("receive");
++
++ // We need two receives when pinging loop back, only one with a real address.
++ size_t pass = 0;
++ do {
++ receive_cb.clear();
++ memset(getInputBufData(), 0x00, getInputBufSize());
++ ASSERT_NO_THROW(socket.asyncReceive(static_cast<void*>(getInputBufData()),
++ getInputBufSize(), 0, &reply_endpoint, receive_cb));
++
++ // Run the read handler.
++ io_service_->runOne();
++ } while (++pass < 2 && (!receive_cb.getCalled()));
++
++ // Callback should have been invoked without an error code.
++ ASSERT_TRUE(receive_cb.getCalled());
++ ASSERT_EQ(0, receive_cb.getCode());
++
++ // Verify the reply came from the target address.
++ EXPECT_EQ(ping_to_addr.toText(), reply_endpoint.getAddress().toText());
++
++ // Verify we got at least enough data for an IP header.
++ size_t bytes_received = receive_cb.getLength();
++ ASSERT_GE(bytes_received, sizeof(struct ip));
++
++ // Build the reply from data
++ uint8_t* icbuf = getInputBufData();
++
++ // Find the IP header length...
++ struct ip* ip_header = (struct ip*)(icbuf);
++ auto hlen = (ip_header->ip_hl << 2);
++
++ // Make sure we received enough data.
++ ASSERT_TRUE(bytes_received >= (hlen + sizeof(struct icmp)))
++ << "received packet too short to be ICMP";
++
++ // Verify the message type.
++ struct icmp* reply = (struct icmp*)(icbuf + hlen);
++ auto msg_type = reply->icmp_type;
++ ASSERT_EQ(ICMPMsg::ECHO_REPLY, msg_type);
++
++ // Verify the id and sequence values.
++ auto id = ntohs(reply->icmp_hun.ih_idseq.icd_id);
++ EXPECT_EQ(0x1122, id);
++
++ auto sequence = ntohs(reply->icmp_hun.ih_idseq.icd_seq);
++ EXPECT_EQ(0x3344, sequence);
++
++ // Close the socket.
++ ASSERT_NO_THROW_LOG(socket.close());
++ ASSERT_FALSE(socket.isOpen());
++}
++
++}
+diff --git a/src/hooks/dhcp/ping_check/tests/meson.build b/src/hooks/dhcp/ping_check/tests/meson.build
+new file mode 100644
+index 0000000000..8beca7813e
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/tests/meson.build
+@@ -0,0 +1,21 @@
++if not TESTS_OPT.enabled()
++ subdir_done()
++endif
++
++dhcp_ping_check_tests = executable(
++ 'dhcp-ping-check-tests',
++ 'config_cache_unittests.cc',
++ 'icmp_endpoint_unittests.cc',
++ 'icmp_msg_unittests.cc',
++ 'icmp_socket_unittests.cc',
++ 'ping_channel_unittests.cc',
++ 'ping_check_config_unittests.cc',
++ 'ping_check_mgr_unittests.cc',
++ 'ping_context_store_unittests.cc',
++ 'ping_context_unittests.cc',
++ 'run_unittests.cc',
++ dependencies: [CRYPTO_DEP, GTEST_DEP],
++ include_directories: [include_directories('.'), include_directories('..')] + INCLUDES,
++ link_with: [dhcp_ping_check_archive] + LIBS_BUILT_SO_FAR,
++)
++test('dhcp-ping-check-tests', dhcp_ping_check_tests, protocol: 'gtest')
+diff --git a/src/hooks/dhcp/ping_check/tests/ping_channel_unittests.cc b/src/hooks/dhcp/ping_check/tests/ping_channel_unittests.cc
+new file mode 100644
+index 0000000000..4c57a2e500
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/tests/ping_channel_unittests.cc
+@@ -0,0 +1,821 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++/// @file This file contains tests which exercise the PingChannel class.
++
++#include <config.h>
++
++#include <ping_channel.h>
++#include <ping_test_utils.h>
++#include <asiolink/interval_timer.h>
++#include <asiolink/io_service_thread_pool.h>
++#include <dhcp/iface_mgr.h>
++#include <util/multi_threading_mgr.h>
++#include <testutils/multi_threading_utils.h>
++#include <testutils/gtest_utils.h>
++#include <gtest/gtest.h>
++
++#include <boost/multi_index/indexed_by.hpp>
++#include <boost/multi_index/member.hpp>
++#include <boost/multi_index/mem_fun.hpp>
++#include <boost/multi_index/hashed_index.hpp>
++#include <boost/multi_index/ordered_index.hpp>
++#include <boost/multi_index_container.hpp>
++#include <boost/multi_index/composite_key.hpp>
++
++#include <queue>
++#include <list>
++#include <thread>
++#include <mutex>
++
++using namespace std;
++using namespace isc;
++using namespace isc::asiolink;
++using namespace isc::dhcp;
++using namespace isc::ping_check;
++using namespace isc::util;
++using namespace isc::test;
++using namespace boost::asio::error;
++
++namespace ph = std::placeholders;
++
++namespace {
++
++/// @brief Tag for index by address.
++struct AddressIdSequenceIndexTag { };
++
++/// @brief A multi index container holding pointers ICMPMsgPtr
++///
++/// The message may be accessed using the following index(es):
++/// - using an IPv4 address, id, and sequence number
++typedef boost::multi_index_container<
++ // It holds pointers to ICMPMsg objects.
++ ICMPMsgPtr,
++ boost::multi_index::indexed_by<
++ // Specification of the first index starts here.
++ // This index sorts PingContexts by IPv4 addresses represented as
++ // IOAddress objects.
++ // Specification of the first index starts here.
++ boost::multi_index::ordered_unique<
++ boost::multi_index::tag<AddressIdSequenceIndexTag>,
++ boost::multi_index::composite_key<
++ ICMPMsg,
++ // The boolean value specifying if context is waiting for a reply
++ boost::multi_index::const_mem_fun<ICMPMsg, const IOAddress&,
++ &ICMPMsg::getSource>,
++ boost::multi_index::const_mem_fun<ICMPMsg, uint16_t,
++ &ICMPMsg::getId>,
++ boost::multi_index::const_mem_fun<ICMPMsg, uint16_t,
++ &ICMPMsg::getSequence>
++ >
++ >
++ >
++> ReplyContainer;
++
++/// @brief Single-threaded test fixture for exercising a PingChannel.
++class PingChannelTest : public IOServiceTest {
++public:
++ /// @brief Constructor
++ PingChannelTest() : mutex_(new mutex()), stopped_(false) {
++ MultiThreadingMgr::instance().setMode(false);
++ };
++
++ /// @brief Destructor
++ virtual ~PingChannelTest() {
++ stopped_ = true;
++ if (channel_) {
++ channel_->close();
++ }
++ if (ios_pool_) {
++ ios_pool_->getIOService()->stopAndPoll();
++ ios_pool_->stop();
++ }
++ ios_pool_.reset();
++ test_timer_.cancel();
++ test_io_service_->stopAndPoll();
++ MultiThreadingMgr::instance().setMode(false);
++ }
++
++ /// @brief Called prior to test destruction.
++ /// Ensure we stop the pool in the even a test failed in an unexpected
++ /// manner that left it running. Otherwise we can get false TSAN complaints.
++ virtual void TearDown() {
++ // Stop the thread pool (if one).
++ if (ios_pool_) {
++ ios_pool_->stop();
++ }
++ }
++
++ /// @brief Initializes the IOServiceThreadPool
++ ///
++ /// @param num_threads number of threads in the pool
++ /// @param defer_start enables deferred start of the pool's IOService
++ void initThreadPool(size_t num_threads = 1, bool defer_start = false) {
++ ios_pool_.reset(new IoServiceThreadPool(IOServicePtr(), num_threads, defer_start));
++ };
++
++ /// @brief Callback to invoke to fetch the next ping target.
++ ///
++ /// Fetches the next entry from the front of the send queue (if one). Checks for
++ /// test completion before returning.
++ ///
++ /// @param[out] next upon return it will contain the next target address. Contents are
++ /// only meaningful if the function returns true.
++ ///
++ /// @return True another target address exists, false otherwise.
++ virtual bool nextToSend(IOAddress& next) {
++ if (stopped_) {
++ return (false);
++ }
++ MultiThreadingLock lock(*mutex_);
++ bool use_next = true;
++ if (send_queue_.empty()) {
++ use_next = false;
++ } else {
++ next = send_queue_.front();
++ }
++
++ stopIfDone();
++ return (use_next);
++ }
++
++ /// @brief Callback to invoke when an ECHO write has completed.
++ ///
++ /// Ensures the completed echo matches the front of the send queue and then
++ /// pops it from the front of the queue. Checks for test completion before
++ /// returning.
++ ///
++ /// @param echo ICMP echo message that as sent
++ virtual void echoSent(ICMPMsgPtr& echo, bool send_failed) {
++ if (stopped_) {
++ return;
++ }
++ MultiThreadingLock lock(*mutex_);
++ ASSERT_EQ(echo->getDestination(), send_queue_.front()) << "send queue mismatch";
++ send_queue_.pop();
++ if (!send_failed) {
++ echos_sent_.push_back(echo);
++ }
++ stopIfDone();
++ }
++
++ /// @brief Callback to invoke when an ICMP reply has been received.
++ ///
++ /// Stores the reply if it is an ECHO REPLY message. We check to the
++ /// do avoid storing our outbound ECHO REQUESTs when testing with loop back
++ /// address. Checks for test completion before returning.
++ ///
++ /// @param reply ICMP message that was received
++ virtual void replyReceived(ICMPMsgPtr& reply) {
++ if (stopped_) {
++ return;
++ }
++ MultiThreadingLock lock(*mutex_);
++ if (reply->getType() == ICMPMsg::ECHO_REPLY) {
++ // If loopback routing is enabled, Insert the original destination address
++ // as the reply's source address.
++ if (channel_->route_loopback_) {
++ IOAddress address = channel_->loopback_map_.find(reply->getSequence());
++ if (address != IOAddress::IPV4_ZERO_ADDRESS()) {
++ reply->setSource(address);
++ }
++ }
++
++ replies_received_.push_back(reply);
++ storeReply(reply);
++ }
++
++ stopIfDone();
++ }
++
++ /// @brief Tests that a channel can send and receive, reliably
++ /// in either single or multi-threaded mode.
++ ///
++ /// The test queues the given number of requests, beginning with
++ /// address 127.0.0.1 and incrementing the address through the number
++ /// of targets. It then opens the channel and initiates reading and
++ /// and writing, running until the test completes or times out.
++ /// It expects to receive a reply for every request.
++ ///
++ /// @param num_threads number of threads in the thread pool. If 0,
++ /// the channel will be single-threaded, sharing the test's IOService,
++ /// otherwise the channel will be driven by an IOServiceThreadPool with
++ /// the given number of threads.
++ /// @param num_targets number of target IP addresses to ping. Must not
++ /// be greater than 253.
++ /// @param set_error_trigger optional function that sets the error trigger
++ /// condition.
++ void sendReceiveTest(size_t num_threads, size_t num_targets = 25,
++ const std::function<void()>& set_error_trigger = [](){});
++
++ /// @brief Tests for graceful behavior when a channel encounters a read
++ /// or write error, in either single or multi-threaded mode.
++ ///
++ /// The test runs in two passes. The first pass sends and receives until
++ /// the error trigger occurs. The error should induce a graceful cessation
++ /// of operations. After verifying expected state of affairs, the second pass
++ /// is begun by re-opening the channel and resuming operations until the test
++ /// completes or times out.
++ ///
++ /// @param set_error_trigger function that sets the error trigger condition
++ /// @param num_threads number of threads in the thread pool. If 0,
++ /// the channel will be single-threaded, sharing the test's IOService,
++ /// otherwise the channel will be driven by an IOServiceThreadPool with
++ /// the given number of threads.
++ /// @param num_targets number of target IP addresses to ping. Must not
++ /// be greater than 253.
++ void ioErrorTest(const std::function<void()>& set_error_trigger,
++ size_t num_threads, size_t num_targets = 10);
++
++ /// @brief Adds a reply to reply store.
++ ///
++ /// Fails if a reply for the same address, id, and sequence number is already
++ /// in the store. Must be used in a thread-safe context.
++ ///
++ /// @param reply reply to store
++ void storeReply(ICMPMsgPtr& reply) {
++ auto retpair = replies_map_.insert(reply);
++ ASSERT_TRUE(retpair.second)
++ << "failed to insert reply for: " << reply->getSource()
++ << ", id: " << reply->getId() << ", sequence: " << reply->getSequence();
++ }
++
++ /// @brief Fetches a reply from the store that matches a given ECHO
++ ///
++ /// Must be used in a thread-safe context.
++ ///
++ /// @param echo echo for whom a reply is sought
++ ///
++ /// @return The matching reply if found, otherwise an empty ICMPMsgPtr.
++ ICMPMsgPtr findReply(const ICMPMsgPtr& echo) {
++ auto const& index = replies_map_.get<AddressIdSequenceIndexTag>();
++ auto key = boost::make_tuple(echo->getDestination(), echo->getId(), echo->getSequence());
++ auto iter = index.find(key);
++ return (iter == index.end() ? ICMPMsgPtr() : *iter);
++ }
++
++ /// @brief Channel instance.
++ TestablePingChannelPtr channel_;
++
++ /// @brief IoServiceThreadPool instance
++ IoServiceThreadPoolPtr ios_pool_;
++
++ /// @brief The mutex used to protect internal state.
++ const boost::scoped_ptr<std::mutex> mutex_;
++
++ /// @brief Queue of IOAddresses for which to send ECHO REQUESTs.
++ std::queue<IOAddress> send_queue_;
++
++ /// @brief List of ECHO REQUESTs that have been successfully sent in the order
++ /// they were sent.
++ std::list<ICMPMsgPtr> echos_sent_;
++
++ /// @brief List of ECHO REPLYs that have been successfully received in the
++ /// order they were received.
++ std::list<ICMPMsgPtr> replies_received_;
++
++ /// @brief Map of ECHO REPLYs received, indexed by source IP, id, and sequence number.
++ ReplyContainer replies_map_;
++
++ /// @brief Flag which indicates that the manager has been stopped.
++ bool stopped_;
++};
++
++void
++PingChannelTest::sendReceiveTest(size_t num_threads, size_t num_targets /* = 25 */,
++ const std::function<void()>& set_error_trigger) {
++ stopped_ = false;
++
++ // Clear state.
++ send_queue_ = {};
++ echos_sent_.clear();
++ replies_received_.clear();
++ replies_map_.clear();
++
++ SKIP_IF(notRoot());
++
++ ASSERT_TRUE(num_targets < 253);
++ auto channel_ios = test_io_service_;
++ if (num_threads) {
++ // Enable MT mode.
++ util::MultiThreadingMgr::instance().setMode(true);
++
++ // Initialize the thread pool to num_threads, defer start.
++ ASSERT_NO_THROW_LOG(initThreadPool(num_threads, true));
++ ASSERT_TRUE(ios_pool_->isStopped());
++ channel_ios = ios_pool_->getIOService();
++ }
++
++ // Create the channel instance with the appropriate io_service.
++ ASSERT_NO_THROW_LOG(channel_.reset(new TestablePingChannel(
++ channel_ios,
++ std::bind(&PingChannelTest::nextToSend, this, ph::_1),
++ std::bind(&PingChannelTest::echoSent, this, ph::_1, ph::_2),
++ std::bind(&PingChannelTest::replyReceived, this, ph::_1)
++ )));
++
++ // Create the callback to check test completion criteria.
++ // It returns true if we have sent out all the echos and received
++ // all the replies.
++ test_done_cb_ = [this]() {
++ return (send_queue_.empty() && (echos_sent_.size() == replies_received_.size()));
++ };
++
++ // Fill the send queue with num_target addresses to ping.
++ IOAddress target("127.0.0.1");
++ for (auto i = 0; i < num_targets; ++i) {
++ send_queue_.push(target);
++ target = IOAddress::increase(target);
++ }
++
++ (set_error_trigger)();
++
++ // Open the channel.
++ ASSERT_NO_THROW_LOG(channel_->open());
++ ASSERT_TRUE(channel_->isOpen());
++
++ if (num_threads) {
++ ios_pool_->run();
++ }
++
++ // Initiate reading and writing.
++ ASSERT_NO_THROW_LOG(channel_->startRead());
++ ASSERT_NO_THROW_LOG(channel_->startSend());
++
++ // Run the main thread's IOService until we complete or timeout.
++ ASSERT_NO_THROW_LOG(runIOService(1000));
++
++ if (ios_pool_) {
++ // Stop the thread pool.
++ ASSERT_NO_THROW_LOG(ios_pool_->stop());
++ ASSERT_TRUE(ios_pool_->isStopped());
++ }
++
++ // Send queue should be empty.
++ EXPECT_TRUE(send_queue_.empty());
++
++ // Should have as many replies as echos.
++ EXPECT_EQ(echos_sent_.size(), replies_received_.size());
++
++ // Should have a reply for every echo.
++ for (auto const& echo : echos_sent_) {
++ ICMPMsgPtr reply = findReply(echo);
++ EXPECT_TRUE(reply) << "no reply found for:" << echo->getDestination()
++ << ", id:" << echo->getId() << ", sequence: " << echo->getSequence();
++ }
++
++ stopped_ = true;
++ if (channel_) {
++ channel_->close();
++ }
++ if (ios_pool_) {
++ ios_pool_->getIOService()->stopAndPoll();
++ ios_pool_->stop();
++ }
++ ios_pool_.reset();
++ test_timer_.cancel();
++ test_io_service_->stopAndPoll();
++ MultiThreadingMgr::instance().setMode(false);
++}
++
++void
++PingChannelTest::ioErrorTest(const std::function<void()>& set_error_trigger,
++ size_t num_threads, size_t num_targets) {
++ ASSERT_TRUE(num_targets < 253);
++ SKIP_IF(notRoot());
++
++ ASSERT_TRUE(replies_received_.empty());
++
++ /// If it's an MT test create the thread pool.
++ auto channel_ios = test_io_service_;
++ if (num_threads) {
++ // Enable MT mode.
++ util::MultiThreadingMgr::instance().setMode(true);
++
++ // Initialize the thread pool to num_threads, defer start.
++ ASSERT_NO_THROW_LOG(initThreadPool(num_threads, true));
++ ASSERT_TRUE(ios_pool_->isStopped());
++ channel_ios = ios_pool_->getIOService();
++ }
++
++ // Set local shutdown called flag to false.
++ bool shutdown_cb_called = false;
++
++ // Create the channel instance with the appropriate io_service.
++ ASSERT_NO_THROW_LOG(channel_.reset(new TestablePingChannel(
++ channel_ios,
++ std::bind(&PingChannelTest::nextToSend, this, ph::_1),
++ std::bind(&PingChannelTest::echoSent, this, ph::_1, ph::_2),
++ std::bind(&PingChannelTest::replyReceived, this, ph::_1),
++ ([this, &shutdown_cb_called]() {
++ shutdown_cb_called = true;
++ test_io_service_->stop();
++ })
++ )));
++
++ // Set the test_done_cb_ to always return false (i.e. test is not
++ // done).
++ test_done_cb_ = []() {
++ return (false);
++ };
++
++ // Fill the send queue with target addresses to ping.
++ IOAddress target("127.0.0.1");
++ for (auto i = 0; i < (num_targets / 2); ++i) {
++ send_queue_.push(target);
++ target = IOAddress::increase(target);
++ }
++
++ // Set the error trigger.
++ (set_error_trigger)();
++
++ // FIRST PASS
++
++ // Open the channel.
++ ASSERT_NO_THROW_LOG(channel_->open());
++ ASSERT_TRUE(channel_->isOpen());
++
++ if (num_threads) {
++ ios_pool_->run();
++ }
++
++ // Initiate reading and writing.
++ ASSERT_NO_THROW_LOG(channel_->startRead());
++ ASSERT_NO_THROW_LOG(channel_->startSend());
++
++ // Run the main thread's IOService until we stop or timeout.
++ ASSERT_NO_THROW_LOG(runIOService(1000));
++
++ // Shutdown callback should have been invoked, the channel should be closed,
++ // but the pool should still be running.
++ ASSERT_TRUE(shutdown_cb_called);
++ ASSERT_FALSE(channel_->isOpen());
++
++ if (ios_pool_) {
++ ASSERT_TRUE(ios_pool_->isRunning());
++
++ // Pause the thread pool.
++ ASSERT_NO_THROW_LOG(ios_pool_->pause());
++ ASSERT_TRUE(ios_pool_->isPaused());
++ }
++
++ // Save how many echos sent and replies received during the first pass.
++ auto first_pass_echo_count = echos_sent_.size();
++ auto first_pass_reply_count = replies_received_.size();
++
++ // Should have sent some but not all.
++ EXPECT_LE(first_pass_echo_count, num_targets);
++
++ // SECOND PASS
++
++ // Modify the test done callback to check test completion criteria.
++ // It returns true if we have sent out all the echos and received
++ // all the replies.
++ test_done_cb_ = [this, &first_pass_reply_count]() {
++ return (send_queue_.empty() && (replies_received_.size() > first_pass_reply_count));
++ };
++
++ // Fill the send queue with target addresses to ping.
++ for (auto i = 0; i < (num_targets / 2); ++i) {
++ send_queue_.push(target);
++ target = IOAddress::increase(target);
++ }
++
++ // Resume running the thread pool (if one).
++ if (ios_pool_) {
++ ASSERT_NO_THROW_LOG(ios_pool_->run());
++ ASSERT_TRUE(ios_pool_->isRunning());
++ }
++
++ // Resume reopening the channel and restarting IO operations.
++ ASSERT_NO_THROW_LOG(channel_->open());
++ ASSERT_TRUE(channel_->isOpen());
++ ASSERT_NO_THROW_LOG(channel_->startRead());
++ ASSERT_NO_THROW_LOG(channel_->startSend());
++
++ // Run the main thread's IOService until we complete or timeout.
++ ASSERT_NO_THROW_LOG(runIOService(1000));
++
++ // Stop the thread pool (if one).
++ if (ios_pool_) {
++ ASSERT_NO_THROW_LOG(ios_pool_->stop());
++ ASSERT_TRUE(ios_pool_->isStopped());
++ }
++
++ // Send queue should be empty.
++ EXPECT_TRUE(send_queue_.empty());
++
++ // Should have sent as many echos as we queued.
++ EXPECT_EQ(echos_sent_.size(), num_targets);
++
++ // Should have more replies than we had, but likely not all.
++ EXPECT_GE(replies_received_.size(), first_pass_reply_count);
++}
++
++// Verifies PingChannel open and close operations.
++TEST_F(PingChannelTest, openCloseST) {
++ SKIP_IF(notRoot());
++
++ // Create the channel instance.
++ ASSERT_NO_THROW_LOG(channel_.reset(new TestablePingChannel(
++ test_io_service_,
++ std::bind(&PingChannelTest::nextToSend, this, ph::_1),
++ std::bind(&PingChannelTest::echoSent, this, ph::_1, ph::_2),
++ std::bind(&PingChannelTest::replyReceived, this, ph::_1)
++ )));
++
++ ASSERT_TRUE(channel_);
++
++ ASSERT_TRUE(channel_->getSingleThreaded());
++
++ // Verify it is not open.
++ ASSERT_FALSE(channel_->isOpen());
++
++ EXPECT_FALSE(channel_->getWatchSocket());
++ EXPECT_EQ(channel_->getRegisteredWriteFd(), -1);
++ EXPECT_EQ(channel_->getRegisteredReadFd(), -1);
++
++ // Verify that invoking close is harmless.
++ ASSERT_NO_THROW_LOG(channel_->close());
++
++ // Attempt to open the channel.
++ ASSERT_NO_THROW_LOG(channel_->open());
++
++ // PingChannel::open() is synchronous and while it has a callback
++ // it should never be invoked. Run the service to make sure.
++ ASSERT_NO_THROW_LOG(runIOService(1000));
++
++ // Verify the channel is open.
++ ASSERT_TRUE(channel_->isOpen());
++
++ // Verify the WatchSocket was created and that its fd and that of the
++ // PingSocket are both registered with IfaceMgr.
++ ASSERT_TRUE(channel_->getWatchSocket());
++ int registered_write_fd = channel_->getRegisteredWriteFd();
++ EXPECT_EQ(registered_write_fd, channel_->getWatchSocket()->getSelectFd());
++ EXPECT_TRUE(IfaceMgr::instance().isExternalSocket(registered_write_fd));
++ int registered_read_fd = channel_->getRegisteredReadFd();
++ EXPECT_EQ(registered_read_fd, channel_->getPingSocket()->getNative());
++ EXPECT_TRUE(IfaceMgr::instance().isExternalSocket(registered_read_fd));
++
++ // A subsequent open should be harmless.
++ ASSERT_NO_THROW_LOG(channel_->open());
++
++ // Closing the socket should work.
++ ASSERT_NO_THROW_LOG(channel_->close());
++
++ // Verify watch socket is gone, registered fds are reset, and prior
++ // registered fds are no longer registered.
++ EXPECT_FALSE(channel_->getWatchSocket());
++ EXPECT_EQ(channel_->getRegisteredWriteFd(), -1);
++ EXPECT_FALSE(IfaceMgr::instance().isExternalSocket(registered_write_fd));
++ EXPECT_EQ(channel_->getRegisteredReadFd(), -1);
++ EXPECT_FALSE(IfaceMgr::instance().isExternalSocket(registered_read_fd));
++
++ // Verify it is not open.
++ ASSERT_FALSE(channel_->isOpen());
++}
++
++// Verifies PingChannel open and close operations.
++TEST_F(PingChannelTest, openCloseMT) {
++ SKIP_IF(notRoot());
++ MultiThreadingTest mt;
++
++ // Create the channel instance.
++ ASSERT_NO_THROW_LOG(channel_.reset(new TestablePingChannel(
++ test_io_service_,
++ std::bind(&PingChannelTest::nextToSend, this, ph::_1),
++ std::bind(&PingChannelTest::echoSent, this, ph::_1, ph::_2),
++ std::bind(&PingChannelTest::replyReceived, this, ph::_1)
++ )));
++
++ ASSERT_TRUE(channel_);
++
++ ASSERT_FALSE(channel_->getSingleThreaded());
++
++ // Verify it is not open.
++ ASSERT_FALSE(channel_->isOpen());
++
++ // Verify that invoking close is harmless.
++ ASSERT_NO_THROW_LOG(channel_->close());
++
++ // Attempt to open the channel.
++ ASSERT_NO_THROW_LOG(channel_->open());
++
++ // PingChannel::open() is synchronous and while it has a callback
++ // it should never be invoked. Run the service to make sure.
++ ASSERT_NO_THROW_LOG(runIOService(1000));
++
++ // Verify the channel is open.
++ ASSERT_TRUE(channel_->isOpen());
++
++ // Verify that single-threaded members are not set.
++ EXPECT_FALSE(channel_->getWatchSocket());
++ EXPECT_EQ(channel_->getRegisteredWriteFd(), -1);
++ EXPECT_EQ(channel_->getRegisteredReadFd(), -1);
++
++ // A subsequent open should be harmless.
++ ASSERT_NO_THROW_LOG(channel_->open());
++
++ // Closing the socket should work.
++ ASSERT_NO_THROW_LOG(channel_->close());
++
++ // Verify it is not open.
++ ASSERT_FALSE(channel_->isOpen());
++}
++
++// Verifies that a PingChannel can perpetuate sending requests and receiving
++// replies when driven by a single-threaded IOService.
++TEST_F(PingChannelTest, sendReceiveST) {
++ sendReceiveTest(0);
++}
++
++// Verifies that a PingChannel can perpetuate sending requests and receiving
++// replies when driven by a multi-threaded IOServiceThreadPool 3 threads
++TEST_F(PingChannelTest, sendReceiveMT) {
++ // Use a thread pool with 3 threads.
++ sendReceiveTest(3);
++}
++
++// Verifies that an exception throw from asyncRead triggers graceful channel
++// shutdown and that operations can be resumed with a single-threaded channel.
++TEST_F(PingChannelTest, readExceptionErrorST) {
++ ioErrorTest(
++ [this]() {
++ channel_->throw_on_read_number_ = 5;
++ }, 0);
++}
++
++// Verifies that an exception throw from asyncRead triggers graceful channel
++// shutdown and that operations can be resumed with a multi-threaded channel.
++TEST_F(PingChannelTest, readExceptionErrorMT) {
++ // Use a thread pool with 3 threads.
++ ioErrorTest(
++ [this]() {
++ channel_->throw_on_read_number_ = 5;
++ }, 3, 20);
++}
++
++// Verifies that a fatal error code passed into socketReadCallback triggers graceful channel
++// shutdown and that operations can be resumed with a single-threaded channel.
++TEST_F(PingChannelTest, readFatalErrorST) {
++ ioErrorTest(
++ [this]() {
++ channel_->ec_on_read_number_ = 3;
++ // See boost/asio/error.hpp for error codes
++ channel_->read_error_ec_ = make_error_code(fault);
++ }, 0);
++}
++
++// Verifies that a fatal error code passed into socketReadCallback triggers graceful channel
++// shutdown and that operations can be resumed with a single-threaded channel.
++TEST_F(PingChannelTest, readFatalErrorMT) {
++ ioErrorTest(
++ [this]() {
++ channel_->ec_on_read_number_ = 3;
++ // See boost/asio/error.hpp for error codes
++ channel_->read_error_ec_ = make_error_code(fault);
++ }, 4);
++}
++
++// Verifies that a non-fatal, EWOULDBLOCK error passed into socketReadCallback does
++// not disrupt reading for a single-threaded channel.
++TEST_F(PingChannelTest, readAgainErrorST) {
++ sendReceiveTest(0, 10,
++ [this]() {
++ channel_->ec_on_read_number_ = 4;
++ // See boost/asio/error.hpp for error codes
++ channel_->read_error_ec_ = make_error_code(would_block);
++ });
++}
++
++// Verifies that a non-fatal, EWOULDBLOCK error passed into socketReadCallback does
++// not disrupt reading for a multi-threaded channel.
++TEST_F(PingChannelTest, readAgainErrorMT) {
++ sendReceiveTest(3, 10,
++ [this]() {
++ channel_->ec_on_read_number_ = 4;
++ // See boost/asio/error.hpp for error codes
++ channel_->read_error_ec_ = make_error_code(would_block);
++ });
++}
++
++// Verifies that an exception throw from asyncRead triggers graceful channel
++// shutdown and that operations can be resumed with a single-threaded channel.
++TEST_F(PingChannelTest, writeExceptionErrorST) {
++ ioErrorTest(
++ [this]() {
++ channel_->throw_on_write_number_ = 5;
++ }, 0);
++}
++
++// Verifies that an exception throw from asyncRead triggers graceful channel
++// shutdown and that operations can be resumed with a multi-threaded channel.
++TEST_F(PingChannelTest, writeExceptionErrorMT) {
++ // Use a thread pool with 3 threads.
++ ioErrorTest(
++ [this]() {
++ channel_->throw_on_write_number_ = 5;
++ }, 3);
++}
++
++// Verifies that a fatal error code passed into socketReadCallback triggers graceful channel
++// shutdown and that operations can be resumed with a single-threaded channel.
++TEST_F(PingChannelTest, writeFatalErrorST) {
++ ioErrorTest(
++ [this]() {
++ channel_->ec_on_write_number_ = 3;
++ // See boost/asio/error.hpp for error codes
++ channel_->write_error_ec_ = make_error_code(fault);
++ }, 0);
++}
++
++// Verifies that a fatal error code passed into socketReadCallback triggers graceful channel
++// shutdown and that operations can be resumed with a single-threaded channel.
++TEST_F(PingChannelTest, writeFatalErrorMT) {
++ ioErrorTest(
++ [this]() {
++ channel_->ec_on_write_number_ = 3;
++ // See boost/asio/error.hpp for error codes
++ channel_->write_error_ec_ = make_error_code(fault);
++ }, 4);
++}
++
++// Verifies that a non-fatal, EWOULDBLOCK error passed into socketWriteCallback does
++// not disrupt writing for a single-threaded channel.
++TEST_F(PingChannelTest, writeAgainErrorST) {
++ sendReceiveTest(0, 10,
++ [this]() {
++ channel_->ec_on_write_number_ = 6;
++ // See boost/asio/error.hpp for error codes
++ channel_->write_error_ec_ = make_error_code(would_block);
++ });
++}
++
++// Verifies that a non-fatal, EWOULDBLOCK error passed into socketWriteCallback
++// does not disrupt writing for a multi-threaded channel.
++TEST_F(PingChannelTest, writeAgainErrorMT) {
++ sendReceiveTest(3, 10,
++ [this]() {
++ channel_->ec_on_write_number_ = 6;
++ // See boost/asio/error.hpp for error codes
++ channel_->write_error_ec_ = make_error_code(would_block);
++ });
++}
++
++// Verify the recoverable write errors do not disrupt writing for a
++// single-threaded channel.
++TEST_F(PingChannelTest, writeSendFailedErrorST) {
++ SKIP_IF(notRoot());
++
++ std::list<boost::asio::error::basic_errors> errors = {
++ boost::asio::error::network_unreachable,
++ boost::asio::error::host_unreachable,
++ boost::asio::error::network_down,
++ boost::asio::error::no_buffer_space,
++ boost::asio::error::access_denied
++ };
++
++ for (auto const& error : errors) {
++ sendReceiveTest(0, 10,
++ [this, error]() {
++ channel_->ec_on_write_number_ = 6;
++ // See boost/asio/error.hpp for error codes
++ channel_->write_error_ec_ = make_error_code(error);
++ });
++
++ // Sanity check, we should have sent one less than we targeted.
++ EXPECT_EQ(echos_sent_.size(), 9);
++ }
++}
++
++// Verify the recoverable write errors do not disrupt writing for a
++// multi-threaded channel.
++TEST_F(PingChannelTest, writeSendFailedErrorMT) {
++ SKIP_IF(notRoot());
++
++ std::list<boost::asio::error::basic_errors> errors = {
++ boost::asio::error::network_unreachable,
++ boost::asio::error::host_unreachable,
++ boost::asio::error::network_down,
++ boost::asio::error::no_buffer_space,
++ boost::asio::error::access_denied
++ };
++
++ for (auto const& error : errors) {
++ sendReceiveTest(3, 10,
++ [this, error]() {
++ channel_->ec_on_write_number_ = 6;
++ // See boost/asio/error.hpp for error codes
++ channel_->write_error_ec_ = make_error_code(error);
++ });
++
++ // Sanity check, we should have sent one less than we targeted.
++ EXPECT_EQ(echos_sent_.size(), 9);
++ }
++}
++
++} // end of anonymous namespace
+diff --git a/src/hooks/dhcp/ping_check/tests/ping_check_config_unittests.cc b/src/hooks/dhcp/ping_check/tests/ping_check_config_unittests.cc
+new file mode 100644
+index 0000000000..a831a0efab
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/tests/ping_check_config_unittests.cc
+@@ -0,0 +1,287 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++/// @file This file contains tests which exercise the PingCheckConfig class.
++
++#include <config.h>
++#include <ping_check_config.h>
++#include <testutils/gtest_utils.h>
++
++#include <gtest/gtest.h>
++#include <list>
++
++using namespace std;
++using namespace isc;
++using namespace isc::data;
++using namespace isc::ping_check;
++
++namespace {
++
++// Verifies PingCheckConfig constructors and accessors.
++TEST(PingCheckConfigTest, basics) {
++ PingCheckConfig config;
++
++ // Verify initial values.
++ EXPECT_TRUE(config.getEnablePingCheck());
++ EXPECT_EQ(1, config.getMinPingRequests());
++ EXPECT_EQ(100, config.getReplyTimeout());
++ EXPECT_EQ(60, config.getPingClttSecs());
++ EXPECT_EQ(0, config.getPingChannelThreads());
++
++ // Verify accessors.
++ EXPECT_NO_THROW_LOG(config.setEnablePingCheck(false));
++ EXPECT_FALSE(config.getEnablePingCheck());
++
++ EXPECT_NO_THROW_LOG(config.setMinPingRequests(4));
++ EXPECT_EQ(4, config.getMinPingRequests());
++
++ EXPECT_NO_THROW_LOG(config.setReplyTimeout(250));
++ EXPECT_EQ(250, config.getReplyTimeout());
++
++ EXPECT_NO_THROW_LOG(config.setPingClttSecs(120));
++ EXPECT_EQ(120, config.getPingClttSecs());
++
++ EXPECT_NO_THROW_LOG(config.setPingChannelThreads(6));
++ EXPECT_EQ(6, config.getPingChannelThreads());
++
++ // Verify copy construction.
++ PingCheckConfig config2(config);
++ EXPECT_FALSE(config2.getEnablePingCheck());
++ EXPECT_EQ(4, config2.getMinPingRequests());
++ EXPECT_EQ(250, config2.getReplyTimeout());
++ EXPECT_EQ(120, config2.getPingClttSecs());
++ EXPECT_EQ(6, config2.getPingChannelThreads());
++}
++
++// Exercises PingCheckConfig parameter parsing with valid configuration
++// permutations.
++TEST(PingCheckConfigTest, parseValidScenarios) {
++ // Describes a test scenario.
++ struct Scenario {
++ int line_; // Scenario line number
++ std::string json_; // JSON configuration to parse
++ bool exp_enable_ping_check_; // Expected value for enable-ping-check
++ uint32_t exp_min_ping_requests_; // Expected value for min-ping-requests
++ uint32_t exp_reply_timeout_; // Expected value for reply-timeout
++ uint32_t exp_ping_cltt_secs_; // Expected value for ping-cltt-secs
++ size_t exp_num_threads_; // Expected value for ping-channel-threads
++ };
++
++ // List of test scenarios to run.
++ list<Scenario> scenarios = {
++ {
++ // Empty map
++ __LINE__,
++ R"({ })",
++ true, 1, 100, 60, 0
++ },
++ {
++ // Only enable-ping-check",
++ __LINE__,
++ R"({ "enable-ping-check" : false })",
++ false, 1, 100, 60, 0
++ },
++ {
++ // Only min-ping-requests",
++ __LINE__,
++ R"({ "min-ping-requests" : 3 })",
++ true, 3, 100, 60, 0
++ },
++ {
++ // Only reply-timeout",
++ __LINE__,
++ R"({ "reply-timeout" : 250 })",
++ true, 1, 250, 60, 0
++ },
++ {
++ // Only ping-cltt-secs",
++ __LINE__,
++ R"({ "ping-cltt-secs" : 77 })",
++ true, 1, 100, 77, 0
++ },
++ {
++ // Only ping-channel-threads",
++ __LINE__,
++ R"({ "ping-channel-threads" : 5 })",
++ true, 1, 100, 60, 5
++ },
++ {
++ // All parameters",
++ __LINE__,
++ R"(
++ {
++ "enable-ping-check" : false,
++ "min-ping-requests" : 2,
++ "reply-timeout" : 375,
++ "ping-cltt-secs" : 120,
++ "ping-channel-threads" : 6
++ })",
++ false, 2, 375, 120, 6
++ },
++ };
++
++ // Iterate over the scenarios.
++ for (auto const& scenario : scenarios) {
++ stringstream oss;
++ oss << "scenario at line: " << scenario.line_;
++ SCOPED_TRACE(oss.str());
++
++ // Convert JSON texts to Element map.
++ ConstElementPtr json_elements;
++ ASSERT_NO_THROW_LOG(json_elements = Element::fromJSON(scenario.json_));
++
++ // Parsing elements should succeed.
++ PingCheckConfig config;
++ ASSERT_NO_THROW_LOG(config.parse(json_elements));
++
++ // Verify expected values.
++ EXPECT_EQ(scenario.exp_enable_ping_check_, config.getEnablePingCheck());
++ EXPECT_EQ(scenario.exp_min_ping_requests_, config.getMinPingRequests());
++ EXPECT_EQ(scenario.exp_reply_timeout_, config.getReplyTimeout());
++ EXPECT_EQ(scenario.exp_ping_cltt_secs_, config.getPingClttSecs());
++ EXPECT_EQ(scenario.exp_num_threads_, config.getPingChannelThreads());
++ }
++}
++
++// Exercises PingCheckConfig parameter parsing with invalid configuration
++// permutations.
++TEST(PingCheckConfigTest, parseInvalidScenarios) {
++ // Describes a test scenario.
++ struct Scenario {
++ int line_; // Scenario line number
++ string json_; // JSON configuration to parse
++ string exp_message_; // Expected exception message
++ };
++
++ // List of test scenarios to run. Most scenario supply
++ // all valid parameters except one in error. This allows
++ // us to verify that no values are changed if any are in error.
++ list<Scenario> scenarios = {
++ {
++ __LINE__,
++ R"(
++ {
++ "enable-ping-check" : false,
++ "min-ping-requests" : 3,
++ "reply-timeout" : 250,
++ "ping-cltt-secs" : 90,
++ "ping-channel-threads" : 4,
++ "bogus" : false
++ })",
++ "spurious 'bogus' parameter"
++ },
++ {
++ __LINE__,
++ R"(
++ {
++ "enable-ping-check" : "not bool",
++ "min-ping-requests" : 3,
++ "reply-timeout" : 250,
++ "ping-cltt-secs" : 90,
++ "ping-channel-threads" : 4
++ })",
++ "'enable-ping-check' parameter is not a boolean"
++ },
++ {
++ __LINE__,
++ R"(
++ {
++ "enable-ping-check" : false,
++ "min-ping-requests" : 0,
++ "reply-timeout" : 250,
++ "ping-cltt-secs" : 90,
++ "ping-channel-threads" : 4
++ })",
++ "invalid min-ping-requests: '0', must be greater than 0"
++ },
++ {
++ __LINE__,
++ R"(
++ {
++ "enable-ping-check" : false,
++ "min-ping-requests" : -2,
++ "reply-timeout" : 250,
++ "ping-cltt-secs" : 90,
++ "ping-channel-threads" : 4
++ })",
++ "invalid min-ping-requests: '-2', must be greater than 0"
++ },
++ {
++ __LINE__,
++ R"(
++ {
++ "enable-ping-check" : false,
++ "min-ping-requests" : 1,
++ "reply-timeout" : 0,
++ "ping-cltt-secs" : 90,
++ "ping-channel-threads" : 4
++ })",
++ "invalid reply-timeout: '0', must be greater than 0"
++ },
++ {
++ __LINE__,
++ R"(
++ {
++ "enable-ping-check" : false,
++ "min-ping-requests" : 1,
++ "reply-timeout" : -77,
++ "ping-cltt-secs" : 90,
++ "ping-channel-threads" : 4
++ })",
++ "invalid reply-timeout: '-77', must be greater than 0"
++ },
++ {
++ __LINE__,
++ R"(
++ {
++ "enable-ping-check" : false,
++ "min-ping-requests" : 1,
++ "reply-timeout" : 250,
++ "ping-cltt-secs" : -3,
++ "ping-channel-threads" : 4
++ })",
++ "invalid ping-cltt-secs: '-3', cannot be less than 0"
++ },
++ {
++ __LINE__,
++ R"(
++ {
++ "enable-ping-check" : false,
++ "min-ping-requests" : 1,
++ "reply-timeout" : 250,
++ "ping-cltt-secs" : 90,
++ "ping-channel-threads" : -1
++ })",
++ "invalid ping-channel-threads: '-1', cannot be less than 0"
++ }
++ };
++
++ // Iterate over the scenarios.
++ PingCheckConfig default_config;
++ for (auto const& scenario : scenarios) {
++ stringstream oss;
++ oss << "scenario at line: " << scenario.line_;
++ SCOPED_TRACE(oss.str());
++
++ // Convert JSON text to a map of parameters.
++ ConstElementPtr json_elements;
++ ASSERT_NO_THROW_LOG(json_elements = Element::fromJSON(scenario.json_));
++
++ // Parsing parameters should throw.
++ PingCheckConfig config;
++ ASSERT_THROW_MSG(config.parse(json_elements), dhcp::DhcpConfigError,
++ scenario.exp_message_);
++
++ // Original values should be intact.
++ EXPECT_EQ(default_config.getEnablePingCheck(), config.getEnablePingCheck());
++ EXPECT_EQ(default_config.getMinPingRequests(), config.getMinPingRequests());
++ EXPECT_EQ(default_config.getReplyTimeout(), config.getReplyTimeout());
++ EXPECT_EQ(default_config.getPingClttSecs(), config.getPingClttSecs());
++ EXPECT_EQ(default_config.getPingChannelThreads(), config.getPingChannelThreads());
++ }
++}
++
++} // end of anonymous namespace
+diff --git a/src/hooks/dhcp/ping_check/tests/ping_check_mgr_unittests.cc b/src/hooks/dhcp/ping_check/tests/ping_check_mgr_unittests.cc
+new file mode 100644
+index 0000000000..ded13b085c
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/tests/ping_check_mgr_unittests.cc
+@@ -0,0 +1,1878 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++/// @file This file contains tests which exercise the PingCheckMgr class.
++#include <config.h>
++
++#include <ping_check_mgr.h>
++#include <ping_test_utils.h>
++#include <cc/data.h>
++#include <dhcp/pkt4.h>
++#include <dhcpsrv/cfgmgr.h>
++#include <dhcpsrv/lease.h>
++#include <hooks/hooks_manager.h>
++#include <util/chrono_time_utils.h>
++#include <testutils/gtest_utils.h>
++#include <testutils/multi_threading_utils.h>
++
++#include <gtest/gtest.h>
++#include <mutex>
++#include <chrono>
++
++using namespace std;
++using namespace isc;
++using namespace isc::data;
++using namespace isc::dhcp;
++using namespace isc::util;
++using namespace isc::asiolink;
++using namespace isc::ping_check;
++using namespace isc::hooks;
++using namespace isc::test;
++using namespace std::chrono;
++using namespace boost::asio::error;
++
++namespace ph = std::placeholders;
++
++namespace {
++
++// Sanity check the basics for production class, PingCheckMgr, single-threaded mode.
++TEST(PingCheckMgr, basicsST) {
++ SKIP_IF(IOServiceTest::notRoot());
++ MultiThreadingMgr::instance().setMode(false);
++
++ // Create a multi-threaded manager.
++ IOServicePtr main_ios(new IOService());
++ PingCheckMgrPtr mgr;
++ ASSERT_NO_THROW_LOG(mgr.reset(new PingCheckMgr(0)));
++ ASSERT_TRUE(mgr);
++ mgr->setIOService(main_ios);
++
++ // Sanity check the global configuration. More robust tests are done
++ // elsewhere.
++ auto& config = mgr->getGlobalConfig();
++ EXPECT_TRUE(config->getEnablePingCheck());
++ EXPECT_EQ(1, config->getMinPingRequests());
++ EXPECT_EQ(100, config->getReplyTimeout());
++ EXPECT_EQ(60, config->getPingClttSecs());
++ EXPECT_EQ(0, config->getPingChannelThreads());
++
++ // Verify we report as stopped.
++ EXPECT_FALSE(mgr->isRunning());
++ EXPECT_TRUE(mgr->isStopped());
++ EXPECT_FALSE(mgr->isPaused());
++
++ // Starting it should be OK.
++ ASSERT_NO_THROW_LOG(mgr->start());
++
++ // Verify we report as running.
++ EXPECT_TRUE(mgr->isRunning());
++ EXPECT_FALSE(mgr->isStopped());
++ EXPECT_FALSE(mgr->isPaused());
++
++ // Pausing it should be harmless.
++ ASSERT_NO_THROW_LOG(mgr->pause());
++
++ // Verify we report as running.
++ EXPECT_TRUE(mgr->isRunning());
++ EXPECT_FALSE(mgr->isStopped());
++ EXPECT_FALSE(mgr->isPaused());
++
++ // Resuming it should be harmless.
++ ASSERT_NO_THROW_LOG(mgr->resume());
++
++ // Verify we report as running.
++ EXPECT_TRUE(mgr->isRunning());
++ EXPECT_FALSE(mgr->isStopped());
++ EXPECT_FALSE(mgr->isPaused());
++
++ // Stopping it should be fine
++ ASSERT_NO_THROW_LOG(mgr->stop());
++
++ // Verify we report as stopped.
++ EXPECT_FALSE(mgr->isRunning());
++ EXPECT_TRUE(mgr->isStopped());
++ EXPECT_FALSE(mgr->isPaused());
++
++ // Re-starting it should be OK.
++ ASSERT_NO_THROW_LOG(mgr->start());
++
++ // Verify we report as running.
++ EXPECT_TRUE(mgr->isRunning());
++ EXPECT_FALSE(mgr->isStopped());
++ EXPECT_FALSE(mgr->isPaused());
++
++ // Calling destructor when its running should be OK.
++ ASSERT_NO_THROW_LOG(mgr.reset());
++
++ main_ios->stopAndPoll();
++}
++
++// Sanity check the basics for production class, PingCheckMgr. Bulk of testing
++// is done with test derivation, TestPingCheckMgr.
++TEST(PingCheckMgr, basicsMT) {
++ SKIP_IF(IOServiceTest::notRoot());
++ MultiThreadingTest mt;
++
++ // Create a multi-threaded manager.
++ IOServicePtr main_ios(new IOService());
++ PingCheckMgrPtr mgr;
++ ASSERT_NO_THROW_LOG(mgr.reset(new PingCheckMgr(3)));
++ ASSERT_TRUE(mgr);
++ mgr->setIOService(main_ios);
++
++ // Sanity check the global configuration. More robust tests are done
++ // elsewhere.
++ auto& config = mgr->getGlobalConfig();
++ EXPECT_TRUE(config->getEnablePingCheck());
++ EXPECT_EQ(1, config->getMinPingRequests());
++ EXPECT_EQ(100, config->getReplyTimeout());
++ EXPECT_EQ(60, config->getPingClttSecs());
++ EXPECT_EQ(3, config->getPingChannelThreads());
++
++ // It should not be running yet.
++ EXPECT_FALSE(mgr->isRunning());
++ EXPECT_TRUE(mgr->isStopped());
++ EXPECT_FALSE(mgr->isPaused());
++
++ // Starting it should be OK.
++ ASSERT_NO_THROW_LOG(mgr->start());
++
++ // Verify it's running.
++ EXPECT_TRUE(mgr->isRunning());
++ EXPECT_FALSE(mgr->isStopped());
++ EXPECT_FALSE(mgr->isPaused());
++
++ // Pausing it should be fine.
++ ASSERT_NO_THROW_LOG(mgr->pause());
++
++ // Verify it's paused.
++ EXPECT_FALSE(mgr->isRunning());
++ EXPECT_FALSE(mgr->isStopped());
++ EXPECT_TRUE(mgr->isPaused());
++
++ // Resuming it should be fine.
++ ASSERT_NO_THROW_LOG(mgr->resume());
++
++ // Verify it's running.
++ EXPECT_TRUE(mgr->isRunning());
++ EXPECT_FALSE(mgr->isStopped());
++ EXPECT_FALSE(mgr->isPaused());
++
++ // Stopping it should be fine
++ ASSERT_NO_THROW_LOG(mgr->stop());
++
++ // It should not be running.
++ EXPECT_FALSE(mgr->isRunning());
++ EXPECT_TRUE(mgr->isStopped());
++ EXPECT_FALSE(mgr->isPaused());
++
++ // Re-starting it should be OK.
++ ASSERT_NO_THROW_LOG(mgr->start());
++
++ // Verify it's running.
++ EXPECT_TRUE(mgr->isRunning());
++ EXPECT_FALSE(mgr->isStopped());
++ EXPECT_FALSE(mgr->isPaused());
++
++ // Calling destructor when its running should be OK.
++ ASSERT_NO_THROW_LOG(mgr.reset());
++}
++
++// Verify basic behavior of PingCheckMgr::configure().
++TEST(PingCheckMgr, configure) {
++ // Create a manager.
++ IOServicePtr main_ios(new IOService());
++ PingCheckMgrPtr mgr;
++ ASSERT_NO_THROW_LOG(mgr.reset(new PingCheckMgr()));
++ ASSERT_TRUE(mgr);
++
++ // Verify initial global configuration.
++ auto& default_config = mgr->getGlobalConfig();
++ EXPECT_TRUE(default_config->getEnablePingCheck());
++ EXPECT_EQ(1, default_config->getMinPingRequests());
++ EXPECT_EQ(100, default_config->getReplyTimeout());
++ EXPECT_EQ(60, default_config->getPingClttSecs());
++ EXPECT_EQ(0, default_config->getPingChannelThreads());
++
++ //Create a valid configuration.
++ std::string valid_json_cfg =
++ R"({
++ "enable-ping-check" : false,
++ "min-ping-requests" : 2,
++ "reply-timeout" : 250,
++ "ping-cltt-secs" : 90,
++ "ping-channel-threads" : 3
++ })";
++
++ auto parameters = Element::fromJSON(valid_json_cfg);
++
++ // Parse it.
++ ASSERT_NO_THROW_LOG(mgr->configure(parameters));
++
++ // Verify updated global configuration.
++ auto& config = mgr->getGlobalConfig();
++ ASSERT_TRUE(config);
++ EXPECT_FALSE(config->getEnablePingCheck());
++ EXPECT_EQ(2, config->getMinPingRequests());
++ EXPECT_EQ(250, config->getReplyTimeout());
++ EXPECT_EQ(90, config->getPingClttSecs());
++ EXPECT_EQ(3, config->getPingChannelThreads());
++
++ // Create an invalid configuration.
++ std::string invalid_json_cfg =
++ R"({
++ "enable-ping-check" : true,
++ "min-ping-requests" : 4,
++ "reply-timeout" : 500,
++ "ping-cltt-secs" : 45,
++ "ping-channel-threads" : 6,
++ "bogus" : 0
++ })";
++
++ parameters = Element::fromJSON(invalid_json_cfg);
++
++ // Parsing it should throw.
++ ASSERT_THROW_MSG(mgr->configure(parameters), DhcpConfigError, "spurious 'bogus' parameter");
++
++ // Verify configuration values were left unchanged.
++ auto& final_config = mgr->getGlobalConfig();
++ ASSERT_TRUE(final_config);
++ EXPECT_EQ(final_config->getEnablePingCheck(), config->getEnablePingCheck());
++ EXPECT_EQ(final_config->getMinPingRequests(), config->getMinPingRequests());
++ EXPECT_EQ(final_config->getReplyTimeout(), config->getReplyTimeout());
++ EXPECT_EQ(final_config->getPingClttSecs(), config->getPingClttSecs());
++ EXPECT_EQ(final_config->getPingChannelThreads(), config->getPingChannelThreads());
++}
++
++/// @brief Defines a callback to invoke at the bottom of sendCompleted()
++typedef std::function<void(const ICMPMsgPtr& echo, bool send_failed)> SendCompletedCallback;
++
++/// @brief Defines a callback to invoke at the bottom of replyReceived()
++typedef std::function<void(const ICMPMsgPtr& reply)> ReplyReceivedCallback;
++
++/// @brief Testable derivation of PingCheckMgr
++///
++/// Uses a TestablePingChannel to facilitate more robust testing.
++class TestablePingCheckMgr : public PingCheckMgr {
++public:
++ /// @brief Constructor.
++ ///
++ /// @param num_threads number of threads to use in the thread pool (0 means follow
++ /// core thread pool size)
++ /// @param min_echos minimum number of ECHO REQUESTs sent without replies
++ /// received required to declare an address free to offer. Defaults to 1,
++ /// must be greater than zero.
++ /// @param reply_timeout maximum number of milliseconds to wait for an
++ /// ECHO REPLY after an ECHO REQUEST has been sent. Defaults to 100,
++ TestablePingCheckMgr(uint32_t num_threads, uint32_t min_echos = 1,
++ uint32_t reply_timeout = 100)
++ : PingCheckMgr(num_threads, min_echos, reply_timeout),
++ post_send_completed_cb_(SendCompletedCallback()),
++ post_reply_received_cb_(ReplyReceivedCallback()) {
++ }
++
++ /// @brief Destructor.
++ virtual ~TestablePingCheckMgr() {
++ post_send_completed_cb_ = SendCompletedCallback();
++ post_reply_received_cb_ = ReplyReceivedCallback();
++ if (getIOService()) {
++ getIOService()->stopAndPoll();
++ }
++ }
++
++ /// @brief Fetch the current channel instance.
++ ///
++ /// @return pointer to the TestablePingChannel instance (or an empty pointer).
++ TestablePingChannelPtr getChannel() {
++ return (boost::dynamic_pointer_cast<TestablePingChannel>(channel_));
++ }
++
++ /// @brief Fetches the manager's context store.
++ ///
++ /// @return Pointer to the PingContextStore.
++ PingContextStorePtr getStore() {
++ return (store_);
++ }
++
++ /// @brief Fetches the expiration timer's current interval (in milliseconds).
++ ///
++ /// @return current interval as long or 0L if the timer is not currently
++ /// running or does not exist.
++ long getExpirationTimerInterval() {
++ if (expiration_timer_) {
++ return (expiration_timer_->getInterval());
++ }
++
++ return (0);
++ }
++
++protected:
++ /// @brief Creates a TestablePingChannel instance.
++ ///
++ /// This override the base case creator.
++ ///
++ /// @param io_service IOService that will drive the channel.
++ /// @return pointer to the newly created channel.
++ virtual PingChannelPtr createChannel(asiolink::IOServicePtr io_service) {
++ return (TestablePingChannelPtr(
++ new TestablePingChannel(io_service,
++ std::bind(&PingCheckMgr::nextToSend, this, ph::_1),
++ std::bind(&TestablePingCheckMgr::sendCompleted,
++ this, ph::_1, ph::_2),
++ std::bind(&TestablePingCheckMgr::replyReceived, this, ph::_1),
++ std::bind(&PingCheckMgr::channelShutdown, this))));
++ }
++
++public:
++ /// @brief Fetches the current size of the parking lot.
++ ///
++ /// @return size_t containing the number of entries parked.
++ size_t parkingLotSize() const {
++ auto const& parking_lot = ServerHooks::getServerHooks().getParkingLotPtr("lease4_offer");
++ return (parking_lot->size());
++ }
++
++ /// @brief Callback passed to PingChannel to invoke when an ECHO REQUEST
++ /// send has completed.
++ ///
++ /// -# Invokes the base class implementation
++ /// -# Invokes an optional callback
++ ///
++ /// @param echo ICMP echo message that is sent.
++ /// @param send_failed True if the send completed with a non-fatal error,
++ /// false otherwise.
++ virtual void sendCompleted(const ICMPMsgPtr& echo, bool send_failed) {
++ // Call the production callback.
++ PingCheckMgr::sendCompleted(echo, send_failed);
++
++ // Invoke the post check, if one.
++ if (post_send_completed_cb_) {
++ (post_send_completed_cb_)(echo, send_failed);
++ }
++ }
++
++ /// @brief Callback invoked by the channel to process received ICMP messages.
++ ///
++ /// -# Invokes the base class implementation
++ /// -# Pauses the test IOService thread and returns if the parking lot is empty
++ /// -# Invokes an option callback passing in the reply received
++ ///
++ /// @param reply pointer to the ICMP message received.
++ virtual void replyReceived(const ICMPMsgPtr& reply) {
++ if (reply->getType() == ICMPMsg::ECHO_REQUEST) {
++ return;
++ }
++
++ // If we're routing loopback messages, look up the original address based
++ // on the sequence number and use it as the reply's source address.
++ if (getChannel()->route_loopback_) {
++ IOAddress address = getChannel()->loopback_map_.find(reply->getSequence());
++ if (address != IOAddress::IPV4_ZERO_ADDRESS()) {
++ reply->setSource(address);
++ }
++ }
++
++ // Call the production callback.
++ PingCheckMgr::replyReceived(reply);
++
++ // Invoke the post check, if one.
++ if (post_reply_received_cb_) {
++ (post_reply_received_cb_)(reply);
++ }
++ }
++
++ /// @brief Fetches the thread pool (if it exists).
++ ///
++ /// @return pointer to theIoServiceThreadPool. Will be empty
++ /// in ST mode or if the manager has not been started.
++ asiolink::IoServiceThreadPoolPtr getThreadPool() {
++ return (thread_pool_);
++ }
++
++ /// @brief Sets the network_state object.
++ ///
++ /// @param network_state pointer to a NetworkState instance.
++ void setNetworkState(NetworkStatePtr network_state) {
++ network_state_ = network_state;
++ }
++
++ /// @brief Callback to invoke at the bottom of sendCompleted().
++ SendCompletedCallback post_send_completed_cb_;
++
++ /// @brief Callback to invoke at the bottom of replyReceived().
++ ReplyReceivedCallback post_reply_received_cb_;
++};
++
++/// @brief Defines a shared pointer to a PingCheckMgr.
++typedef boost::shared_ptr<TestablePingCheckMgr> TestablePingCheckMgrPtr;
++
++/// @brief Holds a lease and its associated query.
++struct LeaseQueryPair {
++public:
++ /// @brief Constructor.
++ ///
++ /// @param lease pointer to the lease.
++ /// @param query pointer to the query.
++ LeaseQueryPair(Lease4Ptr lease, Pkt4Ptr query) : lease_(lease), query_(query) {
++ };
++
++ /// @brief Pointer to the lease.
++ Lease4Ptr lease_;
++
++ /// @brief Pointer to the query.
++ Pkt4Ptr query_;
++};
++
++/// @brief Container of leases and their associated queries.
++typedef std::vector<LeaseQueryPair> LeaseQueryPairs;
++
++/// @brief Test fixture for exercising PingCheckMgr.
++///
++/// Uses a TestablePingCheckMgr instance for all tests and
++/// provides numerous helper functions.
++class PingCheckMgrTest : public IOServiceTest {
++public:
++ /// @brief Constructor.
++ PingCheckMgrTest() : mgr_(), lease_query_pairs_(), mutex_(new mutex()),
++ test_start_time_(PingContext::now()), unparked_(0) {
++ MultiThreadingMgr::instance().setMode(false);
++ };
++
++ /// @brief Destructor.
++ virtual ~PingCheckMgrTest() {
++ test_timer_.cancel();
++ test_io_service_->stopAndPoll();
++ MultiThreadingMgr::instance().setMode(false);
++ }
++
++ /// @brief Pretest setup.
++ ///
++ /// Registers the hook point and creates its parking lot.
++ virtual void SetUp() {
++ HooksManager::registerHook("lease4_offer");
++ parking_lot_ = boost::make_shared<ParkingLotHandle>(
++ ServerHooks::getServerHooks().getParkingLotPtr("lease4_offer"));
++ }
++
++ /// @brief Ensure we stop cleanly.
++ virtual void TearDown() {
++ if (mgr_) {
++ mgr_->stop();
++ }
++
++ HooksManager::clearParkingLots();
++ }
++
++ /// @brief Creates the test's manager instance.
++ ///
++ /// @param num_threads number of threads in the thread pool.
++ /// @param min_echos minimum number of echos per ping check.
++ /// @param reply_timeout reply timeout per ping.
++ /// @param start_and_pause when false, the manager is only created,
++ /// when true it is created, started and then paused. This allows
++ /// manipulation of context store contents while the threads are doing
++ /// no work.
++ void createMgr(uint32_t num_threads,
++ uint32_t min_echos = 1,
++ uint32_t reply_timeout = 100,
++ bool start_and_pause = false) {
++ ASSERT_NO_THROW_LOG(
++ mgr_.reset(new TestablePingCheckMgr(num_threads, min_echos, reply_timeout)));
++ ASSERT_TRUE(mgr_);
++ mgr_->setIOService(test_io_service_);
++
++ if (start_and_pause) {
++ ASSERT_NO_THROW_LOG(mgr_->start());
++
++ if (!MultiThreadingMgr::instance().getMode()) {
++ ASSERT_FALSE(mgr_->getThreadPool());
++ } else {
++ ASSERT_TRUE(mgr_->getThreadPool());
++ ASSERT_NO_THROW_LOG(mgr_->pause());
++ ASSERT_TRUE(mgr_->isPaused());
++ }
++ }
++ }
++
++ /// @brief Add a new lease and query pair to the test's list of lease query pairs.
++ ///
++ /// Creates a bare-bones DHCPv4 lease and DHCPDISCOVER, wraps them in a
++ /// LeaseQueryPair and adds the pair to the end of the test's internal
++ /// list of pairs, lease_query_pairs_.
++ ///
++ /// @param target IOAddress of the lease.
++ /// @param transid transaction id of the query.
++ ///
++ /// @return A copy of the newly created pair.
++ LeaseQueryPair makeLeaseQueryPair(IOAddress target, uint16_t transid) {
++ // Make a lease and query pair
++ Lease4Ptr lease(new Lease4());
++ lease->addr_ = IOAddress(target);
++ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, transid));
++ LeaseQueryPair lqp(lease, query);
++ lease_query_pairs_.push_back(lqp);
++ return (lqp);
++ }
++
++ /// @brief Start ping checks for a given number of targets.
++ ///
++ /// The function first creates and parks the given number of targets, and
++ /// then starts a ping check for each of them. Parking them all first
++ /// establishes the number of ping checks expected to be conducted during
++ /// the test prior to actually starting any of them. This avoids the
++ /// parking lot from becoming empty part way through the test.
++ ///
++ /// It unpark callback lambda increments the unparked_ counter and then
++ /// pushes the unparked lease/query pair to either the list of frees
++ /// or list of declines.
++ ///
++ /// @param num_targets number of target ip addresses to ping check.
++ /// @param start_address starting target address. Defaults to 127.0.0.1.
++ ///
++ /// @return last target address started.
++ IOAddress startTargets(size_t num_targets, IOAddress start_address = IOAddress("127.0.0.1")) {
++ IOAddress target = start_address;
++ for (auto i = 0; i < num_targets; ++i) {
++ auto lqp = makeLeaseQueryPair(IOAddress(target), i+1);
++ HooksManager::park("lease4_offer", lqp.query_,
++ [this, lqp]() {
++ MultiThreadingLock lock(*mutex_);
++ ++unparked_;
++ auto handle = lqp.query_->getCalloutHandle();
++ bool offer_address_in_use;
++ handle->getArgument("offer_address_in_use", offer_address_in_use);
++ offer_address_in_use ? declines_.push_back(lqp) : frees_.push_back(lqp);
++ });
++
++ try {
++ mgr_->startPing(lqp.lease_, lqp.query_, parking_lot_);
++ } catch (const std::exception& ex) {
++ ADD_FAILURE() << "startPing threw: " << ex.what();
++ }
++
++ target = IOAddress::increase(target);
++ }
++
++ return(target);
++ }
++
++ /// @brief Fetches the context, by lease address, from the store for a
++ /// given lease query pair.
++ ///
++ /// @param lqp LeaseQueryPair for which the context is desired.
++ /// @return pointer to the found context or an empty pointer.
++ PingContextPtr getContext(const LeaseQueryPair& lqp) {
++ return (getContext(lqp.lease_->addr_));
++ }
++
++ /// @brief Fetches the context, by lease address, from the store for address.
++ ///
++ /// @param address lease ip address for which the context is desired.
++ /// @return pointer to the found context or an empty pointer.
++ PingContextPtr getContext(const IOAddress& address) {
++ return (mgr_->getStore()->getContextByAddress(address));
++ }
++
++ /// @brief Updates a context in the store.
++ ///
++ /// @param context context to update.
++ void updateContext(PingContextPtr& context) {
++ ASSERT_NO_THROW_LOG(mgr_->getStore()->updateContext(context));
++ }
++
++ /// @brief Tests equality of two timestamps within a given tolerance.
++ ///
++ /// The two time stamps are considered equal if the absolute value of their
++ /// difference is between 0 and the specified tolerance (inclusive).
++ ///
++ /// @param lhs first TimeStamp to compare.
++ /// @param rhs second TimeStamp to compare.
++ /// @param tolerance margin of difference allowed for equality in milliseconds.
++ /// Defaults to 10.
++ ///
++ /// @return True if the time stamps are "equal", false otherwise.
++ bool fuzzyEqual(const TimeStamp& lhs, const TimeStamp& rhs, long tolerance = 10) {
++ auto diff = abs(duration_cast<milliseconds>(lhs - rhs).count());
++ return (diff >= 0 && diff <= tolerance);
++ }
++
++ /// @brief Tests equality of two longs within a given tolerance.
++ ///
++ /// The two values are considered equal if the absolute value of their
++ /// difference is between 0 and the specified tolerance (inclusive).
++ ///
++ /// @param lhs first value to compare.
++ /// @param rhs second value to compare.
++ /// @param tolerance margin of difference allowed for equality in milliseconds.
++ /// Defaults to 10.
++ ///
++ /// @return True if the time values are "equal", false otherwise.
++ bool fuzzyEqual(const long& lhs, const long& rhs, long tolerance = 10) {
++ auto diff = abs(lhs - rhs);
++ return (diff >= 0 && diff <= tolerance);
++ }
++
++ /// @brief Creates an ECHO REQUEST message from a given address.
++ ///
++ /// @param target ip address to use as the echo's destination address.
++ /// @return Pointer to the new message.
++ ICMPMsgPtr makeEchoRequest(const IOAddress& target) {
++ ICMPMsgPtr msg(new ICMPMsg());
++ msg->setType(ICMPMsg::ECHO_REQUEST);
++ msg->setDestination(IOAddress(target));
++ msg->setSource(IOAddress("127.0.0.1"));
++ return (msg);
++ }
++
++ /// @brief Creates an ECHO_REPLY message from a given address.
++ ///
++ /// @param from ip address to use as the reply's source address.
++ /// @return Pointer to the new message.
++ ICMPMsgPtr makeEchoReply(const IOAddress& from) {
++ ICMPMsgPtr msg(new ICMPMsg());
++ msg->setType(ICMPMsg::ECHO_REPLY);
++ msg->setSource(IOAddress(from));
++ msg->setDestination(IOAddress("127.0.0.1"));
++ return (msg);
++ }
++
++ /// @brief Creates an TARGET_UNREACHABLE message from a given address.
++ ///
++ /// @param target ip address to use as the reply's source address.
++ /// @return Pointer to the new message.
++ ICMPMsgPtr makeUnreachable(const IOAddress& target) {
++ // Make the TARGET_UNREACHABLE message first.
++ ICMPMsgPtr msg(new ICMPMsg());
++ msg->setType(ICMPMsg::TARGET_UNREACHABLE);
++ msg->setSource(IOAddress("127.0.0.1"));
++ msg->setDestination(IOAddress("127.0.0.1"));
++
++ // Now embed the ping target's "original" echo into the unreachable
++ // message's payload. This includes the IP header followed by the
++ // ECHO REQUEST. First make the IP header and add it to the payload.
++ // We only set values we care about.
++ struct ip ip_header;
++ memset((void *)(&ip_header), 0x00, sizeof(struct ip));
++ ip_header.ip_v = 4;
++ ip_header.ip_hl = 5; /* shift left twice = 20 */
++ ip_header.ip_len = 48; /* ip_header + echo length */
++ ip_header.ip_dst.s_addr = htonl(target.toUint32());
++ ip_header.ip_src.s_addr = htonl(msg->getSource().toUint32());
++ msg->setPayload((const uint8_t*)(&ip_header), sizeof(struct ip));
++
++ // Now make the ECHO_REQUEST, pack it and add that to the payload.
++ ICMPMsgPtr echo = makeEchoRequest(target);
++ ICMPPtr packed_echo = echo->pack();
++ msg->setPayload((const uint8_t*)(packed_echo.get()), sizeof(struct icmp));
++
++ return (msg);
++ }
++
++ /// @brief Compares a LeaseQueryPair collection to the internal collection
++ /// of pairs created (see makeLeaseQueryPairs()).
++ ///
++ /// @param test_collection Collection of pairs to compare against those in
++ /// the creation collection.
++ void compareLeaseQueryPairs(LeaseQueryPairs& test_collection) {
++ // We should have as many in the test_collection as we have creation
++ // collection.
++ ASSERT_EQ(test_collection.size(), lease_query_pairs_.size());
++
++ // Order is not guaranteed so we sort both lists then compare.
++ std::sort(test_collection.begin(), test_collection.end(),
++ [](LeaseQueryPair const& a, LeaseQueryPair const& b)
++ { return (a.lease_->addr_ < b.lease_->addr_); });
++
++ std::sort(lease_query_pairs_.begin(), lease_query_pairs_.end(),
++ [](LeaseQueryPair const& a, LeaseQueryPair const& b)
++ { return (a.lease_->addr_ < b.lease_->addr_); });
++
++ auto dpi = test_collection.begin();
++ for (auto const& lqpi : lease_query_pairs_) {
++ ASSERT_EQ((*dpi).lease_->addr_, lqpi.lease_->addr_);
++ ++dpi;
++ }
++ }
++
++ /// @brief Exercises the operational basics: create, start, and stop
++ /// for TestablePingCheckMgr.
++ ///
++ /// @param num_threads number of threads in the thread pool.
++ void testOperationalBasics(size_t num_threads) {
++ SKIP_IF(notRoot());
++
++ // Create manager with the given number of threads.
++ ASSERT_NO_THROW_LOG(createMgr(num_threads));
++ ASSERT_TRUE(mgr_);
++
++ // Should not be running.
++ EXPECT_FALSE(mgr_->isRunning());
++ EXPECT_TRUE(mgr_->isStopped());
++ EXPECT_FALSE(mgr_->isPaused());
++
++ // Channel should not yet exist.
++ ASSERT_FALSE(mgr_->getChannel());
++
++ // Start the manager.
++ ASSERT_NO_THROW_LOG(mgr_->start());
++
++ // Thread pool should exist in MT mode only.
++ if (MultiThreadingMgr::instance().getMode()) {
++ ASSERT_TRUE(mgr_->getThreadPool());
++ } else {
++ ASSERT_FALSE(mgr_->getThreadPool());
++ }
++
++ // Should be running.
++ EXPECT_TRUE(mgr_->isRunning());
++ EXPECT_FALSE(mgr_->isStopped());
++ EXPECT_FALSE(mgr_->isPaused());
++
++ // Channel should exist and be open.
++ auto channel = mgr_->getChannel();
++ ASSERT_TRUE(channel);
++ ASSERT_TRUE(channel->isOpen());
++
++ // Context store should exist and be empty.
++ auto store = mgr_->getStore();
++ ASSERT_TRUE(store);
++ auto pings = store->getAll();
++ ASSERT_EQ(0, pings->size());
++
++ // Destruction should be graceful.
++ ASSERT_NO_THROW_LOG(mgr_.reset());
++ }
++
++ /// @brief Verifies that startPing() creates a new context in the store and
++ /// it can be fetched with the nextToSend() callback.
++ void testStartPing() {
++ SKIP_IF(notRoot());
++
++ // Create manager with thread-pool size of 3, min_echos 2, reply_timeout 250 ms.
++ // ST mode should ingore requested thread number.
++ ASSERT_NO_THROW_LOG(createMgr(3, 2, 250));
++ ASSERT_TRUE(mgr_);
++
++ // Make a lease and query pair
++ auto lqp1 = makeLeaseQueryPair(IOAddress("127.0.0.101"), 101);
++
++ // Channel isn't open, startPing should throw.
++ ASSERT_THROW_MSG(mgr_->startPing(lqp1.lease_, lqp1.query_, parking_lot_), InvalidOperation,
++ "PingCheckMgr::startPing() - channel isn't open");
++
++ // Start the manager. This will open the channel.
++ ASSERT_NO_THROW_LOG(mgr_->start());
++ ASSERT_TRUE(mgr_->isRunning());
++
++ if (mgr_->getThreadPool()) {
++ // Pause the manager so startPing() will succeed but no events will occur.
++ // This should let us add contexts that sit in WAITING_TO_SEND state.
++ ASSERT_NO_THROW_LOG(mgr_->pause());
++ ASSERT_TRUE(mgr_->isPaused());
++ }
++
++ // Call startPing() again. It should work.
++ ASSERT_NO_THROW_LOG(mgr_->startPing(lqp1.lease_, lqp1.query_, parking_lot_));
++
++ // Calling startPing() on the same lease should fail, duplicates not allowed.
++ ASSERT_THROW_MSG(mgr_->startPing(lqp1.lease_, lqp1.query_, parking_lot_), DuplicateContext,
++ "PingContextStore::addContex: context already exists for: 127.0.0.101");
++
++ // Our context should be present.
++ auto const& store = mgr_->getStore();
++ auto pings = store->getAll();
++ ASSERT_EQ(1, pings->size());
++ PingContextPtr context1;
++ ASSERT_NO_THROW_LOG(context1 = store->getContextByAddress(lqp1.lease_->addr_));
++ ASSERT_TRUE(context1);
++
++ // Verify the context's state.
++ EXPECT_EQ(2, context1->getMinEchos());
++ EXPECT_EQ(250, context1->getReplyTimeout());
++ EXPECT_EQ(0, context1->getEchosSent());
++ EXPECT_EQ(PingContext::EMPTY_TIME(), context1->getLastEchoSentTime());
++ EXPECT_LE(test_start_time_, context1->getSendWaitStart());
++ EXPECT_EQ(PingContext::EMPTY_TIME(), context1->getNextExpiry());
++ EXPECT_LE(test_start_time_, context1->getCreatedTime());
++ EXPECT_EQ(lqp1.lease_, context1->getLease());
++ EXPECT_EQ(lqp1.query_, context1->getQuery());
++ EXPECT_EQ(PingContext::WAITING_TO_SEND, context1->getState());
++
++ // Sleep a bit to make sure there's a difference in context times.
++ usleep(5);
++
++ // Make a second lease and query pair
++ auto lqp2 = makeLeaseQueryPair(IOAddress("127.0.0.102"), 102);
++
++ // Start a ping for lease2.
++ ASSERT_NO_THROW_LOG(mgr_->startPing(lqp2.lease_, lqp2.query_, parking_lot_));
++
++ // Both contexts should be present.
++ pings = store->getAll();
++ ASSERT_EQ(2, pings->size());
++
++ // Fetch the second context by address.
++ PingContextPtr context2;
++ ASSERT_NO_THROW_LOG(context2 = store->getContextByAddress(lqp2.lease_->addr_));
++ ASSERT_TRUE(context2);
++
++ // Verify the second context's state.
++ EXPECT_EQ(2, context2->getMinEchos());
++ EXPECT_EQ(250, context2->getReplyTimeout());
++ EXPECT_EQ(0, context2->getEchosSent());
++ EXPECT_EQ(PingContext::EMPTY_TIME(), context2->getLastEchoSentTime());
++ // Its send_wait_start_time_ should be more recent than context1.
++ EXPECT_LE(context1->getSendWaitStart(), context2->getSendWaitStart());
++ EXPECT_EQ(PingContext::EMPTY_TIME(), context2->getNextExpiry());
++ // Its created_time_ should be more recent than context1.
++ EXPECT_LE(context1->getCreatedTime(), context2->getCreatedTime());
++ EXPECT_EQ(lqp2.lease_, context2->getLease());
++ EXPECT_EQ(lqp2.query_, context2->getQuery());
++ EXPECT_EQ(PingContext::WAITING_TO_SEND, context2->getState());
++ }
++
++ /// @brief Exercises PingCheckMgr::nextToSend().
++ void testNextToSend() {
++ SKIP_IF(notRoot());
++
++ // Create a paused manager. 3 threads, 2 echos, 250 ms timeout.
++ // ST mode should ingore requested thread number.
++ createMgr(3, 2, 250, true);
++
++ // Calling nextToSend() should return false.
++ IOAddress next("0.0.0.0");
++ ASSERT_FALSE(mgr_->nextToSend(next));
++
++ // Now let's start 3 contexts.
++ size_t num_targets = 3;
++ IOAddress target("127.0.0.1");
++ for (auto i = 0; i < num_targets; ++i) {
++ auto lqp = makeLeaseQueryPair(IOAddress(target), i+1);
++
++ // Call startPing().
++ ASSERT_NO_THROW_LOG(mgr_->startPing(lqp.lease_, lqp.query_, parking_lot_));
++ target = IOAddress::increase(target);
++
++ PingContextPtr context = getContext(lqp);
++ ASSERT_TRUE(context);
++
++ // Verify the context's initial state is correct.
++ EXPECT_EQ(0, context->getEchosSent());
++ EXPECT_EQ(PingContext::EMPTY_TIME(), context->getLastEchoSentTime());
++ EXPECT_LE(test_start_time_, context->getSendWaitStart());
++ EXPECT_EQ(PingContext::EMPTY_TIME(), context->getNextExpiry());
++ EXPECT_LE(test_start_time_, context->getCreatedTime());
++ EXPECT_EQ(PingContext::WAITING_TO_SEND, context->getState());
++
++ // Sleep a few before we add the next one to ensure ordering by
++ // time is consistent.
++ usleep(5);
++ }
++
++ // Consecutive calls to nextToSend() should return target addresses
++ // in the order they were created.
++ for (auto const& lqp : lease_query_pairs_) {
++ // Next to send should return the next address to send.
++ ASSERT_TRUE(mgr_->nextToSend(next));
++
++ // It should match the lease as created.
++ ASSERT_EQ(next, lqp.lease_->addr_);
++
++ // Fetch the corresponding context.
++ PingContextPtr context = getContext(next);
++ ASSERT_TRUE(context);
++
++ // Verify the state has properly moved to SENDING.
++ EXPECT_EQ(0, context->getEchosSent());
++ EXPECT_EQ(PingContext::EMPTY_TIME(), context->getLastEchoSentTime());
++ EXPECT_EQ(PingContext::EMPTY_TIME(), context->getNextExpiry());
++ EXPECT_EQ(PingContext::SENDING, context->getState());
++ }
++
++ // A final call to nextToSend should return false.
++ ASSERT_FALSE(mgr_->nextToSend(next));
++ }
++
++ /// @brief Exercises PingCheckMgr::setNextExpiration.
++ void testSetNextExpiration() {
++ SKIP_IF(notRoot());
++
++ // Create a paused manager. 3 threads, 2 echos, 500 ms timeout.
++ // ST mode should ingore requested thread number.
++ createMgr(3, 2, 500, true);
++
++ // Should not have an expiration time, timer should not be running.
++ ASSERT_EQ(PingContext::EMPTY_TIME(), mgr_->getNextExpiry());
++ ASSERT_EQ(mgr_->getExpirationTimerInterval(), 0);
++
++ // Now let's start 3 contexts.
++ size_t num_targets = 3;
++ IOAddress target("127.0.0.1");
++ for (auto i = 0; i < num_targets; ++i) {
++ auto lqp = makeLeaseQueryPair(IOAddress(target), i+1);
++
++ // Call startPing().
++ ASSERT_NO_THROW_LOG(mgr_->startPing(lqp.lease_, lqp.query_, parking_lot_));
++ target = IOAddress::increase(target);
++ }
++
++ // Still should not have an expiration time nor running timer.
++ ASSERT_EQ(PingContext::EMPTY_TIME(), mgr_->getNextExpiry());
++ EXPECT_EQ(mgr_->getExpirationTimerInterval(), 0);
++
++ // Simulate a completed send for the second context.
++ PingContextPtr context2;
++ context2 = getContext(lease_query_pairs_[1]);
++ ASSERT_TRUE(context2);
++ context2->beginWaitingForReply(test_start_time_ - milliseconds(50));
++ updateContext(context2);
++
++ // Call setNextExpiration().
++ ASSERT_NO_THROW_LOG(mgr_->setNextExpiration());
++
++ // Refresh the context.
++ context2 = getContext(lease_query_pairs_[1]);
++
++ // Verify the mgr has the same next expiration as the context and
++ // that the expiration timer is running. Allow for some fudge in
++ // the checks.
++ auto original_mgr_expiry = mgr_->getNextExpiry();
++ EXPECT_TRUE(fuzzyEqual(original_mgr_expiry, context2->getNextExpiry()));
++
++ auto original_interval = mgr_->getExpirationTimerInterval();
++ EXPECT_TRUE(fuzzyEqual(original_interval, 450));
++
++ // Simulate a completed send for the third context.
++ PingContextPtr context3;
++ context3 = getContext(lease_query_pairs_[2]);
++ ASSERT_TRUE(context3);
++ context3->beginWaitingForReply();
++ updateContext(context3);
++
++ // Call setNextExpiration().
++ ASSERT_NO_THROW_LOG(mgr_->setNextExpiration());
++
++ // Refresh the context.
++ context3 = getContext(lease_query_pairs_[2]);
++
++ // Context3 should have a later expiration than context2.
++ EXPECT_LT(context2->getNextExpiry(), context3->getNextExpiry());
++
++ // Expiration and timer should still match the original values based on
++ // the second context.
++ EXPECT_TRUE(fuzzyEqual(mgr_->getNextExpiry(), original_mgr_expiry));
++ EXPECT_EQ(mgr_->getExpirationTimerInterval(), original_interval);
++
++ // Simulate a completed send for the first context but use a smaller
++ // timeout and back date it.
++ PingContextPtr context1;
++ context1 = getContext(lease_query_pairs_[0]);
++ ASSERT_TRUE(context1);
++ context1->setReplyTimeout(50);
++ context1->beginWaitingForReply(test_start_time_ - milliseconds(1));
++ updateContext(context1);
++
++ // Call setNextExpiration().
++ ASSERT_NO_THROW_LOG(mgr_->setNextExpiration());
++
++ // Refresh the context.
++ context1 = getContext(lease_query_pairs_[0]);
++
++ // Context1 should have a earlier expiration than context2.
++ EXPECT_LT(context1->getNextExpiry(), context2->getNextExpiry());
++ // Timer interval should be based on context1.
++ EXPECT_TRUE(fuzzyEqual(mgr_->getExpirationTimerInterval(), 50, 20))
++ << " interval: " << mgr_->getExpirationTimerInterval();
++
++ // Move all contexts to TARGET_FREE. This should leave none
++ // still waiting.
++ context1->setState(PingContext::TARGET_FREE);
++ updateContext(context1);
++ context2->setState(PingContext::TARGET_FREE);
++ updateContext(context2);
++ context3->setState(PingContext::TARGET_FREE);
++ updateContext(context3);
++
++ // Call setNextExpiration().
++ ASSERT_NO_THROW_LOG(mgr_->setNextExpiration());
++
++ // Should not have an expiration time, timer should not be running.
++ ASSERT_EQ(PingContext::EMPTY_TIME(), mgr_->getNextExpiry());
++ ASSERT_EQ(mgr_->getExpirationTimerInterval(), 0);
++ }
++
++ /// @brief Exercises PingCheckMgr::sendCompleted.
++ void testSendCompleted() {
++ SKIP_IF(notRoot());
++
++ // Create a paused manager. 3 threads, 2 echos, 500 ms timeout.
++ // ST mode should ingore requested thread number.
++ createMgr(3, 2, 500, true);
++
++ // Start a ping for an address so we have a context.
++ IOAddress target("127.0.0.2");
++ auto lqp = makeLeaseQueryPair(IOAddress(target), 102);
++
++ // Call startPing().
++ ASSERT_NO_THROW_LOG(mgr_->startPing(lqp.lease_, lqp.query_, parking_lot_));
++
++ // Simulate a completed send for the context.
++ PingContextPtr context;
++ context = getContext(lqp);
++ ASSERT_TRUE(context);
++
++ // Make an ECHO REQUEST packet based on context.
++ ICMPMsgPtr echo_request = makeEchoRequest(context->getLease()->addr_);
++
++ // Invoke sendCompleted() with fabricated request. Should succeed.
++ ASSERT_NO_THROW_LOG(mgr_->sendCompleted(echo_request, false));
++
++ // Refresh the context.
++ context = getContext(context->getLease()->addr_);
++
++ EXPECT_EQ(PingContext::WAITING_FOR_REPLY, context->getState());
++ EXPECT_EQ(1, context->getEchosSent());
++ EXPECT_GE(context->getLastEchoSentTime(), test_start_time_);
++
++ // Verify the mgr has the same next expiration as the context and
++ // that the expiration timer is running. Allow for some fudge in
++ // the checks.
++ EXPECT_GT(context->getNextExpiry(), test_start_time_);
++ EXPECT_TRUE(fuzzyEqual(mgr_->getNextExpiry(), context->getNextExpiry()));
++ EXPECT_TRUE(fuzzyEqual(mgr_->getExpirationTimerInterval(), 500));
++
++ // Make an ECHO REQUEST packet for an address that has no context.
++ echo_request = makeEchoRequest(IOAddress("192.168.0.1"));
++
++ // Invoking sendCompleted() with request for a non-existent address be harmless.
++ ASSERT_NO_THROW_LOG(mgr_->sendCompleted(echo_request, false));
++
++ // Invoking sendCompleted() with an invalid message type should be harmless.
++ echo_request->setType(ICMPMsg::ECHO_REPLY);
++ ASSERT_NO_THROW_LOG(mgr_->sendCompleted(ICMPMsgPtr(), false));
++
++ // Invoking sendCompleted() with an empty message should be harmless.
++ echo_request.reset();
++ ASSERT_NO_THROW_LOG(mgr_->sendCompleted(ICMPMsgPtr(), false));
++
++ // Verify expiration values should not have not been altered.
++ EXPECT_TRUE(fuzzyEqual(mgr_->getNextExpiry(), context->getNextExpiry()));
++ EXPECT_TRUE(fuzzyEqual(mgr_->getExpirationTimerInterval(), 500));
++ }
++
++ /// @brief Exercises PingCheckMgr::replyReceived() for ECHO REPLYs. Note this
++ /// also exercises handleEchoReply().
++ void testReplyReceivedForEchoReply() {
++ SKIP_IF(notRoot());
++
++ // Create a paused manager. 3 threads, 2 echos, 500 ms timeout.
++ // ST mode should ingore requested thread number.
++ createMgr(3, 2, 500, true);
++
++ // Install a post reply received callback to stop the test if we're done.
++ mgr_->post_reply_received_cb_ =
++ [this](const ICMPMsgPtr& /* reply */) {
++ MultiThreadingLock lock(*mutex_);
++ if (mgr_->parkingLotSize() == 0) {
++ stopTestService();
++ return;
++ }
++ };
++
++ // Turn off loopback routing.
++ mgr_->getChannel()->route_loopback_ = false;
++
++ // Start a ping for an address so we have a context.
++ startTargets(1);
++ auto lqp = lease_query_pairs_[0];
++
++ // Simulate a completed send for the context.
++ PingContextPtr context;
++ context = getContext(lqp);
++ ASSERT_TRUE(context);
++
++ // Make an ECHO REQUEST packet based on context and invoke sendCompleted().
++ ICMPMsgPtr echo_request = makeEchoRequest(context->getLease()->addr_);
++ ASSERT_NO_THROW_LOG(mgr_->sendCompleted(echo_request, false));
++
++ // Should still have one parked query.
++ EXPECT_EQ(1, mgr_->parkingLotSize());
++
++ // Verify the expiration timer is running.
++ EXPECT_TRUE(fuzzyEqual(mgr_->getExpirationTimerInterval(), 500));
++
++ // Make an ECHO REPLY packet based on context and invoke replyReceived().
++ ICMPMsgPtr echo_reply = makeEchoReply(context->getLease()->addr_);
++ ASSERT_NO_THROW_LOG(mgr_->replyReceived(echo_reply));
++
++ // Verify the expiration timer is no longer running.
++ EXPECT_EQ(mgr_->getExpirationTimerInterval(), 0);
++
++ // The context should no longer be in the store.
++ EXPECT_FALSE(getContext(lqp));
++
++ // We should have dropped the query from the lot rather than unparking it.
++ EXPECT_EQ(mgr_->parkingLotSize(), 0);
++ EXPECT_EQ(unparked_, 1);
++
++ // We should have one decline that matches our lease query pair.
++ compareLeaseQueryPairs(declines_);
++
++ // Make an ECHO REPLY packet for an address that has no context.
++ echo_reply = makeEchoReply(IOAddress("192.168.0.1"));
++
++ // Invoke replyReceived() for a reply with no matching context,
++ // it should not throw.
++ ASSERT_NO_THROW_LOG(mgr_->PingCheckMgr::replyReceived(echo_reply));
++
++ // Invoke replyReceived() an empty message, it should not throw.
++ // (Bypass test implementation for this check).
++ echo_reply.reset();
++ ASSERT_NO_THROW_LOG(mgr_->PingCheckMgr::replyReceived(echo_reply));
++ }
++
++ /// @brief Exercises PingCheckMgr::replyReceived() for UNREACHABLEs. Note this
++ /// also exercises handleTargetUnreachable().
++ void testReplyReceivedForTargetUnreachable() {
++ SKIP_IF(notRoot());
++
++ // Create a paused manager. 3 threads, 2 echos, 500 ms timeout.
++ // ST mode should ingore requested thread number.
++ createMgr(3, 2, 500, true);
++
++ // Install a post reply received callback to stop the test if we're done.
++ mgr_->post_reply_received_cb_ =
++ [this](const ICMPMsgPtr& /* reply */) {
++ MultiThreadingLock lock(*mutex_);
++ if (mgr_->parkingLotSize() == 0) {
++ stopTestService();
++ return;
++ }
++ };
++
++ // Turn off loopback routing.
++ mgr_->getChannel()->route_loopback_ = false;
++
++ // Start a ping for an address so we have a context.
++ startTargets(1);
++ auto lqp = lease_query_pairs_[0];
++
++ // Simulate a completed send for the context.
++ PingContextPtr context;
++ context = getContext(lqp);
++ ASSERT_TRUE(context);
++
++ // Make an ECHO REQUEST packet based on context and invoke sendCompleted().
++ ICMPMsgPtr echo_request = makeEchoRequest(context->getLease()->addr_);
++ ASSERT_NO_THROW_LOG(mgr_->sendCompleted(echo_request, false));
++
++ // Should still have one parked query.
++ EXPECT_EQ(1, mgr_->parkingLotSize());
++
++ // Verify the expiration timer is running.
++ EXPECT_TRUE(fuzzyEqual(mgr_->getExpirationTimerInterval(), 500));
++
++ // Make an ECHO REPLY packet based on context and invoke replyReceived().
++ ICMPMsgPtr unreachable = makeUnreachable(context->getLease()->addr_);
++ ASSERT_NO_THROW_LOG(mgr_->replyReceived(unreachable));
++
++ // Verify the expiration timer is no longer running.
++ EXPECT_EQ(mgr_->getExpirationTimerInterval(), 0);
++
++ // The context should no longer be in the store.
++ EXPECT_FALSE(getContext(lqp));
++
++ // We should have unparked the query from the lot.
++ EXPECT_EQ(mgr_->parkingLotSize(), 0);
++ EXPECT_EQ(unparked_, 1);
++
++ // We should have one free that matches our lease query pair.
++ compareLeaseQueryPairs(frees_);
++
++ // Invoke replyReceived() for an unreachable with no matching context,
++ // it should not throw.
++ unreachable = makeUnreachable(IOAddress("192.168.0.1"));
++ ASSERT_NO_THROW_LOG(mgr_->replyReceived(unreachable));
++ }
++
++ /// @brief Verifies expiration processing by invoking expirationTimedout().
++ /// This also exercises processExpiredSince(), doNextEcho(), finishFree(),
++ /// and setNextExpiration().
++ void testExpirationProcessing() {
++ SKIP_IF(notRoot());
++
++ // Create a paused manager. 3 threads, 1 echos, 250 ms timeout.
++ // ST mode should ingore requested thread number.
++ createMgr(3, 1, 250, true);
++
++ // Start four ping checks, then stage them so:
++ //
++ // First context is WAITING_TO_SEND, no expiry.
++ // Second context is WAITING_FOR_REPLY, has expired and has
++ // exhausted min_echos_.
++ // Third context is WAITING_FOR_REPLY, has expired but has
++ // not exhausted min_echos_.
++ // Fourth context is WAITING_FOR_REPLY but has not yet expired.
++ //
++ size_t num_targets = 4;
++
++ // Start the desired number of targets with an unpark callback
++ // that increments the unparked count.
++ startTargets(num_targets);
++
++ // Now establish the desired state for each context.
++ // First context is in WAITING_TO_SEND, no expiry.
++ PingContextPtr context1 = getContext(lease_query_pairs_[0]);
++ ASSERT_TRUE(context1);
++ EXPECT_EQ(context1->getState(), PingContext::WAITING_TO_SEND);
++
++ // Second context setup: expired and has exhausted min_echos_
++ PingContextPtr context2 = getContext(lease_query_pairs_[1]);
++ ASSERT_TRUE(context2);
++ context2->beginWaitingForReply(test_start_time_ - milliseconds(500));
++ updateContext(context2);
++
++ // Third context setup: expired but has not exhausted min_echos_
++ PingContextPtr context3 = getContext(lease_query_pairs_[2]);
++ ASSERT_TRUE(context3);
++ context3->setMinEchos(2);
++ context3->beginWaitingForReply(test_start_time_ - milliseconds(500));
++ updateContext(context3);
++
++ // Fourth context setup: has not yet expired
++ PingContextPtr context4 = getContext(lease_query_pairs_[3]);
++ ASSERT_TRUE(context4);
++ context4->beginWaitingForReply(test_start_time_);
++ updateContext(context4);
++
++ // Now invoke expirationTimedout().
++ ASSERT_NO_THROW_LOG(mgr_->expirationTimedOut());
++
++ // Verify the contexts are in the expected states.
++ // Context1 should still be WAITING_TO_SEND.
++ context1 = getContext(lease_query_pairs_[0]);
++ ASSERT_TRUE(context1);
++ EXPECT_EQ(context1->getState(), PingContext::WAITING_TO_SEND);
++
++ // Context2 should be gone by unparking and its address freed.
++ IOAddress address = lease_query_pairs_[1].lease_->addr_;
++ context2 = getContext(address);
++ ASSERT_FALSE(context2);
++ EXPECT_EQ(unparked_, 1);
++ ASSERT_EQ(frees_.size(), 1);
++ EXPECT_EQ(frees_[0].lease_->addr_, address);
++
++ // Context3 should be in WAITING_TO_SEND.
++ context3 = getContext(lease_query_pairs_[2]);
++ ASSERT_TRUE(context3);
++ EXPECT_EQ(context3->getState(), PingContext::WAITING_TO_SEND);
++
++ // Context4 should still be WAITING_FOR_REPLY.
++ context4 = getContext(lease_query_pairs_[3]);
++ ASSERT_TRUE(context4);
++ EXPECT_EQ(context4->getState(), PingContext::WAITING_FOR_REPLY);
++
++ // Manager's next_expiry_ should be based on context4?
++ EXPECT_TRUE(fuzzyEqual(mgr_->getNextExpiry(), context4->getNextExpiry()));
++ }
++
++ /// @brief Generates a number of ping checks to local loop back addresses.
++ ///
++ /// Pings should all result in ECHO_REPLYs that get "declined". Declined
++ /// addresses are added to a list. Test completion is gated by the parking
++ /// lot becoming empty or test times out.
++ void testMultiplePingsWithReply() {
++ SKIP_IF(notRoot());
++
++ // Create manager with thread-pool size of 3, min_echos 1,
++ // reply_timeout 1000 milliseconds. Larger time out for this test
++ // avoids sporadic expirations which leads to unaccounted for UNPARKs.
++ // ST mode should ingore requested thread number.
++ ASSERT_NO_THROW_LOG(createMgr(3, 1, 1000));
++ ASSERT_TRUE(mgr_);
++
++ // Install a post reply received callback to stop the test if we're done.
++ int num_targets = 25;
++ mgr_->post_reply_received_cb_ =
++ [this, num_targets](const ICMPMsgPtr& /* reply */) {
++ MultiThreadingLock lock(*mutex_);
++ if (unparked_ == num_targets) {
++ stopTestService();
++ return;
++ }
++ };
++
++ // Start the manager. This will open the channel.
++ ASSERT_NO_THROW_LOG(mgr_->start());
++ ASSERT_TRUE(mgr_->isRunning());
++
++ // Start the ping checks.
++ startTargets(num_targets);
++
++ // Run the main thread's IOService until we complete or timeout.
++ ASSERT_NO_THROW_LOG(runIOService());
++
++ // Stop the thread pool.
++ ASSERT_NO_THROW_LOG(mgr_->stop());
++ ASSERT_TRUE(mgr_->isStopped());
++
++ // Calling nextToSend() should return false.
++ IOAddress next("0.0.0.0");
++ ASSERT_FALSE(mgr_->nextToSend(next));
++
++ // We should have as many declines as we have pairs created.
++ compareLeaseQueryPairs(declines_);
++ }
++
++ /// @brief Generates a large number of ping checks to local loop back addresses.
++ ///
++ /// A pause is induced approximately halfway through the number of replies
++ /// at which point the manager is paused and then resumed. This is intended
++ /// to demonstrate the ability to pause and resume the manager gracefully.
++ /// The pings should all result in ECHO_REPLYs that get "declined". Declined
++ /// addresses are added to a list. Test completion is gated by the parking
++ /// lot becoming empty or test times out.
++ void testMultiplePingsWithReplyAndPause() {
++ SKIP_IF(notRoot());
++
++ // Create manager with thread-pool size of 3, min_echos 1,
++ // reply_timeout 1000 milliseconds. Larger time out for this test
++ // avoids sporadic expirations which leads to unaccounted for UNPARKs.
++ // ST mode should ingore requested thread number.
++ ASSERT_NO_THROW_LOG(createMgr(3, 1, 1000));
++ ASSERT_TRUE(mgr_);
++
++ // Generate ping checks to the desired number of targets.
++ // Set up the pause callback to pause at half the number of
++ // expected replies.
++ size_t num_targets = 24;
++ size_t reply_cnt = 0;
++ size_t pause_at = num_targets / 2;
++ bool test_paused = false;
++
++ // Install post reply callback to stop the test thread when we reach
++ // the pause count.
++ mgr_->post_reply_received_cb_ =
++ [this, &reply_cnt, &test_paused, &pause_at](const ICMPMsgPtr& reply) {
++ MultiThreadingLock lock(*mutex_);
++ if (reply->getType() == ICMPMsg::ECHO_REPLY) {
++ ++reply_cnt;
++ if (pause_at && (reply_cnt >= pause_at)) {
++ test_paused = true;
++ stopTestService();
++ pause_at = 0;
++ }
++ }
++ };
++
++ // Start the manager. This will open the channel.
++ ASSERT_NO_THROW_LOG(mgr_->start());
++ ASSERT_TRUE(mgr_->isRunning());
++ ASSERT_NO_THROW_LOG(mgr_->pause());
++
++ // Start 1/2 desired number of ping checks.
++ startTargets(num_targets / 2);
++
++ // Run the main thread's IOService until we pause or timeout.
++ ASSERT_NO_THROW_LOG(mgr_->resume());
++ ASSERT_TRUE(mgr_->isRunning());
++ ASSERT_NO_THROW_LOG(runIOService());
++
++ // Manager should still be running. Pause it.
++ ASSERT_TRUE(mgr_->isRunning());
++ if (mgr_->getThreadPool()) {
++ ASSERT_NO_THROW_LOG(mgr_->pause());
++ ASSERT_TRUE(mgr_->isPaused());
++ }
++
++ // Verify that the pause callback is why we stopped, that we
++ // received at least as many as we should have before pause
++ // and that we have more work to do. The test is a range as
++ // pausing does not happen exactly at the same point from test
++ // run to test run.
++ ASSERT_TRUE(test_paused);
++ ASSERT_TRUE((reply_cnt >= pause_at) && (reply_cnt < num_targets))
++ << "reply_cnt " << reply_cnt
++ << ", pause_at " << pause_at
++ << ", num_targets " << num_targets;
++
++ mgr_->post_reply_received_cb_ =
++ [this, num_targets](const ICMPMsgPtr& /* reply */) {
++ MultiThreadingLock lock(*mutex_);
++ if (unparked_ == num_targets) {
++ stopTestService();
++ return;
++ }
++ };
++
++ // Start second batch of targets.
++ startTargets(num_targets / 2, IOAddress("127.0.0.15"));
++
++ ASSERT_NO_THROW_LOG(mgr_->resume());
++ ASSERT_TRUE(mgr_->isRunning());
++
++ // Restart the main thread's IOService until we complete or timeout.
++ ASSERT_NO_THROW_LOG(runIOService());
++
++ ASSERT_NO_THROW_LOG(mgr_->stop());
++ ASSERT_TRUE(mgr_->isStopped());
++
++ // Calling nextToSend() should return false.
++ IOAddress next("0.0.0.0");
++ ASSERT_FALSE(mgr_->nextToSend(next));
++
++ // We should have as many declines as we have pairs created.
++ compareLeaseQueryPairs(declines_);
++ }
++
++ /// @brief Verifies that a recoverable error completion in sendCompleted() results
++ /// in the target address being free to use. In other words, it should have
++ /// the same outcome as the receiving a TARGET_UNREACHABLE reply from the OS.
++ void testSendCompletedSendFailed() {
++ SKIP_IF(notRoot());
++
++ // Create manager with thread-pool size of 3, min_echos 1,
++ // reply_timeout 250 milliseconds.
++ // ST mode should ingore requested thread number.
++ ASSERT_NO_THROW_LOG(createMgr(3, 1, 250));
++ ASSERT_TRUE(mgr_);
++
++ // Install a post send completed callback to stop the test if we're done.
++ mgr_->post_send_completed_cb_ =
++ [this](const ICMPMsgPtr& /* echo */, bool send_failed) {
++ MultiThreadingLock lock(*mutex_);
++ if (send_failed) {
++ stopTestService();
++ }
++ };
++
++ // Start the manager.
++ ASSERT_NO_THROW_LOG(mgr_->start());
++
++ // Set the test channel to complete the first send with a network_unreachable
++ // error. This saves us from trying to determine an address in the test
++ // environment that would cause it.
++ mgr_->getChannel()->ec_on_write_number_ = 1;
++ mgr_->getChannel()->write_error_ec_ = make_error_code(network_unreachable);
++
++ // Start a ping for one target.
++ startTargets(1);
++ auto lqp = lease_query_pairs_[0];
++
++ // Run the main thread's IOService until we complete or timeout.
++ ASSERT_NO_THROW_LOG(runIOService());
++
++ // Verify the expiration timer is no longer running.
++ EXPECT_EQ(mgr_->getExpirationTimerInterval(), 0);
++
++ // The context should no longer be in the store.
++ EXPECT_FALSE(getContext(lqp));
++
++ // We should have unparked the query from the lot.
++ EXPECT_EQ(mgr_->parkingLotSize(), 0);
++ EXPECT_EQ(unparked_, 1);
++
++ // We should have one free that matches our lease query pair.
++ compareLeaseQueryPairs(frees_);
++ }
++
++ /// @brief Exercises shouldPing().
++ void testShouldPingTest() {
++ SKIP_IF(notRoot());
++
++ // Create manager with thread-pool size of 3, min_echos 1,
++ // reply_timeout 250 milliseconds.
++ // ST mode should ingore requested thread number.
++ ASSERT_NO_THROW_LOG(createMgr(3, 1, 250));
++ ASSERT_TRUE(mgr_);
++
++ // Make a default config.
++ PingCheckConfigPtr config(new PingCheckConfig());
++
++ // Make a lease query pair.
++ auto lqp1 = makeLeaseQueryPair(IOAddress("127.0.0.2"), 111);
++ const uint8_t id1[] = { 0x31, 0x32, 0x33, 0x34 };
++ ClientIdPtr cid1(new ClientId(id1, sizeof(id1)));
++ lqp1.lease_->client_id_ = cid1;
++
++ Lease4Ptr empty_lease;
++ CalloutHandle::CalloutNextStep status;
++
++ // Ping checking enabled, no old lease, channel doesn't exist, should return CONTINUE.
++ ASSERT_TRUE(config->getEnablePingCheck());
++ ASSERT_NO_THROW_LOG(status = mgr_->shouldPing(lqp1.lease_, lqp1.query_, empty_lease, config));
++ EXPECT_EQ(status, CalloutHandle::NEXT_STEP_CONTINUE);
++
++ // Start the manager, then pause it. This lets us start pings without
++ // them changing state.
++ ASSERT_NO_THROW_LOG(mgr_->start());
++ ASSERT_NO_THROW_LOG(mgr_->pause());
++
++ // Ping checking disabled, no old lease, should return CONTINUE.
++ config->setEnablePingCheck(false);
++ ASSERT_NO_THROW_LOG(status = mgr_->shouldPing(lqp1.lease_, lqp1.query_, empty_lease, config));
++ EXPECT_EQ(status, CalloutHandle::NEXT_STEP_CONTINUE);
++
++ // Ping checking enabled, no old lease, should return PARK.
++ config->setEnablePingCheck(true);
++ ASSERT_NO_THROW_LOG(status = mgr_->shouldPing(lqp1.lease_, lqp1.query_, empty_lease, config));
++ EXPECT_EQ(status, CalloutHandle::NEXT_STEP_PARK);
++
++ // Make an old lease based on the first lease.
++ time_t now = time(0);
++ Lease4Ptr old_lease(new Lease4(*(lqp1.lease_)));
++
++ // Prior lease belonging to the same client with cltt greater than ping-cltt-secs
++ // should return PARK.
++ old_lease->cltt_ = now - config->getPingClttSecs() * 2;
++ ASSERT_NO_THROW_LOG(status = mgr_->shouldPing(lqp1.lease_, lqp1.query_, old_lease, config));
++ EXPECT_EQ(status, CalloutHandle::NEXT_STEP_PARK);
++
++ // Prior lease belonging to the same client but with cltt less than ping-cltt-secs
++ // should return CONTINUE.
++ old_lease->cltt_ = now - config->getPingClttSecs() / 2;
++ ASSERT_NO_THROW_LOG(status = mgr_->shouldPing(lqp1.lease_, lqp1.query_, old_lease, config));
++ EXPECT_EQ(status, CalloutHandle::NEXT_STEP_CONTINUE);
++
++ // Prior lease belonging to a different client, should return PARK.
++ const uint8_t id2[] = { 0x35, 0x36, 0x37, 0x34 };
++ old_lease->client_id_.reset(new ClientId(id2, sizeof(id2)));
++ ASSERT_NO_THROW_LOG(status = mgr_->shouldPing(lqp1.lease_, lqp1.query_, old_lease, config));
++ EXPECT_EQ(status, CalloutHandle::NEXT_STEP_PARK);
++
++ // Now let's start a ping for the lease-query pair.
++ ASSERT_NO_THROW_LOG(mgr_->startPing(lqp1.lease_, lqp1.query_, parking_lot_));
++
++ // Make a second lease query pair. Same address, different client.
++ auto lqp2 = makeLeaseQueryPair(IOAddress("127.0.0.2"), 333);
++ lqp2.lease_->client_id_ = old_lease->client_id_;
++
++ // Trying to start a ping for an address already being checked should return DROP.
++ ASSERT_NO_THROW_LOG(status = mgr_->shouldPing(lqp2.lease_, lqp2.query_, empty_lease, config));
++ EXPECT_EQ(status, CalloutHandle::NEXT_STEP_DROP);
++
++ // Stop the mgr.
++ ASSERT_NO_THROW(mgr_->stop());
++
++ // Ping checking enabled, no old lease, channel isn't open, should return CONTINUE.
++ ASSERT_TRUE(config->getEnablePingCheck());
++ ASSERT_NO_THROW_LOG(status = mgr_->shouldPing(lqp1.lease_, lqp1.query_, empty_lease, config));
++ EXPECT_EQ(status, CalloutHandle::NEXT_STEP_CONTINUE);
++ }
++
++ /// @brief Exercise's getScopedConfig().
++ void testGetScopedConfig() {
++ CfgMgr::instance().setFamily(AF_INET);
++
++ // Start with empty cache, any subnet that hasn't been seen should get parsed
++ // and, if valid, added to the cache.
++ CfgMgr& cfg_mgr = CfgMgr::instance();
++ CfgSubnets4Ptr subnets = cfg_mgr.getStagingCfg()->getCfgSubnets4();
++
++ // Subnet 1 has no ping-check config. Should return global config.
++ ElementPtr user_context = Element::createMap();
++ Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.1.0"), 24, 30, 40, 60, 1));
++ subnet->setContext(user_context);
++ subnets->add(subnet);
++
++ // Subnet 2 has invalid ping-check content. Should return global config.
++ std::string invalid_json_cfg =
++ R"({
++ "ping-check": {
++ "enable-ping-check" : true,
++ "bogus-key-word" : true
++ }
++ })";
++
++ user_context = Element::fromJSON(invalid_json_cfg);
++ subnet.reset(new Subnet4(IOAddress("192.0.2.0"), 24, 30, 40, 60, 2));
++ subnet->setContext(user_context);
++ subnets->add(subnet);
++
++ // Subnet 3 has valid ping check. Should return subnet config
++ std::string valid_json_cfg =
++ R"({
++ "ping-check": {
++ "enable-ping-check" : true,
++ "min-ping-requests" : 13
++ }
++ })";
++
++ user_context = Element::fromJSON(valid_json_cfg);
++ subnet.reset(new Subnet4(IOAddress("192.0.3.0"), 24, 30, 40, 60, 3));
++ subnet->setContext(user_context);
++ subnets->add(subnet);
++
++ // Commit the subnet configuration.
++ cfg_mgr.commit();
++
++ // Create manager with thread-pool size of 3, min_echos 2, reply_timeout 250 ms.
++ ASSERT_NO_THROW_LOG(createMgr(3, 2, 250));
++ ASSERT_TRUE(mgr_);
++
++ Lease4Ptr lease(new Lease4());
++ PingCheckConfigPtr config;
++
++ // Should get the global configuration for subnet 1.
++ lease->addr_ = IOAddress("192.0.1.1");
++ lease->subnet_id_ = 1;
++ ASSERT_NO_THROW_LOG(config = mgr_->getScopedConfig(lease));
++ ASSERT_TRUE(config);
++ ASSERT_EQ(config, mgr_->getGlobalConfig());
++
++ // Should get the global configuration for subnet 2.
++ lease->addr_ = IOAddress("192.0.2.1");
++ lease->subnet_id_ = 2;
++ ASSERT_NO_THROW_LOG(config = mgr_->getScopedConfig(lease));
++ ASSERT_TRUE(config);
++ ASSERT_EQ(config, mgr_->getGlobalConfig());
++
++ // Should get subnet configuration for subnet 3.
++ lease->addr_ = IOAddress("192.0.3.1");
++ lease->subnet_id_ = 3;
++ ASSERT_NO_THROW_LOG(config = mgr_->getScopedConfig(lease));
++ ASSERT_TRUE(config);
++ ASSERT_NE(config, mgr_->getGlobalConfig());
++ EXPECT_EQ(config->getMinPingRequests(), 13);
++ }
++
++ /// @brief Exercises checkSuspended().
++ ///
++ /// This is intended to verify that ping checking is suspended and resumed based
++ /// on the DHCP service state, not to verify every place that checkSuspended()
++ /// is called.
++ void testCheckSuspended() {
++ SKIP_IF(notRoot());
++
++ // Create manager with thread-pool size of 3, min_echos 1,
++ // reply_timeout 250 milliseconds.
++ ASSERT_NO_THROW_LOG(createMgr(3, 1, 250));
++ ASSERT_TRUE(mgr_);
++
++ // Make a default config.
++ PingCheckConfigPtr config(new PingCheckConfig());
++
++ // Give the manager a NetworkState instance.
++ NetworkStatePtr network_state(new NetworkState());
++ mgr_->setNetworkState(network_state);
++
++ // Verify that ping checking is not suspended.
++ ASSERT_FALSE(mgr_->checkSuspended());
++
++ // Start the manager, then pause it. This lets us start pings without
++ // them changing state.
++ ASSERT_NO_THROW_LOG(mgr_->start());
++ ASSERT_NO_THROW_LOG(mgr_->pause());
++
++ // Verfify the ping store is empty.
++ auto store = mgr_->getStore();
++ ASSERT_TRUE(store);
++ auto pings = store->getAll();
++ ASSERT_EQ(0, pings->size());
++
++ // Make a lease query pair.
++ auto lqp1 = makeLeaseQueryPair(IOAddress("127.0.0.2"), 111);
++ const uint8_t id1[] = { 0x31, 0x32, 0x33, 0x34 };
++ ClientIdPtr cid1(new ClientId(id1, sizeof(id1)));
++ lqp1.lease_->client_id_ = cid1;
++
++ // Now let's try to start a ping for the lease-query pair. It should work.
++ ASSERT_NO_THROW_LOG(mgr_->startPing(lqp1.lease_, lqp1.query_, parking_lot_));
++
++ // Verify we have an entry in the store.
++ pings = store->getAll();
++ ASSERT_EQ(1, pings->size());
++
++ // Disable the DHCP service.
++ network_state->disableService(NetworkState::USER_COMMAND);
++
++ // Make a second lease query pair. Different address, different client.
++ auto lqp2 = makeLeaseQueryPair(IOAddress("127.0.0.3"), 333);
++ const uint8_t id2[] = { 0x31, 0x32, 0x33, 0x35 };
++ ClientIdPtr cid2(new ClientId(id1, sizeof(id2)));
++ lqp2.lease_->client_id_ = cid2;
++
++ // Try to start a ping. We should not be able to do it.
++ ASSERT_THROW_MSG(mgr_->startPing(lqp2.lease_, lqp2.query_, parking_lot_),
++ InvalidOperation,
++ "PingCheckMgr::startPing() - DHCP service is suspended!");
++
++ // Store should be empty, having been flushed by suspension detection.
++ pings = store->getAll();
++ ASSERT_EQ(0, pings->size());
++
++ // Ping checking should report as suspended.
++ ASSERT_TRUE(mgr_->checkSuspended());
++
++ // Re-enable the DHCP service.
++ network_state->enableService(NetworkState::USER_COMMAND);
++
++ // Suspension checking should lift the suspension and we should once again
++ // be able to start a new ping check.
++ ASSERT_NO_THROW_LOG(mgr_->startPing(lqp2.lease_, lqp2.query_, parking_lot_));
++
++ // Store should have one check in it.
++ pings = store->getAll();
++ ASSERT_EQ(1, pings->size());
++
++ // Ping checking should report as not suspended.
++ ASSERT_FALSE(mgr_->checkSuspended());
++ }
++
++ /// @brief Manager instance.
++ TestablePingCheckMgrPtr mgr_;
++
++ /// @brief List of lease/query pairs used during the test, in the order
++ /// they were created.
++ LeaseQueryPairs lease_query_pairs_;
++
++ /// @brief The mutex used to protect internal state.
++ const boost::scoped_ptr<std::mutex> mutex_;
++
++ /// @brief Marks the start time of a test.
++ TimeStamp test_start_time_;
++
++ /// @brief Parking lot where the associated query is parked.
++ /// If empty parking is not being employed.
++ ParkingLotHandlePtr parking_lot_;
++
++ /// @brief Number of queries unparked during a test.
++ size_t unparked_;
++
++ /// @brief List of leases that were found to be in-use during a test.
++ LeaseQueryPairs declines_;
++
++ /// @brief List of leases that were found to be free to use during a test.
++ LeaseQueryPairs frees_;
++};
++
++TEST_F(PingCheckMgrTest, operationalBasicsST) {
++ testOperationalBasics(0);
++}
++
++TEST_F(PingCheckMgrTest, operationalBasicsMT) {
++ MultiThreadingTest mt;
++ testOperationalBasics(3);
++}
++
++TEST_F(PingCheckMgrTest, startPingST) {
++ testStartPing();
++}
++
++TEST_F(PingCheckMgrTest, startPingMT) {
++ MultiThreadingTest mt;
++ testStartPing();
++}
++
++TEST_F(PingCheckMgrTest, nextToSendST) {
++ testNextToSend();
++}
++
++TEST_F(PingCheckMgrTest, nextToSendMT) {
++ MultiThreadingTest mt;
++ testNextToSend();
++}
++
++TEST_F(PingCheckMgrTest, setNextExpirationST) {
++ testSetNextExpiration();
++}
++
++TEST_F(PingCheckMgrTest, setNextExpirationMT) {
++ MultiThreadingTest mt;
++ testSetNextExpiration();
++}
++
++TEST_F(PingCheckMgrTest, sendCompletedST) {
++ testSendCompleted();
++}
++
++TEST_F(PingCheckMgrTest, sendCompletedMT) {
++ MultiThreadingTest mt;
++ testSendCompleted();
++}
++
++TEST_F(PingCheckMgrTest, replyReceivedForEchoReplyST) {
++ testReplyReceivedForEchoReply();
++}
++
++TEST_F(PingCheckMgrTest, replyReceivedForEchoReplyMT) {
++ MultiThreadingTest mt;
++ testReplyReceivedForEchoReply();
++}
++
++TEST_F(PingCheckMgrTest, replyReceivedForTargetUnreachableST) {
++ testReplyReceivedForTargetUnreachable();
++}
++
++TEST_F(PingCheckMgrTest, replyReceivedForTargetUnreachableMT) {
++ MultiThreadingTest mt;
++ testReplyReceivedForTargetUnreachable();
++}
++
++TEST_F(PingCheckMgrTest, expirationProcessingST) {
++ testExpirationProcessing();
++}
++
++TEST_F(PingCheckMgrTest, expirationProcessingMT) {
++ MultiThreadingTest mt;
++ testExpirationProcessing();
++}
++
++TEST_F(PingCheckMgrTest, multiplePingsWithReplyST) {
++ testMultiplePingsWithReply();
++}
++
++TEST_F(PingCheckMgrTest, multiplePingsWithReplyMT) {
++ MultiThreadingTest mt;
++ testMultiplePingsWithReply();
++}
++
++TEST_F(PingCheckMgrTest, multiplePingsWithReplyAndPauseST) {
++ testMultiplePingsWithReplyAndPause();
++}
++
++TEST_F(PingCheckMgrTest, multiplePingsWithReplyAndPauseMT) {
++ MultiThreadingTest mt;
++ testMultiplePingsWithReplyAndPause();
++}
++
++TEST_F(PingCheckMgrTest, sendCompletedSendFailedST) {
++ testSendCompletedSendFailed();
++}
++
++TEST_F(PingCheckMgrTest, sendCompletedSendFailedMT) {
++ MultiThreadingTest mt;
++ testSendCompletedSendFailed();
++}
++
++TEST_F(PingCheckMgrTest, shouldPingST) {
++ testShouldPingTest();
++}
++
++TEST_F(PingCheckMgrTest, shouldPingMT) {
++ MultiThreadingTest mt;
++ testShouldPingTest();
++}
++
++TEST_F(PingCheckMgrTest, getScopedConfigST) {
++ testGetScopedConfig();
++}
++
++TEST_F(PingCheckMgrTest, getScopedConfigMT) {
++ MultiThreadingTest mt;
++ testGetScopedConfig();
++}
++
++TEST_F(PingCheckMgrTest, checkSuspendedST) {
++ testCheckSuspended();
++}
++
++TEST_F(PingCheckMgrTest, checkSuspendedMT) {
++ MultiThreadingTest mt;
++ testCheckSuspended();
++}
++
++} // end of anonymous namespace
+diff --git a/src/hooks/dhcp/ping_check/tests/ping_context_store_unittests.cc b/src/hooks/dhcp/ping_check/tests/ping_context_store_unittests.cc
+new file mode 100644
+index 0000000000..3a8854eb0e
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/tests/ping_context_store_unittests.cc
+@@ -0,0 +1,467 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++/// @file This file contains tests which exercise the PingContextStore class.
++
++#include <config.h>
++#include <ping_context_store.h>
++#include <asiolink/io_address.h>
++#include <testutils/gtest_utils.h>
++#include <testutils/multi_threading_utils.h>
++
++#include <gtest/gtest.h>
++#include <sstream>
++
++using namespace std;
++using namespace isc;
++using namespace isc::asiolink;
++using namespace isc::dhcp;
++using namespace isc::ping_check;
++using namespace isc::test;
++using namespace std::chrono;
++
++namespace {
++
++/// @brief Text fixture class for @c PingContextStore
++///
++/// In order to facilitate single and multi threaded testing,
++/// individual tests are implemented as methods that are called
++/// from within TEST_F bodies rather than in TEST_F bodies.
++class PingContextStoreTest : public ::testing::Test {
++public:
++
++ /// @brief Constructor
++ PingContextStoreTest() {
++ }
++
++ /// @brief Destructor
++ virtual ~PingContextStoreTest() = default;
++
++ /// @brief Verifies that contexts can be added to the store given valid leases and queries.
++ /// Also verifies that they can be fetched by address.
++ void addContextTest() {
++ PingContextStore store;
++ PingContextPtr context;
++
++ // Add three contexts, one for each lease/query.
++ auto now = PingContext::now();
++ for (int i = 0; i < leases_.size(); ++i) {
++ ASSERT_NO_THROW_LOG(context = store.addContext(leases_[i], queries_[i], 2, 300));
++ ASSERT_TRUE(context);
++ EXPECT_EQ(leases_[i], context->getLease());
++ EXPECT_EQ(queries_[i], context->getQuery());
++
++ // Check initial values.
++ EXPECT_EQ(PingContext::WAITING_TO_SEND, context->getState());
++ EXPECT_LE(now, context->getSendWaitStart());
++ EXPECT_EQ(2, context->getMinEchos());
++ EXPECT_EQ(300, context->getReplyTimeout());
++ }
++
++ // Make sure they can be fetched by address and by query individually.
++ for (int i = 0; i < leases_.size(); ++i) {
++ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[i]->addr_));
++ ASSERT_TRUE(context);
++ EXPECT_EQ(leases_[i], context->getLease());
++
++ ASSERT_NO_THROW_LOG(context = store.getContextByQuery(queries_[i]));
++ ASSERT_TRUE(context);
++ EXPECT_EQ(queries_[i], context->getQuery());
++ }
++ }
++
++ /// @brief Verifies that the store only allows once entry per IP address.
++ void addContextDuplicateTest() {
++ PingContextStore store;
++ PingContextPtr context;
++
++ ASSERT_NO_THROW_LOG(context = store.addContext(leases_[0], queries_[0], 1, 100));
++ ASSERT_TRUE(context);
++ ASSERT_THROW_MSG(store.addContext(leases_[0], queries_[0], 1, 100), DuplicateContext,
++ "PingContextStore::addContex: context already exists for: 192.0.2.1");
++ }
++
++ /// @brief Verify that addContext fails given invalid input.
++ void addContextInvalidTest() {
++ PingContextStore store;
++
++ // Verify that given an empty lease the add will fail.
++ Lease4Ptr empty_lease;
++ ASSERT_THROW_MSG(store.addContext(empty_lease, queries_[0], 1, 100), BadValue,
++ "PingContextStore::addContext failed:"
++ " PingContext ctor - lease cannot be empty");
++
++ // Verify that given an empty query the add will fail.
++ Pkt4Ptr empty_query;
++ ASSERT_THROW_MSG(store.addContext(leases_[0], empty_query, 1, 100), BadValue,
++ "PingContextStore::addContext failed:"
++ " PingContext ctor - query cannot be empty");
++ }
++
++ /// @brief Verify that contexts can be deleted from the store.
++ void deleteContextTest() {
++ PingContextStore store;
++
++ // Add contexts to store.
++ for (int i = 0; i < leases_.size(); ++i) {
++ PingContextPtr context;
++ ASSERT_NO_THROW_LOG(context = store.addContext(leases_[i], queries_[i], 1, 100));
++ ASSERT_TRUE(context);
++ EXPECT_EQ(leases_[i], context->getLease());
++ EXPECT_EQ(queries_[i], context->getQuery());
++ }
++
++ // Fetch the second context.
++ PingContextPtr orig_context;
++ ASSERT_NO_THROW_LOG(orig_context = store.getContextByAddress(leases_[1]->addr_));
++ ASSERT_TRUE(orig_context);
++ EXPECT_EQ(leases_[1], orig_context->getLease());
++
++ // Delete it.
++ ASSERT_NO_THROW_LOG(store.deleteContext(orig_context));
++
++ // Try to fetch it, shouldn't find it.
++ PingContextPtr context;
++ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[1]->addr_));
++ ASSERT_FALSE(context);
++
++ // Deleting it again should do no harm.
++ ASSERT_NO_THROW_LOG(store.deleteContext(orig_context));
++ }
++
++ /// @brief Verify that contexts in the store can be updated.
++ void updateContextTest() {
++ PingContextStore store;
++ PingContextPtr context;
++
++ // Try to update a context that doesn't exist. It should throw.
++ ASSERT_NO_THROW_LOG(context.reset(new PingContext(leases_[0], queries_[0])));
++ ASSERT_THROW_MSG(store.updateContext(context), InvalidOperation,
++ "PingContextStore::updateContext failed for address:"
++ " 192.0.2.1, not in store");
++
++ auto test_start = PingContext::now();
++
++ // Add contexts to store.
++ for (int i = 0; i < leases_.size(); ++i) {
++ ASSERT_NO_THROW_LOG(context = store.addContext(leases_[i], queries_[i], 1, 100));
++ ASSERT_TRUE(context);
++ EXPECT_EQ(leases_[i], context->getLease());
++ EXPECT_EQ(queries_[i], context->getQuery());
++ }
++
++ // Fetch the second context.
++ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[1]->addr_));
++ ASSERT_TRUE(context);
++ ASSERT_EQ(leases_[1], context->getLease());
++ ASSERT_EQ(queries_[1], context->getQuery());
++
++ // Check initial values for state and expiration.
++ EXPECT_EQ(PingContext::WAITING_TO_SEND, context->getState());
++ EXPECT_LE(test_start, context->getSendWaitStart());
++ EXPECT_LE(PingContext::EMPTY_TIME(), context->getNextExpiry());
++
++ // Modify the state and expiration, then update the context.
++ auto wait_start = PingContext::now();
++ context->beginWaitingForReply(wait_start);
++ ASSERT_NO_THROW_LOG(store.updateContext(context));
++
++ // Fetch the context and verify the values are correct.
++ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[1]->addr_));
++ ASSERT_TRUE(context);
++ EXPECT_EQ(PingContext::WAITING_FOR_REPLY, context->getState());
++ EXPECT_LE(wait_start + milliseconds(context->getReplyTimeout()), context->getNextExpiry());
++ }
++
++ /// @brief Verify that contexts can be fetched based on when they entered WAITING_TO_SEND
++ /// by getNextToSend().
++ void getNextToSendTest() {
++ PingContextStore store;
++ PingContextPtr context;
++
++ // Capture time now.
++ auto start_time = PingContext::now();
++
++ // Add contexts to store.
++ for (int i = 0; i < leases_.size(); ++i) {
++ ASSERT_NO_THROW_LOG(context = store.addContext(leases_[i], queries_[i], 1, 100));
++ ASSERT_TRUE(context);
++ EXPECT_EQ(leases_[i], context->getLease());
++ EXPECT_EQ(queries_[i], context->getQuery());
++ usleep(1000);
++ }
++
++ // Fetching the next context to send should return the first context as
++ // it has the oldest send wait start time.
++ context.reset();
++ ASSERT_NO_THROW(context = store.getNextToSend());
++ ASSERT_TRUE(context);
++ EXPECT_EQ(leases_[0], context->getLease());
++ EXPECT_EQ(queries_[0], context->getQuery());
++ EXPECT_LE(start_time, context->getSendWaitStart());
++
++ // Update the first context's state to TARGET_FREE which should
++ // disqualify it from being returned as next to send.
++ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[0]->addr_));
++ ASSERT_TRUE(context);
++ ASSERT_EQ(PingContext::WAITING_TO_SEND, context->getState());
++ context->setState(PingContext::TARGET_FREE);
++ ASSERT_NO_THROW_LOG(store.updateContext(context));
++
++ // Update the send wait start of the second context making it the
++ // youngest send wait start time.
++ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[1]->addr_));
++ ASSERT_TRUE(context);
++ ASSERT_EQ(PingContext::WAITING_TO_SEND, context->getState());
++ context->setSendWaitStart(start_time + milliseconds(1000));
++ ASSERT_NO_THROW_LOG(store.updateContext(context));
++
++ // Update the send wait start of the third context, making it the oldest.
++ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[2]->addr_));
++ ASSERT_TRUE(context);
++ ASSERT_EQ(PingContext::WAITING_TO_SEND, context->getState());
++ context->setSendWaitStart(start_time + milliseconds(500));
++ ASSERT_NO_THROW_LOG(store.updateContext(context));
++
++ // Fetching the next context to send should return the third context.
++ context.reset();
++ ASSERT_NO_THROW(context = store.getNextToSend());
++ ASSERT_TRUE(context);
++ EXPECT_EQ(leases_[2], context->getLease());
++ EXPECT_EQ(queries_[2], context->getQuery());
++ EXPECT_EQ(start_time + milliseconds(500), context->getSendWaitStart());
++ }
++
++ /// @brief Verify that contexts can be fetched based on when they expire using
++ /// getExpiresNext() and getExpiredSince().
++ void getByExpirationTest() {
++ PingContextStore store;
++ PingContextPtr context;
++
++ // Add contexts to store.
++ for (int i = 0; i < leases_.size(); ++i) {
++ ASSERT_NO_THROW_LOG(context = store.addContext(leases_[i], queries_[i], 1, 100));
++ ASSERT_TRUE(context);
++ EXPECT_EQ(leases_[i], context->getLease());
++ EXPECT_EQ(queries_[i], context->getQuery());
++ }
++
++ // Capture time now.
++ auto start_time = PingContext::now();
++
++ // Update the state and expiration of the first context.
++ // State set to TARGET_FREE should disqualify if from
++ // fetch by expiration even though it has the soonest expiration
++ // time.
++ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[0]->addr_));
++ ASSERT_TRUE(context);
++ context->setState(PingContext::TARGET_FREE);
++ context->setNextExpiry(start_time + milliseconds(1));
++ ASSERT_NO_THROW_LOG(store.updateContext(context));
++
++ // Update the state and expiration of the second context giving it
++ // the youngest expiration time.
++ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[1]->addr_));
++ ASSERT_TRUE(context);
++ context->setState(PingContext::WAITING_FOR_REPLY);
++ context->setNextExpiry(start_time + milliseconds(1000));
++ ASSERT_NO_THROW_LOG(store.updateContext(context));
++
++ // Update the state and expiration of the third context, make it the
++ // soonest qualified expiration time.
++ ASSERT_NO_THROW_LOG(context = store.getContextByAddress(leases_[2]->addr_));
++ ASSERT_TRUE(context);
++ context->setState(PingContext::WAITING_FOR_REPLY);
++ context->setNextExpiry(start_time + milliseconds(500));
++ ASSERT_NO_THROW_LOG(store.updateContext(context));
++
++ // Fetching the context that expires next should return the third context.
++ context.reset();
++ ASSERT_NO_THROW(context = store.getExpiresNext());
++ ASSERT_TRUE(context);
++ EXPECT_EQ(leases_[2], context->getLease());
++ EXPECT_EQ(queries_[2], context->getQuery());
++ EXPECT_EQ(start_time + milliseconds(500), context->getNextExpiry());
++
++ // Fetch all that have expired since current time. Should be none.
++ PingContextCollectionPtr expired_since;
++ ASSERT_NO_THROW_LOG(expired_since = store.getExpiredSince());
++ ASSERT_TRUE(expired_since);
++ EXPECT_EQ(0, expired_since->size());
++
++ // Fetch all that have expired since start time + 750 ms, should be third context.
++ ASSERT_NO_THROW_LOG(expired_since = store.getExpiredSince(start_time + milliseconds(750)));
++ ASSERT_TRUE(expired_since);
++ EXPECT_EQ(1, expired_since->size());
++ context = (*expired_since)[0];
++ EXPECT_EQ(leases_[2], context->getLease());
++ EXPECT_EQ(queries_[2], context->getQuery());
++ EXPECT_EQ(start_time + milliseconds(500), context->getNextExpiry());
++
++ // Fetch all that have expired since start time + 1500 ms
++ // Should be the third and second contexts
++ ASSERT_NO_THROW_LOG(expired_since = store.getExpiredSince(start_time + milliseconds(1500)));
++ ASSERT_TRUE(expired_since);
++ EXPECT_EQ(2, expired_since->size());
++
++ // First in list should be the third context.
++ context = (*expired_since)[0];
++ EXPECT_EQ(leases_[2], context->getLease());
++ EXPECT_EQ(queries_[2], context->getQuery());
++ EXPECT_EQ(start_time + milliseconds(500), context->getNextExpiry());
++
++ // The last one in the list should be the second context.
++ context = (*expired_since)[1];
++ EXPECT_EQ(leases_[1], context->getLease());
++ EXPECT_EQ(queries_[1], context->getQuery());
++ EXPECT_EQ(start_time + milliseconds(1000), context->getNextExpiry());
++ }
++
++ /// @brief Verifies that getAll() and clear() work properly.
++ void getAllAndClearTest() {
++ PingContextStore store;
++
++ // Add contexts to store.
++ for (int i = 0; i < leases_.size(); ++i) {
++ PingContextPtr context;
++ ASSERT_NO_THROW_LOG(context = store.addContext(leases_[i], queries_[i], 1, 100));
++ ASSERT_TRUE(context);
++ EXPECT_EQ(leases_[i], context->getLease());
++ EXPECT_EQ(queries_[i], context->getQuery());
++ }
++
++ // Fetch them all.
++ PingContextCollectionPtr contexts;
++ ASSERT_NO_THROW_LOG(contexts = store.getAll());
++ ASSERT_EQ(leases_.size(), contexts->size());
++
++ // Verify we got them all in order.
++ int i = 0;
++ for (auto const& context : *contexts) {
++ EXPECT_EQ(leases_[i], context->getLease());
++ EXPECT_EQ(queries_[i], context->getQuery());
++ ++i;
++ }
++
++ // Now clear the store. Verify it's empty.
++ ASSERT_NO_THROW_LOG(store.clear());
++ ASSERT_NO_THROW_LOG(contexts = store.getAll());
++ ASSERT_EQ(0, contexts->size());
++
++ // Verify clearing an empty store does no harm.
++ ASSERT_NO_THROW_LOG(store.clear());
++ }
++
++private:
++ /// @brief Prepares the class for a test.
++ virtual void SetUp() {
++ Lease4Ptr lease;
++ lease.reset(new Lease4());
++ lease->addr_ = IOAddress("192.0.2.1");
++ leases_.push_back(lease);
++
++ lease.reset(new Lease4());
++ lease->addr_ = IOAddress("192.0.2.2");
++ leases_.push_back(lease);
++
++ lease.reset(new Lease4());
++ lease->addr_ = IOAddress("192.0.2.3");
++ leases_.push_back(lease);
++
++ Pkt4Ptr query;
++ query.reset(new Pkt4(DHCPDISCOVER, 101));
++ queries_.push_back(query);
++
++ query.reset(new Pkt4(DHCPDISCOVER, 102));
++ queries_.push_back(query);
++
++ query.reset(new Pkt4(DHCPDISCOVER, 103));
++ queries_.push_back(query);
++
++ ASSERT_EQ(leases_.size(), queries_.size());
++ }
++
++public:
++ /// @brief List of pre-made leases.
++ std::vector<Lease4Ptr> leases_;
++
++ /// @brief List of pre-made queries.
++ std::vector<Pkt4Ptr> queries_;
++};
++
++TEST_F(PingContextStoreTest, addContext) {
++ addContextTest();
++}
++
++TEST_F(PingContextStoreTest, addContextMultiThreading) {
++ MultiThreadingTest mt;
++ addContextTest();
++}
++
++TEST_F(PingContextStoreTest, addContextDuplicate) {
++ addContextDuplicateTest();
++}
++
++TEST_F(PingContextStoreTest, addContextDuplicateMultiThreading) {
++ MultiThreadingTest mt;
++ addContextDuplicateTest();
++}
++
++TEST_F(PingContextStoreTest, addContextInvalid) {
++ addContextInvalidTest();
++}
++
++TEST_F(PingContextStoreTest, addContextInvalidMultiThreading) {
++ MultiThreadingTest mt;
++ addContextInvalidTest();
++}
++
++TEST_F(PingContextStoreTest, deleteContext) {
++ deleteContextTest();
++}
++
++TEST_F(PingContextStoreTest, deleteContextMultiThreading) {
++ MultiThreadingTest mt;
++ deleteContextTest();
++}
++
++TEST_F(PingContextStoreTest, updateContext) {
++ updateContextTest();
++}
++
++TEST_F(PingContextStoreTest, updateContextMultiThreading) {
++ MultiThreadingTest mt;
++ updateContextTest();
++}
++
++TEST_F(PingContextStoreTest, getNextToSend) {
++ getNextToSendTest();
++}
++
++TEST_F(PingContextStoreTest, getNextToSendMultiThreading) {
++ MultiThreadingTest mt;
++ getNextToSendTest();
++}
++
++TEST_F(PingContextStoreTest, getByExpiration) {
++ getByExpirationTest();
++}
++
++TEST_F(PingContextStoreTest, getByExpirationMultiThreading) {
++ MultiThreadingTest mt;
++ getByExpirationTest();
++}
++
++TEST_F(PingContextStoreTest, getAllAndClear) {
++ getAllAndClearTest();
++}
++
++TEST_F(PingContextStoreTest, getAllAndClearMultiThreading) {
++ MultiThreadingTest mt;
++ getAllAndClearTest();
++}
++
++} // end of anonymous namespace
+diff --git a/src/hooks/dhcp/ping_check/tests/ping_context_unittests.cc b/src/hooks/dhcp/ping_check/tests/ping_context_unittests.cc
+new file mode 100644
+index 0000000000..4a38277ad6
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/tests/ping_context_unittests.cc
+@@ -0,0 +1,146 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++/// @file This file contains tests which exercise the PingContext class.
++
++#include <config.h>
++#include <ping_context.h>
++#include <asiolink/io_address.h>
++#include <testutils/gtest_utils.h>
++
++#include <gtest/gtest.h>
++#include <sstream>
++
++using namespace std;
++using namespace isc;
++using namespace isc::asiolink;
++using namespace isc::dhcp;
++using namespace isc::ping_check;
++using namespace std::chrono;
++
++namespace {
++
++TEST(PingContextTest, validConstruction) {
++ // Make a valid lease and query.
++ Lease4Ptr lease(new Lease4());
++ lease->addr_ = IOAddress("192.0.2.1");
++ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
++
++ // Capture time now.
++ auto start_time = PingContext::now();
++
++ // Construct the context.
++ PingContextPtr context;
++ ASSERT_NO_THROW_LOG(context.reset(new PingContext(lease, query)));
++
++ // Verify initial content.
++ EXPECT_EQ(lease->addr_, context->getTarget());
++ EXPECT_EQ(1, context->getMinEchos());
++ EXPECT_EQ(100, context->getReplyTimeout());
++ EXPECT_EQ(0, context->getEchosSent());
++ EXPECT_EQ(PingContext::EMPTY_TIME(), context->getLastEchoSentTime());
++ EXPECT_EQ(PingContext::EMPTY_TIME(), context->getSendWaitStart());
++ EXPECT_EQ(PingContext::EMPTY_TIME(), context->getNextExpiry());
++ EXPECT_EQ(PingContext::NEW, context->getState());
++
++ // Start time should be less than or equal to created time.
++ EXPECT_LE(start_time, context->getCreatedTime());
++ EXPECT_EQ(lease, context->getLease());
++ EXPECT_EQ(query, context->getQuery());
++}
++
++TEST(PingContextTest, invalidConstruction) {
++ // Make a valid lease and query.
++ Lease4Ptr lease(new Lease4());
++ lease->addr_ = IOAddress("192.0.2.1");
++ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
++
++ // Empty lease should throw.
++ Lease4Ptr empty_lease;
++ PingContextPtr context;
++ ASSERT_THROW_MSG(context.reset(new PingContext(empty_lease, query)), BadValue,
++ "PingContext ctor - lease cannot be empty");
++
++ // Empty query should throw.
++ Pkt4Ptr empty_query;
++ ASSERT_THROW_MSG(context.reset(new PingContext(lease, empty_query)), BadValue,
++ "PingContext ctor - query cannot be empty");
++
++ // Empty lease address should throw.
++ lease->addr_ = IOAddress::IPV4_ZERO_ADDRESS();
++ ASSERT_THROW_MSG(context.reset(new PingContext(lease, query)), BadValue,
++ "PingContext ctor - target address cannot be 0.0.0.0");
++}
++
++// Tests conversion of PingContext::State to string and vice-versa.
++TEST(PingContext, stateConversion) {
++ EXPECT_EQ(PingContext::NEW, PingContext::stringToState("NEW"));
++ EXPECT_EQ(PingContext::WAITING_TO_SEND, PingContext::stringToState("WAITING_TO_SEND"));
++ EXPECT_EQ(PingContext::SENDING, PingContext::stringToState("SENDING"));
++ EXPECT_EQ(PingContext::WAITING_FOR_REPLY, PingContext::stringToState("WAITING_FOR_REPLY"));
++ EXPECT_EQ(PingContext::TARGET_FREE, PingContext::stringToState("TARGET_FREE"));
++ EXPECT_EQ(PingContext::TARGET_IN_USE, PingContext::stringToState("TARGET_IN_USE"));
++ ASSERT_THROW_MSG(PingContext::stringToState("bogus"), BadValue,
++ "Invalid PingContext::State: 'bogus'");
++
++ EXPECT_EQ("NEW", PingContext::stateToString(PingContext::NEW));
++ EXPECT_EQ("WAITING_TO_SEND", PingContext::stateToString(PingContext::WAITING_TO_SEND));
++ EXPECT_EQ("SENDING", PingContext::stateToString(PingContext::SENDING));
++ EXPECT_EQ("WAITING_FOR_REPLY", PingContext::stateToString(PingContext::WAITING_FOR_REPLY));
++ EXPECT_EQ("TARGET_FREE", PingContext::stateToString(PingContext::TARGET_FREE));
++ EXPECT_EQ("TARGET_IN_USE", PingContext::stateToString(PingContext::TARGET_IN_USE));
++}
++
++TEST(PingContext, accessors) {
++ // Make a valid lease and query.
++ Lease4Ptr lease(new Lease4());
++ lease->addr_ = IOAddress("192.0.2.1");
++ Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
++
++ // Capture time now.
++ auto time_now = PingContext::now();
++
++ // Construct a context.
++ PingContextPtr context;
++ ASSERT_NO_THROW_LOG(context.reset(new PingContext(lease, query, 1, 50)));
++
++ EXPECT_NO_THROW_LOG(context->setMinEchos(4));
++ EXPECT_EQ(4, context->getMinEchos());
++
++ EXPECT_NO_THROW_LOG(context->setReplyTimeout(200));
++ EXPECT_EQ(200, context->getReplyTimeout());
++
++ EXPECT_NO_THROW_LOG(context->setEchosSent(7));
++ EXPECT_EQ(7, context->getEchosSent());
++
++ EXPECT_NO_THROW_LOG(context->setLastEchoSentTime(time_now));
++ EXPECT_EQ(time_now, context->getLastEchoSentTime());
++
++ EXPECT_NO_THROW_LOG(context->setState(PingContext::SENDING));
++ EXPECT_EQ(PingContext::SENDING, context->getState());
++
++ time_now += milliseconds(100);
++ EXPECT_NO_THROW_LOG(context->setSendWaitStart(time_now));
++ EXPECT_EQ(time_now, context->getSendWaitStart());
++
++ time_now += milliseconds(100);
++ EXPECT_NO_THROW_LOG(context->setNextExpiry(time_now));
++ EXPECT_EQ(time_now, context->getNextExpiry());
++
++ EXPECT_FALSE(context->isWaitingToSend());
++ time_now += milliseconds(100);
++ ASSERT_NO_THROW_LOG(context->beginWaitingToSend(time_now));
++ EXPECT_EQ(time_now, context->getSendWaitStart());
++ EXPECT_TRUE(context->isWaitingToSend());
++
++ EXPECT_FALSE(context->isWaitingForReply());
++ auto exp_expiry = time_now + milliseconds(context->getReplyTimeout());
++ ASSERT_NO_THROW_LOG(context->beginWaitingForReply(time_now));
++ EXPECT_EQ(exp_expiry, context->getNextExpiry());
++ EXPECT_TRUE(context->isWaitingForReply());
++}
++
++} // end of anonymous namespace
+diff --git a/src/hooks/dhcp/ping_check/tests/ping_test_utils.h b/src/hooks/dhcp/ping_check/tests/ping_test_utils.h
+new file mode 100644
+index 0000000000..df1ede7526
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/tests/ping_test_utils.h
+@@ -0,0 +1,396 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++#ifndef PING_TEST_UTILS_H
++#define PING_TEST_UTILS_H
++
++#include <ping_channel.h>
++#include <asiolink/interval_timer.h>
++#include <asiolink/io_service.h>
++#include <asiolink/io_address.h>
++#include <testutils/gtest_utils.h>
++#include <asiolink/io_service_thread_pool.h>
++#include <util/multi_threading_mgr.h>
++#include <mutex>
++
++#include <gtest/gtest.h>
++#include <queue>
++#include <list>
++#include <thread>
++#include <map>
++
++namespace isc {
++namespace ping_check {
++
++/// @brief Test timeout (ms).
++const long TEST_TIMEOUT = 10000;
++
++/// @brief Maps IOAddresses to sequence numbers.
++///
++/// Outbound requests are assigned a unique id and sequence
++/// number. This map is used to track the request's destination
++/// address by its sequence number. The channel can then substitute
++/// the loopback address, 127.0.0.1, as the destination address.
++/// Upon response receipt, the original destination can be found by
++/// the sequence number sent back in the response.
++class LoopbackMap {
++public:
++ /// @brief Constructor.
++ LoopbackMap() : map_(), mutex_(new std::mutex) {
++ }
++
++ /// @brief Destructor.
++ ~LoopbackMap() = default;
++
++ /// @brief Find and IOAddress associated with a sequence number.
++ ///
++ /// @param sequence sequence number to search by
++ ///
++ /// @return address found or IPV4_ZERO_ADDRESS.
++ asiolink::IOAddress find(uint16_t sequence) {
++ util::MultiThreadingLock lock(*mutex_);
++ auto const& iter = map_.find(sequence);
++ if (iter == map_.end()) {
++ return (asiolink::IOAddress::IPV4_ZERO_ADDRESS());
++ }
++
++ return (iter->second);
++ }
++
++ /// @brief Adds an entry for a sequence number and address
++ ///
++ /// @param sequence sequence number associated with the address
++ /// @param address address to add to the map
++ ///
++ /// @return true if the entry was added, false otherwise.
++ bool add(uint16_t sequence, const asiolink::IOAddress& address) {
++ util::MultiThreadingLock lock(*mutex_);
++ if (map_.count(sequence)) {
++ return (false);
++ }
++
++ map_.emplace(sequence, address);
++ return (true);
++ };
++
++ /// @brief Map of addresses by sequence number.
++ std::map<uint16_t, asiolink::IOAddress> map_;
++
++ /// @brief Mutex to protect the map during operations.
++ const boost::scoped_ptr<std::mutex> mutex_;
++};
++
++/// @brief Testable derivation of PingChannel
++///
++/// Overrides read and write functions to inject IO errors.
++class TestablePingChannel : public PingChannel {
++public:
++ /// @brief Constructor
++ ///
++ /// Instantiates the channel with its socket closed.
++ ///
++ /// @param io_service pointer to the IOService instance that will manage
++ /// the channel's IO. Must not be empty
++ /// @param next_to_send_cb callback to invoke to fetch the next IOAddress
++ /// to ping
++ /// @param echo_sent_cb callback to invoke when an ECHO send has completed
++ /// @param reply_received_cb callback to invoke when an ICMP reply has been
++ /// received. This callback is passed all inbound ICMP messages (e.g. ECHO
++ /// REPLY, UNREACHABLE, etc...)
++ /// @param shutdown_cb callback to invoke when the channel has shutdown due
++ /// to an error
++ ///
++ /// @throw BadValue if io_service is empty.
++ TestablePingChannel(asiolink::IOServicePtr& io_service,
++ NextToSendCallback next_to_send_cb,
++ EchoSentCallback echo_sent_cb,
++ ReplyReceivedCallback reply_received_cb,
++ ShutdownCallback shutdown_cb = ShutdownCallback())
++ : PingChannel(io_service, next_to_send_cb, echo_sent_cb, reply_received_cb, shutdown_cb),
++ read_number_(0), throw_on_read_number_(0), ec_on_read_number_(0), read_error_ec_(),
++ write_number_(0), throw_on_write_number_(0), ec_on_write_number_(0), write_error_ec_(),
++ route_loopback_(true), loopback_map_(), stopped_(false) {
++ }
++
++ /// @brief Virtual destructor
++ virtual ~TestablePingChannel() {
++ stopped_ = true;
++ }
++
++ // @brief Schedules the next send.
++ //
++ // If the socket is not currently sending it posts a call to @c sendNext()
++ // to the channel's IOService.
++ virtual void startSend() {
++ if (stopped_) {
++ return;
++ }
++ PingChannel::startSend();
++ }
++
++ /// @brief Perform asynchronous read or feign a read error
++ ///
++ /// This virtual function is provided as means to inject errors during
++ /// read operations to facilitate testing. It tracks the number of
++ /// reads that have occurred since channel open and instigates an
++ /// error trigger on the trigger read number if a trigger has been set.
++ ///
++ /// @param data buffer to receive incoming message
++ /// @param length length of the data buffer
++ /// @param offset offset into buffer where data is to be put
++ /// @param endpoint source of the communication
++ /// @param callback callback object
++ virtual void asyncReceive(void* data, size_t length, size_t offset,
++ asiolink::IOEndpoint* endpoint, SocketCallback& callback) {
++ if (stopped_) {
++ return;
++ }
++ ++read_number_;
++
++ // If we're set to fail with an exception, do so.
++ if (throw_on_read_number_ && (read_number_ == throw_on_read_number_)) {
++ isc_throw(Unexpected, "Injected read error");
++ }
++
++ // If we're set to fail via the callback, post a call with the
++ // desired error code.
++ if (ec_on_read_number_ && read_number_ == ec_on_read_number_) {
++ getIOService()->post([this]() { socketReadCallback(read_error_ec_, 0); });
++ return;
++ }
++
++ // No scheduled error, proceed with normal read.
++ PingChannel::asyncReceive(data, length, offset, endpoint, callback);
++ }
++
++ /// @brief Perform asynchronous write or feign a write error
++ ///
++ /// This virtual function is provided as means to inject errors during
++ /// write operations to facilitate testing. It tracks the number of
++ /// writes that have occurred since channel open and instigates an
++ /// error trigger on the trigger write number if a trigger has been set.
++ ///
++ /// @param data buffer of data to write
++ /// @param length length of the data buffer
++ /// @param endpoint destination of the communication
++ /// @param callback callback object
++ virtual void asyncSend(void* data, size_t length, asiolink::IOEndpoint* endpoint,
++ SocketCallback& callback) {
++ if (stopped_) {
++ return;
++ }
++ ++write_number_;
++ if (throw_on_write_number_ && (write_number_ == throw_on_write_number_)) {
++ isc_throw(Unexpected, "Injected write error");
++ }
++
++ if (ec_on_write_number_ && write_number_ == ec_on_write_number_) {
++ ICMPMsgPtr fake_echo(new ICMPMsg());
++ fake_echo->setType(ICMPMsg::ECHO_REQUEST);
++ fake_echo->setDestination(endpoint->getAddress());
++ getIOService()->post([this, fake_echo]() { socketWriteCallback(fake_echo, write_error_ec_, 0); });
++ return;
++ }
++
++ // In order to make testing more predictable, we need slow writes down a bit.
++ usleep(5000);
++
++ // If loopback routing is enabled, store the destination address by
++ // sequence number in the loopback map, then replace the destination
++ // endpoint with 127.0.0.1 and send it there.
++ if (route_loopback_) {
++ struct icmp* reply = (struct icmp*)(data);
++ auto sequence = (ntohs(reply->icmp_hun.ih_idseq.icd_seq));
++ loopback_map_.add(sequence, endpoint->getAddress());
++ ICMPEndpoint lo_endpoint(asiolink::IOAddress("127.0.0.1"));
++ PingChannel::asyncSend(data, length, &lo_endpoint, callback);
++ return;
++ }
++
++ PingChannel::asyncSend(data, length, endpoint, callback);
++ }
++
++ /// @brief Fetches the PingSocket.
++ ///
++ /// @return pointer to the PingSocket instance.
++ PingSocketPtr getPingSocket() {
++ return (socket_);
++ }
++
++ /// @brief Checks if channel was opened in single-threaded mode.
++ ///
++ /// @return True if channel is single-threaded.
++ bool getSingleThreaded() const {
++ return (single_threaded_);
++ }
++
++ /// @brief Fetch the WatchSocket instance.
++ ///
++ /// @return pointer to the WatchSocket.
++ util::WatchSocketPtr getWatchSocket() const {
++ return (watch_socket_);
++ }
++
++ /// @brief The "write-ready" socket descriptor registered IfaceMgr.
++ ///
++ /// @return registered socket descriptor.
++ int getRegisteredWriteFd() const {
++ return (registered_write_fd_);
++ }
++
++ /// @brief The "read-ready" socket descriptor registered IfaceMgr.
++ ///
++ /// @return registered socket descriptor.
++ int getRegisteredReadFd() const {
++ return (registered_read_fd_);
++ }
++
++ /// @brief Tracks the number of reads since the channel was created
++ size_t read_number_;
++
++ /// @brief Read number on which to thrown an exception from asyncReceive()
++ size_t throw_on_read_number_;
++
++ /// @brief Read number on which to inject a socketReadCallback with an error code
++ size_t ec_on_read_number_;
++
++ /// @brief Error code to inject on read error trigger
++ boost::system::error_code read_error_ec_;
++
++ /// @brief Tracks the number of writes since the channel was created
++ size_t write_number_;
++
++ /// @brief Write number on which to thrown an exception from asyncSend()
++ size_t throw_on_write_number_;
++
++ /// @brief Error code to inject on write error trigger
++ size_t ec_on_write_number_;
++
++ /// @brief Error code to inject on write error trigger
++ boost::system::error_code write_error_ec_;
++
++ /// @brief Enables routing of 127.0.0.x by to 127.0.0.1 via sequence number.
++ bool route_loopback_;
++
++ /// @brief Maps loopback addresses to sequence numbers when loopback routing
++ /// is enabled.
++ LoopbackMap loopback_map_;
++
++ /// @brief Flag which indicates that the manager has been stopped.
++ bool stopped_;
++};
++
++/// @brief Defines a pointer to a TestablePingChannel
++typedef boost::shared_ptr<TestablePingChannel> TestablePingChannelPtr;
++
++/// @brief Defines a callback type for test completion check functions.
++typedef std::function<bool()> TestDoneCallback;
++
++/// @brief Test fixture class which uses an IOService for time management and/or IO
++class IOServiceTest : public ::testing::Test {
++public:
++ /// @brief Constructor.
++ ///
++ /// Starts test timer which detects timeouts.
++ IOServiceTest()
++ : test_io_service_(new asiolink::IOService()),
++ test_timer_(test_io_service_),
++ run_io_service_timer_(test_io_service_),
++ test_done_cb_() {
++ test_timer_.setup(std::bind(&IOServiceTest::timeoutHandler, this, true),
++ TEST_TIMEOUT,
++ asiolink::IntervalTimer::ONE_SHOT);
++ }
++
++ /// @brief Indicates if current user is not root
++ ///
++ /// @return True if neither the uid or the effective
++ /// uid is root.
++ static bool notRoot() {
++ return (getuid() != 0 && geteuid() != 0);
++ }
++
++ /// @brief Destructor.
++ ///
++ /// Removes active clients.
++ virtual ~IOServiceTest() {
++ test_timer_.cancel();
++ run_io_service_timer_.cancel();
++ test_io_service_->stopAndPoll();
++ }
++
++ /// @brief Callback function invoke upon test timeout.
++ ///
++ /// It stops the IO service and reports test timeout.
++ ///
++ /// @param fail_on_timeout Specifies if test failure should be reported.
++ void timeoutHandler(const bool fail_on_timeout) {
++ if (fail_on_timeout) {
++ ADD_FAILURE() << "Timeout occurred while running the test!";
++ }
++
++ test_io_service_->stop();
++ }
++
++ /// @brief Stops the IOService if criteria for test completion has been met.
++ ///
++ /// Stops the IOService If there either no test completion callback or the
++ /// call back returns true.
++ void stopIfDone() {
++ // If there is no done test callback or it returns true, stop the service.
++ if (!test_done_cb_ || (test_done_cb_)()) {
++ test_io_service_->stop();
++ }
++ }
++
++ /// @brief Posts a call to stop the io service to the io service.
++ ///
++ /// This should be used when stopping the service from callbacks on
++ /// thread pool threads.
++ void stopTestService() {
++ if (!test_io_service_->stopped()) {
++ test_io_service_->post([&]() { test_io_service_->stop(); });
++ }
++ }
++
++ /// @brief Runs IO service with optional timeout.
++ ///
++ /// @param timeout number of milliseconds to run the io service. Defaults to
++ /// zero which means run forever.
++ void runIOService(long timeout = 0) {
++ test_io_service_->stop();
++ test_io_service_->restart();
++
++ if (timeout > 0) {
++ run_io_service_timer_.setup(std::bind(&IOServiceTest::timeoutHandler,
++ this, false),
++ timeout,
++ asiolink::IntervalTimer::ONE_SHOT);
++ }
++
++ test_io_service_->run();
++ test_io_service_->stopAndPoll();
++ }
++
++ /// @brief IO service used in the tests.
++ asiolink::IOServicePtr test_io_service_;
++
++ /// @brief Asynchronous timer service to detect timeouts.
++ asiolink::IntervalTimer test_timer_;
++
++ /// @brief Asynchronous timer for running IO service for a specified amount
++ /// of time.
++ asiolink::IntervalTimer run_io_service_timer_;
++
++ /// @brief Callback function which event handlers can use to check if service
++ /// run should stop.
++ TestDoneCallback test_done_cb_;
++};
++
++} // end of namespace ping_check
++} // end of namespace isc
++
++#endif
+diff --git a/src/hooks/dhcp/ping_check/tests/run_unittests.cc b/src/hooks/dhcp/ping_check/tests/run_unittests.cc
+new file mode 100644
+index 0000000000..d249e2362e
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/tests/run_unittests.cc
+@@ -0,0 +1,19 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++#include <config.h>
++
++#include <log/logger_support.h>
++#include <gtest/gtest.h>
++
++int
++main(int argc, char* argv[]) {
++ ::testing::InitGoogleTest(&argc, argv);
++ isc::log::initLogger();
++ int result = RUN_ALL_TESTS();
++
++ return (result);
++}
+diff --git a/src/hooks/dhcp/ping_check/version.cc b/src/hooks/dhcp/ping_check/version.cc
+new file mode 100644
+index 0000000000..f2250ab126
+--- /dev/null
++++ b/src/hooks/dhcp/ping_check/version.cc
+@@ -0,0 +1,17 @@
++// Copyright (C) 2023-2025 Internet Systems Consortium, Inc. ("ISC")
++//
++// This Source Code Form is subject to the terms of the Mozilla Public
++// License, v. 2.0. If a copy of the MPL was not distributed with this
++// file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++#include <config.h>
++#include <hooks/hooks.h>
++
++extern "C" {
++
++/// @brief returns Kea hooks version.
++int version() {
++ return (KEA_HOOKS_VERSION);
++}
++
++}
+--
+2.39.5 (Apple Git-154)
+
diff --git a/scripts/package-build/libnss-mapuser/.gitignore b/scripts/package-build/libnss-mapuser/.gitignore
new file mode 100644
index 00000000..15657c19
--- /dev/null
+++ b/scripts/package-build/libnss-mapuser/.gitignore
@@ -0,0 +1 @@
+/libnss-mapuser/
diff --git a/scripts/package-build/libnss-mapuser/build.py b/scripts/package-build/libnss-mapuser/build.py
new file mode 120000
index 00000000..3c76af73
--- /dev/null
+++ b/scripts/package-build/libnss-mapuser/build.py
@@ -0,0 +1 @@
+../build.py \ No newline at end of file
diff --git a/scripts/package-build/libnss-mapuser/package.toml b/scripts/package-build/libnss-mapuser/package.toml
new file mode 100644
index 00000000..20ff65b4
--- /dev/null
+++ b/scripts/package-build/libnss-mapuser/package.toml
@@ -0,0 +1,9 @@
+[[packages]]
+name = "libnss-mapuser"
+commit_id = "current"
+scm_url = "https://github.com/vyos/libnss-mapuser.git"
+
+[dependencies]
+packages = [
+ "libaudit-dev"
+]